Memory sealing for the GNU C Library
Please consider subscribing to LWN Subscriptions are the lifeblood of LWN.net. If you appreciate this content and would like to see more of it, your subscription will help to ensure that LWN continues to thrive. Please visit this page to join up and keep LWN on the net.
mseal()
mseal()
mseal()
The mseal() system call allows a process to prevent any future changes to portions of its address space (thus "sealing" them); it was patterned after the mimmutable() system call in OpenBSD.generated a lot of discussion, but it was finally merged for the upcoming 6.10 kernel release. Whilewas initially aimed at securing the Chrome browser, the hope was that it would be useful elsewhere; as a step toward realizing that hope, Adhemerval Zanella has posted a patch series adding support for — and use of —to the GNU C library (glibc).
This new system call is intended to increase security by making it harder for an attacker who has gained some control to make changes to a process's address space. If a region of memory has been sealed, it cannot be unmapped, remapped, or have its protections changed; some madvise() operations are also forbidden. Sealing is a one-way operation; once memory is sealed, it cannot be unsealed for the life of the process.
The first step in the series is to make mseal() available to glibc users. The interface is almost exactly what the kernel provides:
int mseal(void *address, size_t length, unsigned long flags);
The address and length parameters describe the memory range to be sealed; the flags value is currently unused and must be zero.
Zanella did not stop with adding the system-call wrapper, though. The C library, which includes the dynamic loader, does much of the work of putting a process's address space together in the first place; it is well positioned to know which parts of that address space should not change and, thus, can be sealed. Zanella's patch set takes advantage of that information to optionally seal various parts of the address space, including:
The binary code that the process is running, once it has been set up. This sealing works for both static and dynamic binaries. All shared libraries loaded as dependencies will also be sealed, read-only segments included. (For the curious, this sealing happens after the RELRO setup is done, so it would not have been enough to block the XZ backdoor).
Any preloaded libraries.
The kernel's vDSO area.
Any dynamic library loaded with dlopen() using the RTLD_NODELETE flag (which prevents the library from being unloaded on a dlclose() call).
using the flag (which prevents the library from being unloaded on a call). Any audit modules and their dependencies.
While most programs should run just fine (and more securely) with this sealing in place, there will surely be exceptions that are playing complicated tricks with their address space. So, naturally, there is an addition to the list of glibc tunables, called gtld.rtld.seal , controlling the sealing behavior. Setting this knob to zero will disable any sealing performed automatically by the library (though mseal() will continue to work, of course). The default value (one) causes the sealing to happen, but any failure to seal a portion of the address space will be ignored. For more security-critical programs, setting this knob to two will cause the process to be killed if any of the sealing attempts fails.