memfd_create and file sealing: an anonymous file nobody can modify (including you)
Published by RodHat

There’s a pattern you’ve probably written a dozen times: you have a blob of data (a config, a shared buffer, a compiled artifact) and you need to hand it to another process without it changing on you. The usual answers are: write it to a file (now there’s a path someone can muck with), use shm_open() (creates a name under /dev/shm, same problem), or mmap() anonymous memory (no handle you can pass). None of these are right.
memfd_create() has been in the kernel since 3.17 (2014) and almost nobody uses it outside of Wayland and systemd. That’s a shame, because it solves exactly this problem and the file sealing API that comes with it is one of the more useful kernel features of the last decade.
The idea: memfd_create(name, flags) creates a file descriptor backed by anonymous memory. The name appears under /proc/<pid>/fd/<n> for debugging but it’s not a real filesystem path: nothing can open it by name. You resize it with ftruncate(), write to it with write() or mmap(), and when you’re done, you seal it. Sealed means sealed. The kernel will refuse any call that would modify the file, forever, regardless of who holds the fd.
The basics
#define _GNU_SOURCE
#include <sys/mman.h> /* memfd_create */
#include <linux/memfd.h> /* MFD_CLOEXEC, MFD_ALLOW_SEALING */
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
int main(void) {
/* Create the anonymous file. MFD_ALLOW_SEALING is required if you
intend to add seals later, since it's not the default. */
int fd = memfd_create("myconfig", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd < 0) {
perror("memfd_create");
return 1;
}
const char *data = "host=db.internal\nport=5432\n";
size_t len = strlen(data);
/* Size it, then write */
ftruncate(fd, (off_t)len);
write(fd, data, len);
/* Seal: prevent any future ftruncate or write.
F_SEAL_SHRINK + F_SEAL_GROW locks the size.
F_SEAL_WRITE blocks write() and writable mmap(). */
if (fcntl(fd, F_ADD_SEALS,
F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) < 0) {
perror("F_ADD_SEALS");
return 1;
}
/* F_SEAL_SEAL prevents anyone from adding more seals later.
With all four set, the fd is now permanently immutable. */
printf("sealed memfd, fd=%d\n", fd);
/* Read it back: RDONLY mmap still works after F_SEAL_WRITE */
lseek(fd, 0, SEEK_SET);
char buf[256] = {};
read(fd, buf, sizeof(buf) - 1);
printf("contents: %s", buf);
return 0;
}
The four seals:
| Seal | Blocks |
|---|---|
F_SEAL_SHRINK | ftruncate shrinking the file |
F_SEAL_GROW | ftruncate growing the file, write() past EOF |
F_SEAL_WRITE | write() and writable mmap() (PROT_WRITE shared) |
F_SEAL_SEAL | F_ADD_SEALS itself, so no future seals can be added |
Stack them in one fcntl() call with bitwise OR. The F_SEAL_WRITE requirement is important: you can only add it if there are no existing writable mmap() mappings of the file at the time of the call. The kernel checks and returns EBUSY if there are.
Passing it to another process
An memfd is just an fd. You pass it the same way you pass any fd: SCM_RIGHTS over a Unix socket, or pidfd_getfd() if you have a pidfd for the target process.
/* Sender side, assuming you have a connected Unix socket 'sock'
and a sealed memfd 'mfd' */
struct msghdr msg = {};
struct iovec iov;
char dummy = 'x';
iov.iov_base = &dummy;
iov.iov_len = 1;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
/* Control message carrying the fd */
char cbuf[CMSG_SPACE(sizeof(int))];
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &mfd, sizeof(int));
sendmsg(sock, &msg, 0);
On the receiving end, the kernel delivers a new fd number that points at the same underlying anonymous file object. The seals travel with the file, so the recipient cannot unseal, cannot write, cannot resize. It doesn’t matter how many processes hold the fd; the seals are on the inode, not the descriptor.
This is the mechanism Wayland compositors use to pass pixel buffers between the compositor and clients without copying: client writes the buffer, seals it, sends the fd, compositor maps it read-only. No copy, no path, no race condition on file contents.
Why not shm_open?
shm_open(3) creates a named object under /dev/shm (on Linux, backed by tmpfs). The name is real. Any process with the right permissions can open it by name, truncate it, write to it, or unlink it. You’re supposed to call shm_unlink() after opening to remove the name, but that only works if you do it in the right order and nothing races you.
memfd_create has no name. There is no path to race on. The only way to get access to the file is via the fd itself, through SCM_RIGHTS or pidfd_getfd() or inheriting it across fork(). Once sealed, it’s immutable regardless of how many copies of the fd exist. This is categorically better for IPC where you want immutability and no ambient name authority.
# See your memfd in /proc -- it shows up but you can't open it by path
ls -la /proc/$$/fd/ | grep memfd
# lrwx------ 1 rod rod 64 Aug 27 14:23 5 -> /memfd:myconfig (deleted)
# The "(deleted)" is cosmetic -- the file exists as long as the fd is open.
Sealing without MFD_ALLOW_SEALING
If you forget MFD_ALLOW_SEALING in the memfd_create() call, F_ADD_SEALS returns EPERM. Always. You can’t retrofit it. Get the flags right at creation time or start over.
# Check what seals are currently on an fd (say fd 5 in your own process)
python3 -c "
import fcntl, os
fd = int(input('fd: '))
seals = fcntl.fcntl(fd, fcntl.F_GET_SEALS)
names = {1:'SEAL_SEAL', 2:'SEAL_SHRINK', 4:'SEAL_GROW', 8:'SEAL_WRITE'}
print(' | '.join(v for k,v in names.items() if seals & k) or 'none')
"
F_GET_SEALS returns the current seal bitmask. Useful for a recipient to verify that the sender actually sealed what they claimed.
Practical: immutable config injection into a child process
Combine memfd_create with CLONE_PIDFD from the pidfd post and you get a clean pattern: create and seal a config memfd in the parent, fork the child with clone3, inject the fd into the child’s table with pidfd_getfd(). The child gets the config via a known fd number, and the seal guarantees it reads exactly what the parent put there.
/* After fork with CLONE_PIDFD, in the parent: */
/* Inject the sealed memfd as fd 3 in the child.
Note: pidfd_getfd duplicates *from* the other process's table.
To inject *into* the child, use dup2 in the child before exec,
or pass via SCM_RIGHTS if the child has a socket it reads at startup. */
/* The typical pattern: parent sends the sealed fd over a pipe
before exec, child reads it from the pipe as its first act. */
The point isn’t a single clever trick; it’s that memfd fds compose with the rest of the modern Linux fd API. They work with epoll, sendfile, copy_file_range, splice. They show up in /proc/<pid>/fd/. They’re just files with no names and optional permanent immutability.
Checking kernel support
memfd_create is in glibc since 2.27. On older systems:
#include <sys/syscall.h>
/* x86-64: SYS_memfd_create = 319, arm64 = 279 */
static int memfd_create(const char *name, unsigned flags) {
return (int)syscall(SYS_memfd_create, name, flags);
}
The sealing API requires MFD_ALLOW_SEALING which is the same kernel (3.17). If you’re on 3.17+, you have both.
uname -r # need >= 3.17, realistically you're on 5.x+ by now
The pidfd post covers the pidfd_getfd() call that lets you move fds between processes without Unix sockets. The io_uring post shows how to use registered file tables: you can register a sealed memfd as a fixed file and do zero-copy reads against it. Kerrisk’s The Linux Programming Interface covers mmap() and the anonymous memory model in detail; the memfd sealing API built on top of that model is in the kernel docs under Documentation/userspace-api/sysfs-platform_profile.rst, no, wait, it’s in Documentation/filesystems/memfd.rst. Read both.