Skip to content

openpax: close the brk MPROTECT bypass and rein in trampoline emulation - #103

Merged
kaniini merged 3 commits into
edera/mainlinefrom
ariadne/eng-217-openpax-mprotect-brk
Nov 13, 2026
Merged

kaniini merged 3 commits into
edera/mainlinefrom
ariadne/eng-217-openpax-mprotect-brk

Conversation

@kaniini

@kaniini kaniini commented Nov 13, 2026

Copy link
Copy Markdown

Three OpenPaX fixes from the Mythos audit.

MPROTECT bypass via brk (ENG-217). map_deny_write_exec() guards mmap and mprotect, but do_brk_flags() never calls it and ORs in VMA_DATA_DEFAULT_FLAGS, which carries VM_EXEC whenever the task's personality has READ_IMPLIES_EXEC. personality(2) lets any task set that, so under MPROTECT two syscalls (personality(READ_IMPLIES_EXEC) then brk()) yield a fresh writable and executable VMA. The same path makes the bss tail of an executable PT_LOAD segment RWX. do_brk_flags() now strips VM_EXEC and VM_MAYEXEC when the mm runs under MPROTECT, as PaX's do_brk() did, so brk(2) keeps working and extensions still merge with the heap.

Trampoline emulation hooked before VMA lookup (ENG-222). The emulator ran at the top of do_user_addr_fault(), so in every PAGEEXEC+EMUTRAMP task every instruction fetch fault, including demand paging of legitimate text, first read up to seven bytes of the faulting code with get_user(), and text that matched a pattern was emulated instead of faulted in. The hook now lives in __bad_area_nosemaphore(), where PaX had it: it runs only for a fetch the task's mappings do not permit, with the fault otherwise about to become SIGSEGV.

Emulation on for every program (ENG-222). OPENPAX_EMUTRAMP_DEFAULT now defaults to n, with help text explaining why. The emulator lets a task with a write primitive and control of RIP load two registers and jump anywhere from any readable memory, which weakens PAGEEXEC and MPROTECT for every program on the system; PaX recommends enabling it per binary with paxmark -E.

Note for the config: linux-kernel-oci's configs/x86_64/zone-openpax.fragment.config sets CONFIG_OPENPAX_EMUTRAMP_DEFAULT=y explicitly, so the Kconfig default alone does not change the zone kernel. That fragment needs its own change once we decide which zone programs need marking.

Built with the latest linux-kernel-oci x86_64 zone.config merged with the OpenPaX fragment: full vmlinux, no warnings. Not runtime-tested; the brk reproducer and a nested-function trampoline test would need an OpenPaX zone kernel.

The lower-severity items in the ENG-222 notes (xattr read bypassing LSM, entropy defaults, argument-block randomisation personality, the /proc/pid/status PaX line) are not part of this change.

ENG-217
ENG-222

MPROTECT is enforced in map_deny_write_exec(), which mmap_region() and
mprotect() call, but the brk path never does.  do_brk_flags() ORs in
VMA_DATA_DEFAULT_FLAGS, which carries VM_EXEC whenever the task's
personality has READ_IMPLIES_EXEC, and personality(2) lets any task set
that for itself: the ELF loader only clears it at exec.  So under
MPROTECT, where mmap(PROT_READ|PROT_WRITE) with READ_IMPLIES_EXEC is
correctly refused, two syscalls still hand out a fresh writable and
executable mapping:

	personality(READ_IMPLIES_EXEC);
	brk(cur + len);	/* new RWX VMA; it does not merge with the RW heap */

Write code there and jump to it.  The same path makes the bss tail of an
executable PT_LOAD segment RWX, via vm_brk_flags(..., prot & PROT_EXEC)
in the ELF loader, for the binary and its interpreter.

Original PaX masked VM_EXEC and VM_MAYEXEC in do_brk() for exactly this
reason.  Do the same in do_brk_flags() when the mm runs under MPROTECT:
strip the execute bits rather than fail, so brk(2) keeps working for
such tasks and extensions still merge with the existing heap.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
The trampoline emulator was hooked at the top of do_user_addr_fault(),
before the VMA lookup, and openpax_fault_is_trampoline() treats any
instruction fetch fault as a candidate.  So in every PAGEEXEC+EMUTRAMP
task every instruction fetch fault, including each demand-paged, NUMA
hinted or userfaultfd fetch of legitimate text, first did up to seven
get_user() reads of the faulting code, and if that text happened to
match one of the patterns it was emulated instead of being faulted in.

PaX only ran the emulator from the bad-area path, for a fault that was
otherwise about to kill the task.  Move the hook to
__bad_area_nosemaphore(), after the user-mode and X86_PF_USER checks and
with interrupts enabled, so it runs only for an access the task's
mappings do not permit: a fetch from non-executable memory.  A fetch
from executable memory that merely needs its page brought in is handled
as an ordinary fault and never reaches it.

A fetch from a non-executable page that is not yet populated now takes
two faults, one to bring the page in and one to be emulated, instead of
being emulated straight from the first; the result is the same.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
With OPENPAX_EMUTRAMP_DEFAULT the ELF loader sets PAXF_EMUTRAMP on every
task.  The emulator then lets any task with a write primitive into some
readable memory and control of the instruction pointer load chosen
values into two registers and jump to a chosen address, from the stack,
the heap, .rodata or a noexec-mount file mapping, without any of that
memory being executable; a bad target just faults again, so such
sequences chain.  That weakens PAGEEXEC and MPROTECT for every program
on the system to help the few that still use stack or heap trampolines.

PaX recommends enabling emulation per binary for this reason.  Default
the option to n and say so in its help text; the programs that need
emulation get it with paxmark -E, through the user.pax.flags xattr.

Signed-off-by: Ariadne Conill <ariadne@ariadne.space>
@kaniini
kaniini merged commit 93dbfdc into edera/mainline Nov 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants