ADR-015 — RO+NX text / data split
- Status: Accepted
- Date: 2026-09-11
Context
ADR-012 made the heap and heap-backed cooperative stacks PXN. ADR-014 punched unmapped holes under the linker stacks. Both left .text / .rodata / .data / .bss / live linker-stack pages in one executable, writable image mapping. SCTLR_EL1.WXN was unusable while text was writable.
NFR-10 still forbids saying “the kernel is W^X” without a scoped probe. This ADR is the next cut: split RO+X from RW+NX.
Decision
- Page-align
__data_startinlinker.ldso.text/.rodatanever share a 4 KiB page with.data. - Identity flags (still 39-bit / 4 KiB / TTBR0):
[KERNEL_TEXT, __data_start)— RO+X (AP[2]=1, PXN clear, UXN set)[__data_start, RAM end)— RW+NX (including.data,.bss, live linker stacks, frame pool / heap)- Device L1 stays XN. Guard holes stay invalid (ADR-014).
SCTLR_EL1.WXNon once text is RO. Writable pages are treated as XN even if a descriptor forgets PXN.- Fail-closed probe: execute-from-
.data(blrto aRETbait) is a current-EL permission IABORT (ro: nx data). Store to RO text is a current-EL permission DABORT (ro: write fault). Serialro: ok.scripts/qemu-smoke.shgreps those strings and rejectsro: probe missed.#[test_case]covers flags + both faults. - NFR-10 text is revised in place (ID unchanged): mention the RO+NX image cut. Do not mint NFR-15+.
- One L3 per mixed 2 MiB (2026-09-11 follow-up). ADR-012 assumed a single L3 for the 2 MiB that straddles
__kernel_end. After this ADR the mixed block is the one that contains__data_startand the first RAM 2 MiB that containsKERNEL_TEXT(0x4008_0000). Those are different blocks once.text/.rodata(or a linker ratchet) crosses0x4020_0000. Reusing oneL3_RAMoverwrites the kernel-text walk. The kernel L3 pool hands out a fresh table per straddle / guard split. The user TTBR0 maps every 2 MiB that holds text or the exception stack, not only the first RAM block. - Post-MMU
.bsspublishes (same follow-up).USER_MAP_OKandframe::initrun afterSCTLR.C. A pre-MMU store to.bsscan be invisible to a later cached read (boot-delta on the PR #20 test image; cts-ai Docker hello onb2bbb99lost the frame pool / user-map ready flag while guard/ro still passed). Do not pin an old nightly for this.
Honesty — is this “the kernel is W^X”?
On this QEMU virt guest, the identity image plus heap is W^X: text/rodata are RO+X, data/bss/live linker stacks/heap are RW+NX, WXN is on. That is not:
- a “secure OS” / “hardened” claim
- a promise that every future mapping (DMA, new windows) stays W^X
- Raspberry Pi or a second ISA
- a reason to drop the heap / guard probes
Say “identity image is W^X on this virt guest (ADR-015 probe)” only when the serial / tests pass. Do not say “the kernel is W^X” as a product sentence.
Consequences
src/paging.rssets AP[2] on text and PXN on data/stacks.src/ro.rsowns the two faults.- Live linker stacks are NX here. If a later change must execute from a linker stack, this ADR is the one to revisit.
- EL0 user TTBR0 (ADR-013 mile) still maps kernel text so the lower-EL handler can restore kernel TTBR0; it omits
.data. Coverage follows the image across L2 blocks. That is isolation-adjacent, not this ADR. linker.ldparks__data_startat0x4020_1000so GHA / Docker / this cloud always exercise two kernel L3s. File presence of that address is not a boot probe.- This ADR does not claim PAN, ASID isolation, or a higher-half kernel.