Repository navigation
openpax: close the brk MPROTECT bypass and rein in trampoline emulation - #103
Merged
Merged
Conversation
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
requested review from
antoineco,
azenla,
bleggett,
sveith and
tycho
as code owners
November 13, 2026 21:49
bleggett
approved these changes
Nov 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three OpenPaX fixes from the Mythos audit.
MPROTECT bypass via brk (ENG-217).
map_deny_write_exec()guardsmmapandmprotect, butdo_brk_flags()never calls it and ORs inVMA_DATA_DEFAULT_FLAGS, which carriesVM_EXECwhenever the task's personality hasREAD_IMPLIES_EXEC.personality(2)lets any task set that, so under MPROTECT two syscalls (personality(READ_IMPLIES_EXEC)thenbrk()) 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 stripsVM_EXECandVM_MAYEXECwhen the mm runs under MPROTECT, as PaX'sdo_brk()did, sobrk(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 withget_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_DEFAULTnow defaults ton, 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 withpaxmark -E.Note for the config:
linux-kernel-oci'sconfigs/x86_64/zone-openpax.fragment.configsetsCONFIG_OPENPAX_EMUTRAMP_DEFAULT=yexplicitly, 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-ocix86_64zone.configmerged 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/statusPaX line) are not part of this change.ENG-217
ENG-222