Applications often need a temporary chunk of data that behaves like a file without needing a persistent pathname. A process may build a configuration snapshot, compiled artifact, or serialized message, then map it into memory or pass it to another process.

A regular temporary file can do that, but it introduces filesystem naming, cleanup, permissions, and lifetime concerns. An anonymous mmap() avoids the pathname, but it does not produce an ordinary file descriptor that can be passed to APIs expecting file-backed data.

Linux provides memfd_create() for the gap between those models. It creates an anonymous, RAM-backed file and returns a normal file descriptor. The descriptor can be read, written, truncated, mapped with mmap(), inherited across fork(), or transferred to another process. When the last reference disappears, the kernel releases the object automatically.

The most distinctive feature is file sealing. A producer can finish writing a memfd and then ask the kernel to reject later writes, growth, shrinkage, or additional seal changes. That makes memfd useful for publishing an in-memory snapshot whose contents should no longer change.

Think of a memfd as a file without a filesystem name

A memfd behaves much more like a regular file than like raw anonymous memory.

memfd_create()
      |
      v
file descriptor
      |
      +--> read() / write()
      +--> ftruncate()
      +--> mmap()
      +--> send to another process

The file starts at size zero. You can grow it with ftruncate() before mapping it, or simply write data and let the file grow as normal writes extend it.

The name passed to memfd_create() is mainly diagnostic. Linux exposes it through paths such as /proc/self/fd/3, typically with a memfd: prefix, but that name is not a pathname you can use to discover the object later. Multiple memfds can use the same name without colliding.

The object’s lifetime follows references rather than directory entries. If no file descriptor, mapping, or other kernel reference remains, there is nothing to unlink and the object is released.

That is different from a named temporary file, whose directory entry and cleanup policy are part of the design.

Create the smallest useful memfd

memfd_create() is declared in <sys/mman.h> when GNU extensions are enabled:

#define _GNU_SOURCE
#include <sys/mman.h>

int fd = memfd_create("config-snapshot", MFD_CLOEXEC);
if (fd == -1) {
    /* handle error */
}

The returned descriptor is open for both reading and writing.

MFD_CLOEXEC sets the close-on-exec flag. Unless a descriptor is intentionally part of an executed program’s interface, close-on-exec is a useful default because it prevents accidental descriptor inheritance across execve().

At this point, the memfd has size zero. Writing to it works like writing to a regular file:

const char payload[] = "configuration-v1\n";

ssize_t n = write(fd, payload, sizeof payload - 1);
if (n != (ssize_t)(sizeof payload - 1)) {
    /* handle short write or error */
}

For production code, a general-purpose writer should loop until all bytes are written because write() is not guaranteed to consume the entire buffer in every situation. The single-write example is intentionally small so the file behavior is easy to see.

Add sealing when the data becomes a snapshot

A plain memfd is still mutable. Any process holding a writable descriptor can change its contents or resize it.

If you want the object to become an immutable snapshot, create it with MFD_ALLOW_SEALING:

int fd = memfd_create(
    "config-snapshot",
    MFD_CLOEXEC | MFD_ALLOW_SEALING
);

Without MFD_ALLOW_SEALING, Linux creates the memfd with F_SEAL_SEAL already set, which prevents adding other seals later. Request sealing up front if you plan to use it.

After populating the file, add the restrictions you need with fcntl():

#include <fcntl.h>

int seals = F_SEAL_WRITE |
            F_SEAL_GROW |
            F_SEAL_SHRINK |
            F_SEAL_SEAL;

if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
    /* handle error */
}

These seals have separate meanings:

  • F_SEAL_WRITE prevents modification of file contents through writes and certain other operations.
  • F_SEAL_GROW prevents increasing the file size.
  • F_SEAL_SHRINK prevents reducing the file size.
  • F_SEAL_SEAL prevents adding any more seals.

The distinction matters. F_SEAL_WRITE alone does not freeze the file size, so a snapshot that must remain structurally unchanged normally combines write, grow, and shrink seals.

Seals belong to the underlying file object, not to one particular descriptor. If another process receives a descriptor for the same memfd, it sees the same seals. Seals can be added but not removed.

Build and freeze a complete snapshot

Here is a complete example that creates a memfd, writes a payload, seals it, reads it back, and verifies that a later write is rejected:

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    const char payload[] = "configuration-v1\n";

    int fd = memfd_create(
        "config-snapshot",
        MFD_CLOEXEC | MFD_ALLOW_SEALING
    );
    if (fd == -1) {
        perror("memfd_create");
        return EXIT_FAILURE;
    }

    ssize_t written = write(fd, payload, sizeof payload - 1);
    if (written != (ssize_t)(sizeof payload - 1)) {
        perror("write");
        close(fd);
        return EXIT_FAILURE;
    }

    int seals = F_SEAL_WRITE |
                F_SEAL_GROW |
                F_SEAL_SHRINK |
                F_SEAL_SEAL;

    if (fcntl(fd, F_ADD_SEALS, seals) == -1) {
        perror("fcntl(F_ADD_SEALS)");
        close(fd);
        return EXIT_FAILURE;
    }

    if (lseek(fd, 0, SEEK_SET) == -1) {
        perror("lseek");
        close(fd);
        return EXIT_FAILURE;
    }

    char buffer[64];
    ssize_t n = read(fd, buffer, sizeof buffer - 1);
    if (n == -1) {
        perror("read");
        close(fd);
        return EXIT_FAILURE;
    }

    buffer[n] = '\0';
    printf("%s", buffer);

    if (write(fd, "x", 1) != -1 || errno != EPERM) {
        fprintf(stderr, "sealed write did not fail with EPERM\n");
        close(fd);
        return EXIT_FAILURE;
    }

    close(fd);
    return EXIT_SUCCESS;
}

