A memfd_create() file starts as a mutable anonymous file. It can be resized, written, and mapped much like a regular file, while its storage remains volatile and disappears after the last reference is released. File sealing adds a different phase to that lifecycle: after data has been populated, the kernel can permanently reject selected classes of later modification.
That transition is useful when one process prepares bytes and then hands the same file description to another process. The receiver can inspect the seals attached to the inode instead of relying only on a convention that the sender will stop changing the object.
Seals attach to the inode
Sealing is enabled when the memfd is created with MFD_ALLOW_SEALING. Without that flag, a memfd starts with F_SEAL_SEAL, which prevents additional seals from being added.
int fd = memfd_create("snapshot", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd == -1)
abort();
if (ftruncate(fd, 4096) == -1)
abort();The seal set belongs to the inode, not to one descriptor number. Duplicated descriptors and descriptors transferred to another process therefore observe the same restrictions. Seals are monotonic: F_ADD_SEALS can add restrictions, but there is no operation that removes them.
This makes sealing a state transition rather than a temporary lock.
no seals
|
+--> SHRINK
| |
| +--> GROW
| |
| +--> WRITE
| |
| +--> SEAL
|
`-- restrictions only accumulateF_SEAL_SEAL closes the transition itself. Once present, later F_ADD_SEALS calls fail with EPERM.
Size and contents are separate properties
F_SEAL_SHRINK prevents reducing the file size. F_SEAL_GROW prevents increasing it. They are independent because a protocol may need to prohibit one direction before the other.
A producer that has finished defining a fixed-size payload can add both:
int seals = F_SEAL_SHRINK | F_SEAL_GROW;
if (fcntl(fd, F_ADD_SEALS, seals) == -1)
abort();Afterward, operations that would change the size across those boundaries fail. The existing bytes are still writable unless a write seal is also present.
That distinction matters for shared-memory formats. A stable length prevents a peer from truncating a mapped object and exposing another process to accesses beyond the new end of the file, but it does not make the payload immutable.
F_SEAL_WRITE requires writable shared mappings to be gone
F_SEAL_WRITE blocks modification of file contents through operations such as write(). It also prevents creation of new shared writable mappings.
The kernel will not add F_SEAL_WRITE while an existing writable shared mapping is active. F_ADD_SEALS fails with EBUSY in that state. A producer that populated the object through MAP_SHARED | PROT_WRITE therefore needs to remove that mapping before applying the seal.
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
abort();
memcpy(p, payload, payload_len);
if (munmap(p, 4096) == -1)
abort();
int seals = F_SEAL_SHRINK |
F_SEAL_GROW |
F_SEAL_WRITE |
F_SEAL_SEAL;
if (fcntl(fd, F_ADD_SEALS, seals) == -1)
abort();The ordering creates a clear publication boundary:
create -> size -> map writable -> populate -> unmap
|
v
add seals
|
v
transfer fdOnce the full seal set succeeds, later holders cannot reopen a mutation path covered by those seals.
F_SEAL_FUTURE_WRITE permits an existing writer to remain
F_SEAL_FUTURE_WRITE has a narrower boundary. It blocks later write() calls and prevents creation of new writable shared mappings, while shared writable mappings that already existed when the seal was installed may continue modifying the file.
That behavior supports a producer-owned update window. A process can keep its existing writable mapping while preventing a newly receiving process from establishing another writable mapping.
producer mapping: writable before seal
|
+---- remains writable
|
F_SEAL_FUTURE_WRITE added
|
+---- new write() rejected
`---- new shared writable mmap rejectedThis is not equivalent to immutable publication. Bytes can still change through the preexisting mapping. A consumer that requires stable contents needs a stronger lifecycle boundary, such as removal of writable mappings followed by F_SEAL_WRITE.
The difference between the two seals is therefore about existing mutation authority. F_SEAL_WRITE requires that writable shared mappings no longer exist when it is added; F_SEAL_FUTURE_WRITE can coexist with mappings that were already writable.
The receiver can verify the contract
A process receiving a memfd over a UNIX domain socket can query the current seal mask with F_GET_SEALS.
int seals = fcntl(fd, F_GET_SEALS);
if (seals == -1)
abort();
int required = F_SEAL_SHRINK |
F_SEAL_GROW |
F_SEAL_WRITE |
F_SEAL_SEAL;
if ((seals & required) != required)
reject_object();This check turns assumptions about the object into kernel-visible state. A protocol can require a specific seal set before parsing data that is expected to remain fixed.
The check should match the actual invariant. F_SEAL_WRITE alone does not prohibit size changes. F_SEAL_GROW | F_SEAL_SHRINK alone does not prohibit overwriting bytes. F_SEAL_SEAL only prevents future changes to the seal set; it does not itself freeze file data.
Descriptor transfer does not weaken seals
A memfd can be transferred between processes with SCM_RIGHTS over a UNIX domain socket. The receiving process obtains a descriptor referring to the same underlying file. Since seals are inode properties, transfer does not create an unsealed copy.
producer consumer
memfd fd
|
add seals
|
sendmsg(SCM_RIGHTS) ----------> recvmsg()
|
v
received fd
|
v
F_GET_SEALSThis property is different from sending raw bytes and trusting a separate metadata field that says the source is immutable. The restriction remains attached to the kernel object that carries the data.
File seals do not provide authentication or confidentiality. A receiver still needs a protocol for deciding which peer is trusted to send an object, and a memfd does not hide its contents from processes that legitimately receive access. Sealing constrains mutation of the object; it does not establish peer identity.
Sealing narrows the shared-memory race surface
Mutable shared memory makes validation difficult when another participant can alter bytes between a check and later use. Copying the payload into private memory is one way to create a stable local snapshot. A fully sealed memfd provides another boundary when sharing the same backing object is desirable.
The useful guarantee is specific: once the relevant seals have been successfully installed, operations covered by those seals are rejected by the kernel. The guarantee does not retroactively validate bytes written before sealing, and it does not assign meaning to the data format.
For a producer-consumer protocol, that separates two concerns cleanly. The producer is responsible for constructing valid bytes before publication. The kernel seal set can then preserve the size and content properties that the consumer expects after publication.
References
- Linux
memfd_create(2): https://man7.org/linux/man-pages/man2/memfd_create.2.html - Linux
F_GET_SEALS(2const): https://man7.org/linux/man-pages/man2/F_GET_SEALS.2const.html