Linux mseal: Protect Mapping Policy Without Claiming Immutable Memory
Use Linux mseal with dedicated mappings, explicit lifetime and feature checks, negative tests, and realistic limits on protection and memory immutability.
Making a memory page read-only does not necessarily make that decision permanent. A process can often call mprotect() to restore write permission, unmap the region, or replace it with a different mapping. That flexibility is useful to allocators and runtimes, but it also means that a corrupted pointer reaching a memory-management operation can undermine assumptions about trusted code or data.
Linux mseal() protects selected virtual-memory mapping policy against later modification. Its central promise concerns mappings, not every possible route to the bytes they contain. It is therefore important to distinguish memory sealing from encryption, file sealing, write protection, and a full process sandbox before adopting it.
This article compares the Linux 6.10 documentation with the current kernel documentation reviewed on October 11, 2026. The interface is present in the 6.10 tree, but architecture support, headers, syscall availability, and error details must be verified for the actual deployment kernel. The current documentation restricts it to 64-bit kernels; do not treat a kernel version string as a complete feature test.
Seal policy after initialization
mseal(addr, len, flags) seals a mapped address range. The start must be page-aligned, the region must contain no unmapped gaps, and address arithmetic must not overflow. The kernel rounds the length to page granularity. Flags are reserved; use zero rather than guessing future behavior from unused bits.
A reasonable application sequence is to allocate a dedicated mapping, populate its data, change it to its final permissions, and only then seal it. Sealing writable memory does not remove its write permission. If the intended object is read-only, the preceding mprotect(PROT_READ) step is essential. Similarly, an executable mapping must already have the desired execution policy before sealing.
Once sealed, a mapping cannot be unsealed through a corresponding munseal() operation. Its lifetime is effectively tied to process termination or replacement of the process image. That is a design commitment, not a flag to toggle around every update. Replaceable configuration should use a deliberate generation or worker-process strategy instead of relying on reclamation of sealed mappings inside one long-lived process.
Understand which operations are constrained
The documentation identifies unmapping, remapping, replacement through mmap(), and permission changes through mprotect() or pkey_mprotect() as constrained operations. Certain destructive madvise() behaviors are also constrained under the documented conditions, especially where discarding anonymous pages could change data that is otherwise not writable.
Do not reduce that list to “all advice is denied” or “all memory writes are denied.” Ordinary writes remain possible on a sealed writable mapping. File-backed or externally shared data requires its own content-ownership analysis. Sealing the local mapping is not a digest check or a guarantee that another participant cannot change the underlying object.
The current documentation explicitly names /proc/self/mem, ptrace() writes, and userfaultfd as mechanisms not covered by sealing’s basic guarantee. Whether a particular process can use them depends on additional policy and setup. Complementary sandbox controls may be relevant, but saying that mseal() alone makes memory immutable would overstate the interface.
A dedicated-mapping probe
This illustrative Linux probe uses the syscall number supplied by the installed architecture headers, not a hard-coded number. It allocates one anonymous page, initializes a string, marks the page read-only, seals it, and checks that two prohibited policy changes fail. Run it as a short-lived unprivileged process on the target kernel; the intentionally sealed page is reclaimed when that process exits.
#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/syscall.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
#ifndef SYS_mseal
fprintf(stderr, "headers do not define SYS_mseal\n");
return 2;
#else
long page_size = sysconf(_SC_PAGESIZE);
if (page_size <= 0) {
fprintf(stderr, "cannot determine page size\n");
return 1;
}
size_t length = (size_t)page_size;
char *p = mmap(NULL, length, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) {
perror("mmap");
return 1;
}
memcpy(p, "approved-state", sizeof("approved-state"));
if (mprotect(p, length, PROT_READ) == -1) {
perror("mprotect read-only");
return 1;
}
if (syscall(SYS_mseal, p, length, 0UL) == -1) {
perror("mseal");
return 2;
}
errno = 0;
if (mprotect(p, length, PROT_READ | PROT_WRITE) != -1 ||
errno != EPERM) {
fprintf(stderr, "unexpected permission-change result\n");
return 1;
}
errno = 0;
if (munmap(p, length) != -1 || errno != EPERM) {
fprintf(stderr, "unexpected unmap result\n");
return 1;
}
if (strcmp(p, "approved-state") != 0) {
fprintf(stderr, "content changed unexpectedly\n");
return 1;
}
puts("mapping policy negative tests passed");
return 0;
#endif
}
No execution on a Linux target is claimed here. The probe avoids a deliberate write fault, uses a single dedicated mapping, and reports unsupported headers separately from successful negative tests. Its exit code two also covers a failed sealing syscall; inspect the actual diagnostic rather than labelling every such result “old kernel.” A syscall filter or another security layer can affect what the process observes.
Errors are not a universal rollback transaction
The current documentation lists EINVAL for invalid flags, an unaligned start, or overflow, and ENOMEM for an invalid or gapped range. It describes those validation failures as leaving the requested range unchanged. Rare internal errors associated with VMA splitting, merging, or resource limits can nevertheless leave partial changes. A blanket rule that every failed seal means “nothing happened” is unsafe.
Behavior after a blocked operation also needs care. The current text describes munmap() as atomic when a sealed VMA appears in its range. It warns that mprotect(), pkey_mprotect(), and madvise() over multiple VMAs may change earlier VMAs before failing at a sealed one. It does not provide a safe rollback assumption for blocked mmap() and mremap() cases. Prefer narrowly scoped operations and do not probe a large mixed range containing application state.
The 6.10 document and current document differ in some error and partial-update descriptions. This is precisely why a deployment should retain its kernel-specific evidence rather than copy an old explanation indefinitely. Use the current source for planning, validate against the running release, and keep the probe separate from production initialization.
Keep allocators and recyclable regions out of the boundary
Do not apply mseal() to arbitrary pointers returned by malloc(). The allocator owns larger mappings and may reuse, shrink, or unmap them. Page rounding can affect neighboring allocations, while permanent sealing can prevent allocator operations or leave read-only storage where later allocations expect writable bytes. Avoiding an explicit free() call does not transfer mapping ownership to the application.
Likewise, memory associated with shared-memory attachment, asynchronous I/O, plugins, or dynamic runtime machinery may need a lifetime shorter than the process’s. Sealing it can turn normal teardown into address-space retention or incompatibility. Dedicated mmap() regions with reviewed ownership are much easier to reason about than opportunistic hardening of existing heap objects.
Budget sealed address space by generation and lifetime. A service that seals every incoming configuration revision can leak address space by design even though each allocation is reachable only briefly. If updates are frequent, prefer an architecture that retires whole workers or stores mutable state separately from a genuinely process-lifetime object.
Build a verification and recovery contract
Test supported and unsupported kernels, missing headers, page-alignment rejection, invalid flags, repeated sealing, and each policy change that the application relies on being denied. Repeated sealing of an already sealed range is documented as a no-op rather than an error. Keep destructive experiments inside disposable child processes, not an already running service.
Record the architecture, kernel build, installed syscall headers, seccomp policy, exact probe source, and results. Choose explicitly whether inability to seal is a startup failure or a documented reduced-hardening mode; never silently claim the stronger guarantee in both cases. Recovery from an incorrectly sealed live object is not an unseal command. Restore the prior application build or configuration and restart through a controlled lifecycle after preserving diagnostic evidence.
Related:
- Linux memfd File Seals: Immutable Shared Data Without a Pathname
- Linux openat2: Constraining Path Resolution Against Symlink and Mount Escapes
Sources: