memfd_secret: memory the kernel itself cannot read
Published by RodHat

mlock(2) pins pages in RAM and keeps them out of swap. The kernel still owns them. Any kernel module with a pointer to your physical memory can read every byte. ptrace through /proc/PID/mem can read them. /proc/kcore can reach them. Cold-boot analysis tools find them exactly where they expect them.
mlock is the right answer for secret memory on most systems. But “the kernel can’t see it” is a different property, and Linux 5.14 added a syscall for that: memfd_secret.
What the direct map is and why it matters
The kernel keeps a contiguous virtual-to-physical mapping of all RAM, called the direct map or physmap. On x86-64 it starts at 0xffff888000000000. Every physical page you have shows up there. A kernel function with a physical address can compute the direct map virtual address with one addition and read anything.
memfd_secret allocates pages and then removes them from the direct map. The pages exist in RAM. The process’s page table maps them. The kernel’s direct map does not. A kernel-mode read of the direct map address for those pages faults.
This is what separates it from mlock. Both prevent swap. Only memfd_secret makes the pages unreachable from kernel virtual address space.
The CONFIG option that some distros disable
The syscall requires CONFIG_SECRETMEM=y. Check before you rely on it:
grep CONFIG_SECRETMEM /boot/config-$(uname -r)
Or just call the syscall and check errno:
int fd = syscall(SYS_memfd_secret, 0);
if (fd < 0 && errno == ENOSYS) {
/* CONFIG_SECRETMEM=n or kernel < 5.14 */
}
Ubuntu 22.04 and later ship it enabled. Debian 12 has it enabled. RHEL 9 and its derivatives ship it disabled. The reason RHEL turned it off is the same reason you should understand before using it: TLB pressure.
The TLB tax
The x86 direct map uses 1 GB and 2 MB huge pages where possible. One TLB entry covers a gigabyte of physical memory. Removing a page from the direct map splits those huge entries into 4 KB PTEs. A single memfd_secret region can force a gigabyte of the direct map to reload as 262,144 small TLB entries.
On a lightly loaded workstation this is invisible. On a database or web server where the kernel spends serious time in its memory allocator and the network stack, those TLB misses accumulate. The RHEL team decided that the production workload risk was not worth the security benefit for a general-purpose server distribution, so they left the config off.
If you are building a secrets daemon, an HSM replacement, or anything that holds TLS private keys for a long-lived process, the math probably still favors using it. If you are writing general-purpose server software and thinking about adding it as a belt-and-suspenders defense: benchmark first.
The three-step pattern
No libc wrapper yet. Direct syscall, then mmap.
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
void *secret_alloc(size_t size) {
int fd = syscall(SYS_memfd_secret, 0);
if (fd < 0)
return NULL;
if (ftruncate(fd, size) < 0) {
close(fd);
return NULL;
}
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd); /* fd is consumed; the mapping holds the reference */
if (ptr == MAP_FAILED)
return NULL;
return ptr;
}
The ftruncate call sets the size before mapping. The flags argument to memfd_secret is currently always 0; no flags are defined yet. Close the fd after mapping: the mapping holds the pages alive, and you want the fd gone so nothing else can map it.
Round size up to a page boundary. The kernel only works in pages, and an unaligned size gets rounded up silently. Better to be explicit than to wonder later why your allocation is 4096 bytes when you asked for 32.
The explicit_bzero requirement
Do not use memset to zero this memory before freeing it.
void secret_free(void *ptr, size_t size) {
explicit_bzero(ptr, size); /* NOT memset */
munmap(ptr, size);
}
The compiler is allowed to eliminate memset as a dead store when it can prove the memory is about to be released. This is a valid optimization in general. It is a catastrophic optimization for a secrets buffer. The compiler will prove that nobody reads ptr after the memset and the munmap, remove the zero, and leave your private key sitting in RAM.
explicit_bzero (glibc 2.25+, available on any current Linux) and memset_s (C11 Annex K, less commonly available) are both specified to survive this optimization: the implementation is not allowed to assume the zero is unused. Use one of them. The kernel does wipe the pages on final release, but the window between your memset-that-got-optimized-away and the kernel’s wipe is exactly when you do not want the key visible.
What it protects against, and what it does not
memfd_secret blocks:
/proc/kcorereads (pages not in the direct map)ptracevia/proc/PID/mem(returnsEPERMfor secret pages)- Kernel module reads via the direct map
- Hibernation images (secret pages are not written to the swap partition during suspend-to-disk)
- Some VM introspection paths used by hypervisors doing memory scanning
It does not block:
- A process with
CAP_SYS_PTRACEusing the syscall directly before the pages are secret - Side-channel attacks (cache timing, Spectre variants)
- Physical access to RAM sticks
- A compromised process in the same address space
If your threat model includes kernel-level attackers, memfd_secret narrows the attack surface meaningfully. If it does not, mlock is enough and cheaper.
Composing with the rest of the stack
Pair this with the rest of the hardening stack. memfd_secret alone does not prevent the process from calling execve and inheriting the mapping in a child that has fewer restrictions. Add PR_SET_NO_NEW_PRIVS before you restrict anything. Use mlock in addition to ensure the secret pages never swap before you get to the point of calling memfd_secret. Use a seccomp filter to block ptrace family syscalls if the process does not need them.
The mlock and MADV_DONTDUMP post covers the baseline: pin pages and exclude them from core dumps. The seccomp-bpf post covers blocking the syscalls that could extract memory. memfd_secret sits between those layers: it’s not a replacement for either, it’s an additional property that removes your secret pages from the kernel’s own view.
The memfd_create and file sealing post is a related but different mechanism: anonymous RAM-backed files with F_SEAL_* to prevent future modification. If you want sealed immutable memory shared between processes, that’s the right tool. If you want memory the kernel can’t inspect, this is.