$RodHat_
MOTD

mseal() landed in Linux 6.10. Browsers wanted it. You can use it too.

Published by

mseal() landed in Linux 6.10. Browsers wanted it. You can use it too.
Photo: AI-generated — no human photographer / RodHat AI Cover

mmap regions have always been mutable after the fact. You can call mprotect(2) to change permissions, mremap(2) to resize or relocate, munmap(2) to free. All of those are legitimate operations. All of them are also weapons in a memory-corruption exploit chain.

The attack goes like this: you have a bug that lets you write to an arbitrary address. You write shellcode into a region that is currently read-write. You call mprotect() to make it read-execute. You jump to it. Or: you have a write primitive, you mprotect() a known-good code region to read-write, you patch a return address, you jump. The kernel will cooperate with all of this as long as the calling process has permission, and the calling process is you, the attacker.

mseal() is the fix. It takes a range of virtual memory and marks it sealed. After that call: mprotect() on the range fails with EPERM. mremap() fails. munmap() fails. The mapping is frozen until the process exits.

The kernel does not care what permissions the region currently holds. Once sealed, nothing changes it. You can seal an RX code region and guarantee that no component inside that process, including compromised code running in it, can make it writable. You can seal a read-only mmap of a config blob and guarantee nothing remaps it into something mutable.

Chrome and Android were the primary drivers for getting this into mainline. Both run heavily sandboxed renderer processes, both have decades of experience with memory-safety exploits, and both need to establish layout invariants that survive corruption. Chrome started using mseal() in Linux 6.10 kernels for its renderer process and V8’s JIT region. When a bug lets an attacker influence what V8 generates, a sealed region means they cannot retroactively rewrite what is already compiled.

The syscall

int mseal(void *addr, size_t len, unsigned long flags);

flags is currently 0, reserved for future extension. The address and length must be page-aligned.

Seal a guard page so nothing accidentally makes it accessible:

void *stack = mmap(NULL, STACK_SIZE + PAGE_SIZE,
                   PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
mprotect(stack + PAGE_SIZE, STACK_SIZE, PROT_READ | PROT_WRITE);
mseal(stack, PAGE_SIZE, 0);  /* guard page: sealed PROT_NONE forever */

Seal a JIT code region after generation:

void *code = mmap(NULL, code_size,
                  PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
emit_bytecode(code, bytecode, code_size);
mprotect(code, code_size, PROT_READ | PROT_EXEC);
mseal(code, code_size, 0);   /* sealed RX: no mprotect, no mremap, no munmap */

That is the pattern for any runtime that generates native code. Generate, make executable, seal. Even if your code generator has a bug that lets an attacker influence what it emits, they cannot go back and rewrite what is already sealed.

glibc 2.40, released August 2024, added the mseal() wrapper. On older glibc you call it through syscall(2):

#include <sys/syscall.h>
#include <unistd.h>

syscall(SYS_mseal, addr, len, 0);

Check that your kernel has it before relying on it:

# need 6.10 or newer
uname -r

# syscall number on x86-64 is 462
# returns ENOSYS on kernels without it

If the kernel does not have mseal(), syscall() returns -1 with errno == ENOSYS and you fall back to whatever you were doing before. The fallback is not worse than the current situation; the sealing is defense in depth, not a hard dependency.

When it actually matters

For most programs that link against libc and do not implement their own sandbox, mseal() is not something you will call directly this week. The kernel does not seal anything automatically. You have to know what you want sealed and call it explicitly after setting up the mapping.

The cases where it belongs in your design:

JIT compilers and language runtimes. If you call mprotect() to flip a region from read-write to read-execute, the next step should be mseal(). The window between “code generated” and “process exits” is the window where an attacker can try to rewrite it.

Plugin loaders that execute untrusted bytecode. Seal the bytecode mapping before running it.

Security libraries that want to guarantee their own .text segment integrity after startup. Map your library’s read-only data or code into a fresh anonymous region, copy it there, seal it, and verify from that sealed copy. A process cannot modify what it cannot remap.

Anything that establishes a security boundary before handing control to untrusted code. Seal the invariants first.

The permanent constraint

Sealing is permanent for the lifetime of the process. There is no munseal(). You cannot undo it. fork() inherits sealed mappings in the child, but they are sealed there too. If you call exec(), the new image gets a fresh address space and no inherited seals.

Design your sealing points deliberately: seal after setup, before you hand control to code you do not fully trust. Getting the order wrong means the region you wanted to seal gets sealed in the wrong state, which is worse than not sealing it at all.

This is the same “lock in during init, cannot undo” character as Landlock’s filesystem restrictions, which nobody was using five years after landing. The difference is that mseal() requires no privilege at all — any process can seal its own mappings. The attack surface for getting it wrong is smaller, and the adoption barrier is lower. Seccomp’s syscall filtering has similar “apply and lock” semantics, and that one did get widely adopted because the browser vendors shipped it in production at scale.

mseal() is the same story. The browsers will ship it. The JVM will ship it. Language runtimes that care about security will ship it. At that point it will be on distributions that run 6.10-derived kernels, which is most of them, and it will just be there.

If you write code that touches mmap and cares about what happens to those regions under attack, check whether you have a sealing point you have not added yet.

Sources

  1. mseal(2) Linux man page (Michael Kerrisk / man7.org)
  2. Linux 6.10 new features and fixes (kernelnewbies.org)