Linux memfd Seals Turn Mutable File State Into a Restricted Contract
A file returned by memfd_create() begins as mutable file state backed by memory-oriented storage, but Linux can progressively remove mutation operations from that file. When creation uses MFD_ALLOW_SEALING, fcntl() with F_ADD_SEALS can prohibit shrinking, growth, writes, future writes, or further changes to the seal set. The resulting restrictions belong to the inode, not to one descriptor, so passing another descriptor for the same object does not restore operations that a seal removed.
This mechanism creates a useful boundary for process-to-process data transfer. A producer can allocate and populate an anonymous file, constrain later mutation, then transfer a descriptor over a UNIX domain socket. A consumer receives a regular file-like object whose permitted mutation surface has already been narrowed by the kernel.
Anonymous naming does not make the object private to one process
memfd_create() returns a descriptor for an anonymous file. The supplied name is primarily diagnostic and does not create a normal pathname that applications use to reopen the object. The descriptor still follows ordinary descriptor semantics: fork() can inherit it, execve() can preserve it unless close-on-exec is set, and descriptor passing can transfer access to another process.
The file can be resized with ftruncate(), mapped with mmap(), and accessed with ordinary file operations subject to permissions and later seals. Its lifetime is therefore descriptor- and mapping-oriented rather than tied to a persistent directory entry.
That distinction matters for IPC designs. Anonymous placement removes pathname publication from the exchange, but it does not itself make the bytes immutable. A receiver holding a writable descriptor can still alter an unsealed object. Sealing is the separate mechanism that narrows that authority.
Seals are monotonic inode state
F_ADD_SEALS adds bits to the seal set. Successfully added seals cannot be removed. F_GET_SEALS reports the current set, and all descriptors referring to the same inode observe the same restrictions.
The monotonic property changes the coordination model. A producer does not hand a consumer a promise expressed only by convention. It can move the object through states whose permitted operations only decrease:
created
|
v
sized and populated
|
v
F_SEAL_SHRINK | F_SEAL_GROW
|
v
F_SEAL_WRITE
|
v
F_SEAL_SEALThis sequence is illustrative rather than mandatory. Applications select seals according to the mutation they need to prohibit. Once F_SEAL_SEAL is present, later attempts to add seals fail with EPERM, so that bit closes the seal configuration itself.
The kernel enforces the restrictions after a successful F_ADD_SEALS; cooperation from processes holding other descriptors is not required.
Size and content are separate mutation dimensions
F_SEAL_SHRINK prevents operations that reduce file size, while F_SEAL_GROW prevents operations that increase it. Neither seal alone makes existing bytes read-only.
That separation is significant for mapped data. If a consumer assumes a fixed layout, an unexpected shrink can invalidate pages beyond the new end of the file and make later accesses unsafe. Preventing shrink protects the extent boundary, while preventing writes protects content. A fixed, immutable payload commonly needs both size restrictions and a write restriction.
F_SEAL_WRITE blocks content modification through operations such as write() and relevant fallocate() modes. It also prevents creation of new shared writable mappings. Linux refuses to add F_SEAL_WRITE with EBUSY while a writable shared mapping already exists, because such a mapping would retain a path for modifying the file after the seal was supposedly established.
A producer that populated the object through MAP_SHARED | PROT_WRITE therefore needs to remove that writable shared mapping before successfully applying F_SEAL_WRITE.
F_SEAL_FUTURE_WRITE preserves existing shared writers
F_SEAL_FUTURE_WRITE defines a different boundary. It blocks later write() calls and later creation of writable mappings, but shared writable mappings established before the seal remain able to modify the file.
That behavior supports a producer that must continue updating a mapped region while preventing newly arriving consumers from obtaining an equivalent write path. It is not interchangeable with F_SEAL_WRITE: the existing producer mapping remains an active mutation channel.
The observable contract must account for that distinction. A consumer that requires bytes to remain stable cannot treat F_SEAL_FUTURE_WRITE alone as proof of immutability. Existing shared writable mappings may still change the payload.
Descriptor transfer does not weaken a seal
A UNIX domain socket can carry a file descriptor with SCM_RIGHTS. The receiving process obtains a descriptor referring to the same underlying open file object or inode state as appropriate to descriptor-passing semantics. File seals remain attached to the inode, so the receiver cannot bypass them merely by receiving a fresh descriptor number.
A compact publication sequence can therefore use kernel state as part of the handoff:
int fd = memfd_create("payload", MFD_ALLOW_SEALING | MFD_CLOEXEC);
ftruncate(fd, payload_size);
/* populate fd */
int seals = F_SEAL_SHRINK |
F_SEAL_GROW |
F_SEAL_WRITE |
F_SEAL_SEAL;
if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
/* do not publish the descriptor */
}
/* transfer fd only after sealing succeeds */Error handling is part of the protocol. If sealing fails, sending the descriptor anyway would publish an object with a weaker mutation contract than the receiver expects. The handoff boundary should therefore follow successful establishment of the required seals.
Seals constrain mutation, not interpretation
A sealed memfd does not validate the data stored inside it. The kernel can enforce that bytes or file size cannot be changed through prohibited operations, but it does not establish that a header is internally consistent, offsets are in range, a checksum is correct, or a serialized object matches an application schema.
This creates two distinct contracts. Seals constrain which file mutations remain possible. Application validation determines whether the fixed payload is acceptable for a particular parser or protocol.
The order can matter. A producer can populate and validate its own representation before sealing. A consumer receiving a fully write-sealed object can validate bytes without a concurrent writer changing them through ordinary write paths or shared writable mappings that would have prevented F_SEAL_WRITE from being added.
Sealing is Linux-specific state, not a portable file guarantee
memfd_create() and the sealing operations described here are Linux interfaces. Code that uses them should treat sealing as an operating-system contract rather than a C language property or a generic POSIX file guarantee.
Even on Linux, the exact seal set matters more than the label “sealed.” A file with only F_SEAL_SHRINK remains writable. A file with F_SEAL_FUTURE_WRITE may still have pre-existing shared writable mappings. A file with F_SEAL_SEAL merely prevents changes to the seal set; its other mutation properties depend on which additional seals are present.
The useful abstraction is therefore explicit capability reduction. The producer starts with a file it can shape, removes selected mutation operations at a deliberate publication boundary, and transfers a descriptor whose remaining behavior is enforced against every holder of that inode.