After F_ADD_SEALS succeeds, the restrictions are enforced by the kernel. The program can still read the data, but a later write() is rejected with EPERM because F_SEAL_WRITE is present.

This produces a useful ownership transition: first the producer has a mutable construction buffer; then it publishes a read-only snapshot.

Sealing interacts with memory mappings

A memfd can also back an mmap() mapping. That is useful when a consumer wants random access without issuing repeated read() calls.

There is an important boundary around F_SEAL_WRITE: Linux refuses to add that seal while a shared writable mapping of the file still exists. The F_ADD_SEALS call fails with EBUSY in that case.

The reason is practical. A shared writable mapping is already a path to modify the file. The kernel cannot truthfully promise that future modifications are forbidden while such a mapping remains active.

A common sequence is therefore:

create memfd
    -> size it
    -> map writable
    -> populate data
    -> unmap writable mapping
    -> add write/grow/shrink seals
    -> give consumers read-only access

If an application deliberately needs existing shared writable mappings to remain usable while preventing future writers, Linux also has F_SEAL_FUTURE_WRITE. Its semantics differ from F_SEAL_WRITE, so choose it only when that distinction is part of the protocol rather than as a drop-in substitute.

Pass the descriptor instead of inventing a pathname

One of memfd’s strongest properties is that the file descriptor itself is the handle.

A process created with fork() inherits the descriptor in the usual way. Unrelated processes can transfer it over a UNIX domain socket using descriptor passing. The receiver then operates on the same underlying file and can inspect its seals with F_GET_SEALS.

That model is useful for IPC because it separates two concerns:

control channel:   "here is a snapshot"
data object:        memfd descriptor

The control message can stay small while the bulk data remains in a file-like object that can be mapped or read directly.

Do not treat /proc/<pid>/fd/<n> as the primary application protocol for transferring the object. Those paths are useful for inspection and some specialized workflows, but descriptor passing gives ownership and lifetime much more explicitly.

Understand what memfd does not guarantee

The name “memory file” can encourage a few incorrect assumptions.

First, memfd is Linux-specific. It is not a POSIX interface, so portable applications need another abstraction or platform-specific implementation.

Second, “lives in RAM” does not mean every byte is permanently resident in physical memory. The interface gives anonymous, volatile file-backed storage semantics; it should not be treated as a mechanism for pinning pages or bypassing normal virtual-memory management.

Third, memfd is not persistent storage. If the application needs data to survive process lifetime, reboot, or explicit handoff into a durable filesystem location, use a real file and the appropriate durability protocol.

Finally, seals constrain operations on the memfd, but they do not validate the data. A consumer still needs to parse lengths, offsets, formats, and other untrusted input defensively. An immutable malformed object is still malformed.

Choose memfd, anonymous mmap, or a temporary file deliberately

These mechanisms overlap, but their useful properties differ.

Use an anonymous mmap() when memory is primarily private process state and you do not need file semantics or descriptor-based sharing.

Use a named or unnamed temporary filesystem file when normal filesystem behavior matters, when another component expects a path, or when you may eventually rename or persist the data.

Use memfd_create() when you specifically want a file descriptor backed by volatile memory, especially when the object will be mapped, passed between processes, or sealed after construction.

Sealing is the feature that often makes the decision clear. If a producer needs to create data once and then give consumers a kernel-enforced guarantee that the object cannot be modified or resized through that file, a sealed memfd is a strong fit.

Common mistakes to avoid

Creating a memfd without MFD_ALLOW_SEALING and then trying to add seals will not work because the initial F_SEAL_SEAL prevents later additions.

Adding only F_SEAL_WRITE does not prevent size changes. Combine it with F_SEAL_GROW and F_SEAL_SHRINK when fixed size is part of the invariant.

Trying to add F_SEAL_WRITE while a shared writable mapping exists fails with EBUSY. Finish mutable mapped access and unmap it before sealing.

Also avoid using a memfd merely because it sounds faster than a temporary file. Performance depends on workload, memory pressure, I/O patterns, and what the alternative filesystem is doing. Choose memfd for its lifetime, descriptor, mapping, and sealing semantics first; measure performance separately if it matters.

Conclusion

memfd_create() gives Linux programs a useful hybrid: an anonymous object with ordinary file semantics and automatic lifetime management.

The basic workflow is straightforward: create the memfd, populate it, optionally map it, then add seals when the data becomes a snapshot. Once write, grow, and shrink seals are in place, consumers can receive the descriptor knowing that those classes of mutation will be rejected by the kernel.

That makes memfd especially useful at process boundaries where a file descriptor is convenient but a persistent pathname is unnecessary. The key is to treat sealing as a protocol transition from mutable construction to immutable publication, not simply as another file flag.