A file descriptor returned by memfd_create() can begin as a writable, resizable anonymous file and later become an object whose permitted mutation operations have been permanently reduced. Linux implements that transition with file seals. The mechanism is attached to the underlying file rather than to one descriptor, so passing a duplicate descriptor across a process boundary does not create an independent sealing state.
This property makes sealing more than a convenience around temporary storage. It changes the authority carried by every descriptor that refers to the same memfd object. The transition is monotonic: seals can be added, but they cannot be removed.
Sealing is an inode-level restriction
memfd_create() creates an anonymous file and returns an open file descriptor for it. The object behaves like a regular file for operations such as ftruncate(), write(), pread(), and mmap(), subject to the flags and permissions relevant to each operation.
Sealing must be enabled when the object is created:
int fd = memfd_create("payload", MFD_CLOEXEC | MFD_ALLOW_SEALING);Without MFD_ALLOW_SEALING, Linux creates the file with F_SEAL_SEAL already set, which prevents adding further seals. With sealing enabled, the initial seal set is empty.
A process adds restrictions through fcntl(fd, F_ADD_SEALS, mask) and reads the current set through fcntl(fd, F_GET_SEALS). The seal set belongs to the file object. dup(), descriptor passing over a Unix-domain socket, and other ways of acquiring another descriptor to that same object do not copy the file into a new sealing domain.
That scope is central to the capability boundary. If one holder successfully adds F_SEAL_GROW, another holder cannot bypass the restriction merely by using a descriptor obtained earlier.
Size seals separate growth from truncation
F_SEAL_GROW prevents increasing the file size. F_SEAL_SHRINK prevents reducing it. They are separate restrictions because size changes have different consequences for consumers.
A producer can populate a memfd, set its final length, then add both seals:
if (ftruncate(fd, payload_size) == -1)
return -1;
if (fcntl(fd, F_ADD_SEALS, F_SEAL_GROW | F_SEAL_SHRINK) == -1)
return -1;After those seals are installed, operations that would violate the fixed-size boundary fail. Existing bytes are not made immutable by size seals alone. A holder can still modify data within the established range unless another seal removes that authority.
This distinction matters for protocols that communicate offsets into a shared object. Fixed length can stabilize the valid address range without stabilizing the byte values stored inside it.
F_SEAL_WRITE has a mapping precondition
F_SEAL_WRITE removes write authority more broadly. Once installed, write-oriented operations against the file are rejected, and new shared writable mappings cannot be created.
The transition cannot always occur immediately. If a writable MAP_SHARED mapping of the file already exists, adding F_SEAL_WRITE fails with EBUSY. Linux refuses to claim the stronger immutability property while a mapping that can modify the file remains active.
That behavior exposes a lifecycle constraint:
create -> size -> populate -> remove writable shared mappings
-> add F_SEAL_WRITE -> distributeThe ordering is not merely stylistic. A producer that distributes the descriptor before completing the transition may allow another process to establish a writable shared mapping. That mapping can then prevent F_SEAL_WRITE from being installed.
A protocol that relies on full write sealing therefore needs ownership rules for the mutable phase. Descriptor transfer is itself a change in who can interfere with the transition.
F_SEAL_FUTURE_WRITE permits a staged handoff
Linux also provides F_SEAL_FUTURE_WRITE. It blocks new write authority while allowing writable shared mappings that already exist to continue modifying the file through those mappings.
This creates a different boundary from F_SEAL_WRITE. A producer can retain an existing writable mapping for controlled updates while preventing newly created writable mappings and ordinary write operations from extending mutation authority to additional paths. The existing mapping remains an intentional exception.
The distinction is observable. A consumer receiving the descriptor after F_SEAL_FUTURE_WRITE cannot infer that the bytes are immutable, because a pre-existing writable mapping may still change them. The seal constrains future acquisition of write access; it does not revoke write access already represented by such mappings.
For publication protocols, that makes F_SEAL_FUTURE_WRITE suitable only when the remaining writer is part of the stated contract. Treating it as equivalent to F_SEAL_WRITE would erase a live mutation channel from the model.
F_SEAL_SEAL closes the policy transition
F_SEAL_SEAL prevents any additional seals from being added. It does not strengthen the data restrictions on its own; instead, it freezes the current seal policy.
Ordering therefore matters. Adding F_SEAL_SEAL before the desired data seals permanently prevents the missing restrictions from being installed. A final transition can combine the intended seals in one F_ADD_SEALS operation:
int seals = F_SEAL_GROW |
F_SEAL_SHRINK |
F_SEAL_WRITE |
F_SEAL_SEAL;
if (fcntl(fd, F_ADD_SEALS, seals) == -1)
return -1;This call still depends on the preconditions of every requested seal. In particular, an incompatible writable shared mapping can make the operation fail rather than leaving the caller with the requested final state. Code must check the result before treating the object as sealed.
F_GET_SEALS can then expose the kernel-maintained policy state to a receiver. Checking that state is stronger than trusting metadata supplied beside the descriptor because the restrictions are enforced on operations against the object itself.
Seals constrain mutation, not every form of identity or persistence
A sealed memfd remains a file descriptor referring to a kernel object with a lifetime determined by references to that object. Sealing does not assign a persistent pathname, turn volatile storage into durable storage, or make descriptor numbers globally meaningful.
It also does not make application-level interpretation safe by itself. A receiver still needs bounds checks, format validation, version rules, and synchronization appropriate to the data protocol. Seals can prevent selected kernel-visible mutations; they do not validate the bytes that were present before the restriction was installed.
The useful architectural property is narrower and stronger: a mutable producer can deliberately reduce the set of kernel operations that future holders may perform on the same file object. Because the reduction is monotonic and shared across descriptors, the kernel can enforce a capability transition that process-local bookkeeping cannot reproduce after descriptors cross process boundaries.
Publication depends on the transition point
The most consequential boundary is the moment at which the producer stops being a mutable owner and starts distributing a restricted object. Before that point, writable mappings, truncation, and writes may be valid parts of construction. After the required seals are confirmed, those operations can be excluded according to the installed mask.
That transition gives descriptor-based protocols a concrete state change rather than a convention such as “do not modify after sending.” It also makes races visible in API behavior: a stray writable shared mapping can block full write sealing, an early F_SEAL_SEAL can freeze an incomplete policy, and F_SEAL_FUTURE_WRITE can intentionally preserve an existing writer.
The resulting contract is precise when each seal is treated as a specific removed authority. Fixed size, blocked writes, preserved existing mappings, and a frozen seal set are separate properties. Combining them deliberately produces a file-backed capability whose mutation surface matches the process-boundary protocol that carries it.