mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
synced 2025-01-07 14:32:23 +00:00
200 lines
7.3 KiB
ReStructuredText
200 lines
7.3 KiB
ReStructuredText
|
.. SPDX-License-Identifier: GPL-2.0
|
|||
|
|
|||
|
=====================
|
|||
|
Introduction of mseal
|
|||
|
=====================
|
|||
|
|
|||
|
:Author: Jeff Xu <jeffxu@chromium.org>
|
|||
|
|
|||
|
Modern CPUs support memory permissions such as RW and NX bits. The memory
|
|||
|
permission feature improves security stance on memory corruption bugs, i.e.
|
|||
|
the attacker can’t just write to arbitrary memory and point the code to it,
|
|||
|
the memory has to be marked with X bit, or else an exception will happen.
|
|||
|
|
|||
|
Memory sealing additionally protects the mapping itself against
|
|||
|
modifications. This is useful to mitigate memory corruption issues where a
|
|||
|
corrupted pointer is passed to a memory management system. For example,
|
|||
|
such an attacker primitive can break control-flow integrity guarantees
|
|||
|
since read-only memory that is supposed to be trusted can become writable
|
|||
|
or .text pages can get remapped. Memory sealing can automatically be
|
|||
|
applied by the runtime loader to seal .text and .rodata pages and
|
|||
|
applications can additionally seal security critical data at runtime.
|
|||
|
|
|||
|
A similar feature already exists in the XNU kernel with the
|
|||
|
VM_FLAGS_PERMANENT flag [1] and on OpenBSD with the mimmutable syscall [2].
|
|||
|
|
|||
|
User API
|
|||
|
========
|
|||
|
mseal()
|
|||
|
-----------
|
|||
|
The mseal() syscall has the following signature:
|
|||
|
|
|||
|
``int mseal(void addr, size_t len, unsigned long flags)``
|
|||
|
|
|||
|
**addr/len**: virtual memory address range.
|
|||
|
|
|||
|
The address range set by ``addr``/``len`` must meet:
|
|||
|
- The start address must be in an allocated VMA.
|
|||
|
- The start address must be page aligned.
|
|||
|
- The end address (``addr`` + ``len``) must be in an allocated VMA.
|
|||
|
- no gap (unallocated memory) between start and end address.
|
|||
|
|
|||
|
The ``len`` will be paged aligned implicitly by the kernel.
|
|||
|
|
|||
|
**flags**: reserved for future use.
|
|||
|
|
|||
|
**return values**:
|
|||
|
|
|||
|
- ``0``: Success.
|
|||
|
|
|||
|
- ``-EINVAL``:
|
|||
|
- Invalid input ``flags``.
|
|||
|
- The start address (``addr``) is not page aligned.
|
|||
|
- Address range (``addr`` + ``len``) overflow.
|
|||
|
|
|||
|
- ``-ENOMEM``:
|
|||
|
- The start address (``addr``) is not allocated.
|
|||
|
- The end address (``addr`` + ``len``) is not allocated.
|
|||
|
- A gap (unallocated memory) between start and end address.
|
|||
|
|
|||
|
- ``-EPERM``:
|
|||
|
- sealing is supported only on 64-bit CPUs, 32-bit is not supported.
|
|||
|
|
|||
|
- For above error cases, users can expect the given memory range is
|
|||
|
unmodified, i.e. no partial update.
|
|||
|
|
|||
|
- There might be other internal errors/cases not listed here, e.g.
|
|||
|
error during merging/splitting VMAs, or the process reaching the max
|
|||
|
number of supported VMAs. In those cases, partial updates to the given
|
|||
|
memory range could happen. However, those cases should be rare.
|
|||
|
|
|||
|
**Blocked operations after sealing**:
|
|||
|
Unmapping, moving to another location, and shrinking the size,
|
|||
|
via munmap() and mremap(), can leave an empty space, therefore
|
|||
|
can be replaced with a VMA with a new set of attributes.
|
|||
|
|
|||
|
Moving or expanding a different VMA into the current location,
|
|||
|
via mremap().
|
|||
|
|
|||
|
Modifying a VMA via mmap(MAP_FIXED).
|
|||
|
|
|||
|
Size expansion, via mremap(), does not appear to pose any
|
|||
|
specific risks to sealed VMAs. It is included anyway because
|
|||
|
the use case is unclear. In any case, users can rely on
|
|||
|
merging to expand a sealed VMA.
|
|||
|
|
|||
|
mprotect() and pkey_mprotect().
|
|||
|
|
|||
|
Some destructive madvice() behaviors (e.g. MADV_DONTNEED)
|
|||
|
for anonymous memory, when users don't have write permission to the
|
|||
|
memory. Those behaviors can alter region contents by discarding pages,
|
|||
|
effectively a memset(0) for anonymous memory.
|
|||
|
|
|||
|
Kernel will return -EPERM for blocked operations.
|
|||
|
|
|||
|
For blocked operations, one can expect the given address is unmodified,
|
|||
|
i.e. no partial update. Note, this is different from existing mm
|
|||
|
system call behaviors, where partial updates are made till an error is
|
|||
|
found and returned to userspace. To give an example:
|
|||
|
|
|||
|
Assume following code sequence:
|
|||
|
|
|||
|
- ptr = mmap(null, 8192, PROT_NONE);
|
|||
|
- munmap(ptr + 4096, 4096);
|
|||
|
- ret1 = mprotect(ptr, 8192, PROT_READ);
|
|||
|
- mseal(ptr, 4096);
|
|||
|
- ret2 = mprotect(ptr, 8192, PROT_NONE);
|
|||
|
|
|||
|
ret1 will be -ENOMEM, the page from ptr is updated to PROT_READ.
|
|||
|
|
|||
|
ret2 will be -EPERM, the page remains to be PROT_READ.
|
|||
|
|
|||
|
**Note**:
|
|||
|
|
|||
|
- mseal() only works on 64-bit CPUs, not 32-bit CPU.
|
|||
|
|
|||
|
- users can call mseal() multiple times, mseal() on an already sealed memory
|
|||
|
is a no-action (not error).
|
|||
|
|
|||
|
- munseal() is not supported.
|
|||
|
|
|||
|
Use cases:
|
|||
|
==========
|
|||
|
- glibc:
|
|||
|
The dynamic linker, during loading ELF executables, can apply sealing to
|
|||
|
non-writable memory segments.
|
|||
|
|
|||
|
- Chrome browser: protect some security sensitive data-structures.
|
|||
|
|
|||
|
Notes on which memory to seal:
|
|||
|
==============================
|
|||
|
|
|||
|
It might be important to note that sealing changes the lifetime of a mapping,
|
|||
|
i.e. the sealed mapping won’t be unmapped till the process terminates or the
|
|||
|
exec system call is invoked. Applications can apply sealing to any virtual
|
|||
|
memory region from userspace, but it is crucial to thoroughly analyze the
|
|||
|
mapping's lifetime prior to apply the sealing.
|
|||
|
|
|||
|
For example:
|
|||
|
|
|||
|
- aio/shm
|
|||
|
|
|||
|
aio/shm can call mmap()/munmap() on behalf of userspace, e.g. ksys_shmdt() in
|
|||
|
shm.c. The lifetime of those mapping are not tied to the lifetime of the
|
|||
|
process. If those memories are sealed from userspace, then munmap() will fail,
|
|||
|
causing leaks in VMA address space during the lifetime of the process.
|
|||
|
|
|||
|
- Brk (heap)
|
|||
|
|
|||
|
Currently, userspace applications can seal parts of the heap by calling
|
|||
|
malloc() and mseal().
|
|||
|
let's assume following calls from user space:
|
|||
|
|
|||
|
- ptr = malloc(size);
|
|||
|
- mprotect(ptr, size, RO);
|
|||
|
- mseal(ptr, size);
|
|||
|
- free(ptr);
|
|||
|
|
|||
|
Technically, before mseal() is added, the user can change the protection of
|
|||
|
the heap by calling mprotect(RO). As long as the user changes the protection
|
|||
|
back to RW before free(), the memory range can be reused.
|
|||
|
|
|||
|
Adding mseal() into the picture, however, the heap is then sealed partially,
|
|||
|
the user can still free it, but the memory remains to be RO. If the address
|
|||
|
is re-used by the heap manager for another malloc, the process might crash
|
|||
|
soon after. Therefore, it is important not to apply sealing to any memory
|
|||
|
that might get recycled.
|
|||
|
|
|||
|
Furthermore, even if the application never calls the free() for the ptr,
|
|||
|
the heap manager may invoke the brk system call to shrink the size of the
|
|||
|
heap. In the kernel, the brk-shrink will call munmap(). Consequently,
|
|||
|
depending on the location of the ptr, the outcome of brk-shrink is
|
|||
|
nondeterministic.
|
|||
|
|
|||
|
|
|||
|
Additional notes:
|
|||
|
=================
|
|||
|
As Jann Horn pointed out in [3], there are still a few ways to write
|
|||
|
to RO memory, which is, in a way, by design. Those cases are not covered
|
|||
|
by mseal(). If applications want to block such cases, sandbox tools (such as
|
|||
|
seccomp, LSM, etc) might be considered.
|
|||
|
|
|||
|
Those cases are:
|
|||
|
|
|||
|
- Write to read-only memory through /proc/self/mem interface.
|
|||
|
- Write to read-only memory through ptrace (such as PTRACE_POKETEXT).
|
|||
|
- userfaultfd.
|
|||
|
|
|||
|
The idea that inspired this patch comes from Stephen Röttger’s work in V8
|
|||
|
CFI [4]. Chrome browser in ChromeOS will be the first user of this API.
|
|||
|
|
|||
|
Reference:
|
|||
|
==========
|
|||
|
[1] https://github.com/apple-oss-distributions/xnu/blob/1031c584a5e37aff177559b9f69dbd3c8c3fd30a/osfmk/mach/vm_statistics.h#L274
|
|||
|
|
|||
|
[2] https://man.openbsd.org/mimmutable.2
|
|||
|
|
|||
|
[3] https://lore.kernel.org/lkml/CAG48ez3ShUYey+ZAFsU2i1RpQn0a5eOs2hzQ426FkcgnfUGLvA@mail.gmail.com
|
|||
|
|
|||
|
[4] https://docs.google.com/document/d/1O2jwK4dxI3nRcOJuPYkonhTkNQfbmwdvxQMyXgeaRHo/edit#heading=h.bvaojj9fu6hc
|