mseal(2): permanently lock down your VMA after you are done with it
Published by RodHat · Updated

Most memory protection hardening works by raising the bar at allocation time: you mmap with the right flags, you mprotect to read-only, you drop write permissions before anyone can abuse them. That is the right instinct. The problem is that those protections are always reversible. Another mprotect adds PROT_WRITE back. An attacker who controls the return address can call mprotect themselves via a gadget. Your hardening lasts exactly as long as it takes someone to undo it.
mseal(2) cuts that thread. Once you seal a VMA range, the kernel refuses to
let anything modify it: no mprotect, no pkey_mprotect, no munmap, no
mmap(MAP_FIXED) targeting the range, no mremap that would shrink or move
it. EPERM, permanently, until the process exits.
Synopsis
#include <sys/syscall.h>
#include <unistd.h>
/* no glibc wrapper until glibc 2.41 */
int ret = syscall(SYS_mseal, addr, len, 0UL);
addr must be page-aligned. len rounds up to the next page boundary. The
flags argument is reserved: pass 0. Anything else returns EINVAL.
The JIT pattern
The canonical use is a JIT engine that needs executable memory:
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
/* 1. allocate writable, non-executable first */
void *code = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* 2. write your generated code */
memcpy(code, jit_buf, jit_len);
__builtin___clear_cache(code, (char *)code + jit_len);
/* 3. drop write, add execute */
mprotect(code, sz, PROT_READ | PROT_EXEC);
/* 4. seal: nothing can touch this VMA again */
syscall(SYS_mseal, code, sz, 0UL);
After step 4, a call-oriented programming gadget that tries to mprotect the
region back to PROT_WRITE gets EPERM from the kernel. No bypass via
/proc/self/mem either: writes through procfs to a sealed, non-writable VMA
are rejected at the kernel’s access_remote_vm path.
Chrome’s V8 seals its code pages after trampolines are installed. glibc 2.41
seals the _rtld_global linker control structure and the main executable’s
read-only segments. The kernel itself uses the same mechanism to protect
certain vDSO pages.
What sealing blocks
Once sealed, the kernel rejects all of the following on any overlapping range:
mprotect(2)andpkey_mprotect(2): no permission changes, period.munmap(2): the range cannot be unmapped.mmap(2)withMAP_FIXEDorMAP_FIXED_NOREPLACEtargeting the range.mremap(2)when the operation would shrink or move the sealed region.
Sealing is one-way. There is no munseal. The range stays sealed until exit.
Errors
EPERM: the range, or part of it, is already sealed.EINVAL:addris not page-aligned,flagsis non-zero, orlenoverflows when rounded up.ENOMEM: the address range is not fully mapped, or spans more VMAs than the kernel will merge.
Kernel and glibc availability
mseal(2) shipped in Linux 6.10 (July 2024). Check at runtime:
#include <linux/version.h>
/* compile-time guard only — check uname(2) or syscall return at runtime */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0)
syscall(SYS_mseal, addr, len, 0UL);
#endif
glibc 2.41 added a direct mseal() wrapper, so you can drop the syscall()
boilerplate if your minimum glibc allows it.
On older kernels, SYS_mseal is undefined. Wrap the call and handle ENOSYS
gracefully if you need to support anything older than 6.10.
BSD
OpenBSD added mimmutable(2) in 7.3 (April 2023), a year before Linux caught
up. The semantics are similar: mark a range immutable against mprotect and
munmap. FreeBSD has no equivalent in base as of 14.2.
When to reach for it
Seal anything you wrote once and never intend to modify again. JIT code regions are the obvious target, but the pattern applies to any mapped data that should be read-only for the process lifetime: constant lookup tables, compiled bytecode, shared memory you own but do not want overwritten via a confused deputy. The cost is one syscall and a permanent commitment. If you are mapping executable memory and not sealing it, you are leaving a knob anyone who controls your instruction pointer can turn.
Sources
- mseal(2) Linux man page (man7.org)
- mseal userspace API documentation (kernel.org)