feat(runtime/linux): mprotect PT_GNU_RELRO read-only after self-relocation#335
Conversation
…ation The static-PIE self-relocation maps a read-only segment holding relocated constants (`.rodata` with vtables or `val` pointer globals) writable so it can slide their RELATIVE slots. Once applied, re-protect that region read-only, restoring the constants' hardening before `main`. `_rt_relocate` now captures the PT_GNU_RELRO program header (and AT_PAGESZ) and, after the relocation pass, `mprotect`s `[bias+p_vaddr, page-rounded end)` to PROT_READ. The step runs after relocation, so it uses the ordinary syscall path; it is best-effort (a kernel page larger than the image's segment alignment leaves the region writable rather than failing the program). Runtime half of briar-systems/mach#1778; the linker emits the PT_GNU_RELRO.
Address review of the PT_GNU_RELRO re-protection: - No runtime page arithmetic. The writer now emits p_memsz already page-rounded, so `_rt_relocate` gates on the region being a whole number of runtime pages (start and size both AT_PAGESZ-congruent) and mprotects it exactly. On a kernel page larger than the image's 4 KiB segment alignment the gate skips, leaving the region writable (correct, just unhardened) instead of risking EINVAL or over-protecting the following segment. Tracked by #336. - Drop the AT_PAGESZ==0 -> 4096 fabrication; AT_PAGESZ is mandatory on linux, so skip hardening when it is absent rather than guessing. - Decouple the re-protection from reloc presence: the RELATIVE apply is guarded rather than early-returned, so the mprotect no longer sits behind a reloc-count gate (they are only coupled by the writer, which emits RELRO for reloc-bearing segments). - Use the bare literal 0x6474e552 for PT_GNU_RELRO in the pre-relocation phdr walk (matching its neighbors, so a codegen change can't silently disable it), and export PROT_READ from the shared OS layer instead of a duplicate local constant.
|
Review changes addressed (pushed as 1. Rounding math. Moved to the writer: mach now emits PT_GNU_RELRO 2. PT_GNU_RELRO constant + PROT_READ. PT_GNU_RELRO is now the bare literal 3. AT_PAGESZ fabrication. Dropped — no 4. Reloc-presence coupling. The RELATIVE apply is now guarded ( 5. mprotect failure policy. Kept best-effort, but the comment now ties the single degradation path explicitly to the large-page gap (mach-std#336). With fix 1 the mprotect only runs when start+size are page-congruent, so the reachable failure surface is near-zero. 6. AT_PAGESZ → OS layer. Filed as mach-std#336 (page_size() hardcodes 4096 with a TODO whose precondition this PR meets); not implemented here. Re-verified: relocated |
What
Runtime half of RELRO hardening for static-PIE (briar-systems/mach#1778). The linker (mach) emits a
PT_GNU_RELROprogram header over the read-only segment it maps writable for self-relocation (.rodataholding relocated constants — vtables,valpointer globals). This restores that region to read-only after the relocation pass, beforemain._rt_relocatenow:AT_PAGESZin the auxv scan and thePT_GNU_RELROheader (p_vaddr,p_memsz) in the program-header walk;The writer emits
p_vaddrpage-aligned andp_memszalready page-rounded (DATA_SEGMENT_RELRO_END style), so the runtime does no arithmetic: it gates on the region being a whole number of runtime pages (start % AT_PAGESZ == 0 && sz % AT_PAGESZ == 0) andmprotects exactly[start, start+sz)toPROT_READ. On a kernel page larger than the image's 4 KiB segment alignment the gate skips — the region stays writable (correct, just unhardened) rather than riskingEINVALor over-protecting the following segment (mach-std#336). The step runs strictly after relocation, so it uses the ordinary syscall path (std.system.os.linux.shared.protect) — the position-independent constraint only applies to the pre-relocation code above it, and the RELATIVE apply is guarded rather than early-returned so re-protection stays independent of reloc presence.Verification
Built with the from-source mach linker (which emits
PT_GNU_RELRO), across all three no-libc linux arches:read=1001read=1001read=1001.rodatastorage faults — proving the region is read-only after startup.wrote ok, exit 0) — confirming the fault is the newmprotect..datastill runs correctly (s=9012, exit 0) on all three arches — RELRO does not touch genuinely-writable data.mach test .(mach-std): 571 ok.Release ordering
Same flow as the #1727 self-relocation runtime: this lands on mach-std
dev→mainrelease, then amach.lockbump in mach activates it and adds the write-fault exec guard to the int harness. The mach linker PR (briar-systems/mach#1811) is independent and already CI-green against the current runtime (the header is emitted; the current runtime simply leaves the region writable).Follow-up: mach-std#336 (surface
AT_PAGESZto the OS layer, replacing the hardcodedpage_size()).