Linux memfd File Seals: Immutable Shared Data Without a Pathname
Publish Linux memfd data safely with file seals, understand writable mappings and descriptor transfer, and distinguish immutable blobs from secret memory.
Linux memfd_create() returns a file descriptor for an anonymous, RAM-backed file that does not need a normal pathname in a mounted directory. One of its most useful applications is safely sharing a completed data object between cooperating processes. File seals can prevent later writes or size changes after the producer has finished building the object.
Sealing can remove a time-of-check/time-of-use race in which one process validates shared bytes and another modifies them before they are consumed. It is not encryption, a process sandbox, or a complete authorization system. The descriptor itself grants access to the underlying object, so transferring it changes who can read, map, and potentially modify the data according to the seals in force.
Create a memfd that permits seals
Call memfd_create() with MFD_ALLOW_SEALING if the application intends to add seals. Without that flag, the object starts with F_SEAL_SEAL, which prevents adding further seals. MFD_CLOEXEC prevents accidental descriptor inheritance across execve().
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stddef.h>
#include <sys/mman.h>
#include <unistd.h>
static int write_all(int fd, const void *buffer, size_t length) {
const unsigned char *p = buffer;
while (length > 0) {
size_t request = length > (size_t)SSIZE_MAX
? (size_t)SSIZE_MAX : length;
ssize_t n = write(fd, p, request);
if (n > 0) {
p += (size_t)n;
length -= (size_t)n;
continue;
}
if (n == -1 && errno == EINTR)
continue;
if (n == 0)
errno = EIO;
return -1;
}
return 0;
}
int create_sealed_blob(const void *bytes, size_t length) {
int fd = memfd_create("validated-config",
MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd == -1)
return -1;
if (write_all(fd, bytes, length) == -1) {
int saved_errno = errno;
(void)close(fd);
errno = saved_errno;
return -1;
}
int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_WRITE | F_SEAL_SEAL;
if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
int saved_errno = errno;
(void)close(fd);
errno = saved_errno;
return -1;
}
return fd;
}
This example assumes write_all() handles partial writes, EINTR, and zero-length progress, and that length fits the process and kernel’s file-size types. A production implementation should validate the content before applying the seals, impose a maximum object size, and close the descriptor on every failed stage. The returned descriptor is writable in its original open mode, but F_SEAL_WRITE prevents later writes once it has been successfully applied.
MFD_ALLOW_SEALING only enables later sealing; it does not apply a seal by itself. Populate and validate the object first, then apply the exact seal set needed by consumers. After F_SEAL_SEAL is applied, no additional seals can be added, so include it only in the final F_ADD_SEALS operation. When several seals are set together, that single call is the final operation.
Understand what each seal protects
F_SEAL_GROW prevents increasing the file size. F_SEAL_SHRINK prevents decreasing it. These protect mappings from one class of peer-induced size changes that could otherwise make offsets invalid or cause SIGBUS when a process accesses a page beyond a shortened file.
F_SEAL_WRITE prevents modification through writes and shared writable mappings. It cannot be added while a shared writable mapping of the file remains active; unmap all such mappings before applying it. Once the seal succeeds, a recipient can map the object read-only and rely on the bytes remaining unchanged through the supported file interfaces.
F_SEAL_FUTURE_WRITE provides a different boundary. It prevents future writes through write() and future shared writable mappings, while existing shared writable mappings remain usable. It can be useful when the producer already has a mapping that must stay writable but wants to prevent new writers. It is not equivalent to F_SEAL_WRITE: code with an existing writable mapping can still change bytes.
F_SEAL_SEAL makes the seal set immutable by disallowing additional seals. It does not itself prevent writes, growth, or shrinkage; combine it with the seals that express the actual data-integrity policy. Always inspect F_GET_SEALS in the receiving process if the receiver must verify a specific trust contract.
Coordinate mappings, writes, and sealing
An application can populate a memfd with write() or pwrite(), or size it with ftruncate() and populate a mapping. With a shared writable mapping, F_SEAL_WRITE must wait until all such mappings are removed. The producer can otherwise apply size seals and a future-write seal according to the desired lifecycle.
Define the transition clearly: the producer owns mutation while constructing the object; validation occurs over the completed representation; the seal operation establishes the immutable state; only then is the descriptor published to consumers. Do not send the descriptor first and rely on a later seal unless recipients are explicitly trusted to wait. A consumer can map and inspect the object before the producer finishes sealing it.
Seals do not validate the data. The producer must verify lengths, format versions, checksums, and resource limits before sending the descriptor. A sealed malformed object is still malformed. If a consumer parses hostile data, it must retain normal bounds checks and memory-safety defenses even when the bytes cannot change during parsing.
Transfer the descriptor as authority
The descriptor can be inherited across fork() or transferred through a Unix-domain socket with SCM_RIGHTS. Passing an integer descriptor number over a text protocol does not transfer the kernel reference. Use descriptor-passing APIs, authenticate the sender, and establish which process owns closure and who may create additional mappings.
A receiver should check the seals and file size before mapping. Map read-only when mutation is not required. Keep the descriptor open for the mapping lifecycle and close it according to a clear ownership policy. A mapping may remain usable after one descriptor closes, and another process can hold a duplicate reference; sealing protects integrity but does not reclaim memory until all references and mappings are gone.
The object is not a durable disk file. memfd_create() creates an anonymous memory-backed object that disappears after the last reference is released. Do not use it as a substitute for persistent storage, crash recovery, or a filesystem permission model. Do not assume its descriptor is secret: /proc/PID/fd, inherited descriptors, and descriptor passing can expose it to other actors according to system permissions.
Keep file seals distinct from memfd_secret
memfd_create() plus seals addresses object sharing and mutation control. memfd_secret() is a separate Linux interface intended to isolate selected secret pages from ordinary direct-map access. A normal memfd is not “secret memory,” and seals do not provide confidentiality. Conversely, secret memory does not provide the same immutable shared-object semantics as a sealed memfd.
Use sealed memfd data for immutable configuration snapshots, compiler artifacts, validated policy blobs, or shared lookup tables. Use secret memory only when the threat model specifically calls for its documented kernel-memory exposure mitigation and the deployment accepts its lifecycle and resource constraints. Keep the two claims separate in architecture documents and security reviews.
Handle kernel support and resource limits
memfd_create() arrived in Linux 3.17, and file-sealing features have evolved over time. Newer flags may not exist in old headers or may be unsupported by an older runtime kernel. Feature-test the exact call and flags that the program requires, and treat EINVAL or ENOSYS as a compatibility result only after validating the arguments.
Anonymous files still consume system resources. Bound the object size, account for memory pressure and process file-descriptor limits, and define behavior when creation, resizing, mapping, or seal application fails. Do not silently fall back to a mutable shared buffer if immutability is part of the security contract. A fallback must either preserve the contract with a different mechanism or reject the operation.
Test the complete publication sequence
Test descriptor creation with and without MFD_ALLOW_SEALING, writes before and after each seal, growth and shrink attempts, shared writable mappings, F_SEAL_FUTURE_WRITE with an existing mapping, receiver-side F_GET_SEALS, descriptor transfer, execve() inheritance, and teardown after one peer exits. Confirm the receiver sees the exact validated bytes and that attempts to mutate them fail in the expected way.
Inject failure between population, validation, sealing, and descriptor transfer. Confirm the producer does not publish a partially initialized object and that cleanup closes its references. Use a Linux kernel and headers matching the supported deployment matrix; compiling on a different operating system does not test Linux seal semantics.
File seals are a small kernel-enforced immutability contract around an otherwise ordinary file-like object. They work best when the producer’s state transition is explicit, the data is validated before publication, and the receiving process verifies the seals it depends on rather than trusting an informal promise.
Related:
- Linux memfd_secret: Isolating Sensitive Userspace Memory
- Linux pidfds: Race-Free Process Handles Beyond Numeric PIDs
Sources: