$RodHat_
Console Tips

mseal(2): permanently lock down your VMA after you are done with it

Published by · Updated

Photo by Zulfugar Karimov
Photo: Zulfugar Karimov / Unsplash

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) and pkey_mprotect(2): no permission changes, period.
  • munmap(2): the range cannot be unmapped.
  • mmap(2) with MAP_FIXED or MAP_FIXED_NOREPLACE targeting 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: addr is not page-aligned, flags is non-zero, or len overflows 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

  1. mseal(2) Linux man page (man7.org)
  2. mseal userspace API documentation (kernel.org)