Repository navigation
An executable's PT_GNU_RELRO is read-only once it has relocated itself - #1237
Conversation
Review of #1237 at 4a9e332 (round 1)ArchitectureThe owner is right. The program seals its own
Size: about 54 production lines are added ( What I checked
Required
Both are comment-only changes. No check needs to run again for them. Evidence
Each pending result must show the following.
Suggestions
SEND BACK. Only the two comment edits are needed. After that, the PR can land once the control and the T14 boot show the results above. |
…d the header's segment is tested above another `toyos::dl::start`'s contract now says that a program whose `PT_GNU_RELRO` cannot be made read-only exits 127 as one whose libraries cannot be loaded does. `seal`'s comment loses a sentence that told a reader nothing, and the applications page says plainly that the kernel does not enforce the seal and that skipping it weakens only the program itself. `an_executables_relro_pages_are_counted_from_its_header` gains the case its comment claimed and no case showed: a header's segment that lies above another `PT_LOAD`, whose RELRO offset is counted from that segment's start, and a RELRO below the header, which is refused. With the subtraction of the segment's start dropped from `executable_relro`, the new case fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1237 at b2172a8 (round 2)ScopeThis round checks only two things: whether round 1's required items are resolved, and whether the code that changed since
Round 1's items
The new host case (
|
374c6ca to
61007d2
Compare
`PT_GNU_RELRO` covers what a program's relocations write once and nothing writes after: `.data.rel.ro`, `.dynamic`, `.got`, vtables and tables of function pointers. The SDK's loader took the write away from a library's once it was relocated, but an executable's stayed writable for its whole life, so a stray or hijacked store could rewrite a vtable. `toyos::dl::start`, which std's and libc's start call once thread-local storage is laid out, now makes the executable's whole RELRO pages read-only with `SYS_VM_PROTECT` after every relocation is applied: its own by `toyos::relocate`, the rest by the library load where it names one. Then the libraries' constructors run, as before. The pages are counted from the executable's header, the first byte of its `PT_LOAD` at file offset 0; a last partial page stays writable, as for a library. A refusal exits 127 with a line on stderr, as a failed library load does. `relro_sealed` stores to a relocated word in a whole RELRO page from a child and holds that the child ends as a memory fault: the x86-64 shared boot's member and a job of the AArch64 job case. Closes issues/an-executables-relro-stays-writable.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the header's segment is tested above another `toyos::dl::start`'s contract now says that a program whose `PT_GNU_RELRO` cannot be made read-only exits 127 as one whose libraries cannot be loaded does. `seal`'s comment loses a sentence that told a reader nothing, and the applications page says plainly that the kernel does not enforce the seal and that skipping it weakens only the program itself. `an_executables_relro_pages_are_counted_from_its_header` gains the case its comment claimed and no case showed: a header's segment that lies above another `PT_LOAD`, whose RELRO offset is counted from that segment's start, and a RELRO below the header, which is refused. With the subtraction of the segment's start dropped from `executable_relro`, the new case fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etal `relro_sealed`'s x86-64 verdict is a member of the T14's `testcases` shared boot, which no guest row runs, and the benches are down. So the record this change closed stays open: what is built, and the boot it owes, with what main must show there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61007d2 to
5f0467f
Compare
|
Orchestrator: rebased onto 75bdec3 at 5f0467f (list-only conflicts, both entries kept; #1237's SDK, guest and doc lines unchanged — verified by the author's line accounting). Ruling on the composition with #1259: no extra run. #1259 maps only non-writable segments from ROOT's pages, RELRO lies in a writable (private) segment, and the layout refuses two segments sharing a page — so the seal never meets a shared page, by construction. #1237 lands on 43093182539 (x86 breadth + aarch64 row) when green. |
|
Orchestrator: landing under the benches-down rule. Guest run 43093182539 at 61007d2: success (x86-64 breadth rows launching many RELRO-bearing programs, plus the AArch64 row). Head 5f0467f is the same change rebased onto 75bdec3 (list-only conflicts; composition with #1259 holds by construction, ruled above). |
Problem
PT_GNU_RELROcovers what a program's relocations write once and nothing writes after:.data.rel.ro,.dynamic,.got, vtables and tables of function pointers. The SDK's loader took the write away from each library's RELRO once relocated, but an executable's stayed writable for its whole life, so a stray or hijacked store could rewrite a vtable (issues/an-executables-relro-stays-writable.md, which stays open only for its x86-64 boot, owed on metal).Behavior
toyos::dl::start, which std's and libc's start call once thread-local storage is laid out, makes the executable's whole RELRO pages read-only withSYS_VM_PROTECTafter every relocation is applied (its own bytoyos::relocate, the rest by the library load where it names one), then runs the libraries' constructors as before. The pages are counted from the executable's header, the first byte of itsPT_LOADat file offset 0. A last partial page stays writable, as for a library. A refusal exits 127 with a line on stderr, as a failed library load does.The manual's applications page states the requirement: a program seals its own relocated data as the loader does a library's.
Tests
an_executables_relro_pages_are_counted_from_its_header, which covers whole and partial pages, no RELRO, and an image whose addresses do not start at 0.relro_sealed: a child stores to a relocated word it has checked lies in a whole RELRO page, and the parent holds that the child ends asFault::Memory. Only a booted system can show this, because the claim is that the kernel faults a store to a page the program protected. On x86-64 it is a member of the T14'stestcasesshared metal boot (DRIVEN_AND_SHARED). On AArch64 it is a job of the QEMU job case (virt_jobs_at_el2), which runs because the change is in the shared loader beneath both architectures. No x86-64 QEMU row runs it: shared-boot members run on metal.Evidence
Round 1's checks ran at
4a9e332e8. Index: r1/index.md.Host checks, chosen for what the change touches (log):
cargo test -p toyos --lib dl: exit 0, 21 passed,an_executables_relro_pages_are_counted_from_its_headeramong them. This is the SDK module the change is in.cargo test --test toyos-checks: exit 0, 44 passed.suite_splitholds the newDRIVEN_AND_SHAREDentry. No host check covers the job case's list; the guest run below does.cargo clippy -p toyos --all-targetsandcargo clippy -p toyos-build --all-targets, with the adopted lints and-D warnings: exit 0 each.--ci host, because nothing in the build system or harness mechanics changed. The merge queue runs it.Guest:
virt_jobs_at_el2(run 43046428666): success (log). The job case builtrelro_sealedfor AArch64 and printedrelro_sealed: a store to a relocated word of PT_GNU_RELRO ended its program as a memory fault. Every other job in that boot also starts through the changed path.executable-relro-read-only-mut(6e1dcac,seal()not called), run 43046434572: red for its reason (log). The holder's assertion fails with "a store to PT_GNU_RELRO did not end the program as a memory fault (exit status: 0)". The storing copy printed "the store to PT_GNU_RELRO landed". Every other job of the case ended with exit 0 and printed its line, includingabuse_readonly_copyout, which runs after it.61007d23a: run 43093182539, queued. No x86-64 guest row runsrelro_sealed, so this run instead shows that sealing breaks no program: every boot starts the system's servers through the changed path, and these rows launch many more:libc_children: every C program undertests/guest/libc, plus five Rust ones, each starting children;session_lifecycleanda_launch_gives_each_unsent_stdio_slot_a_null;std_threads_count_the_cpus_and_give_back_ended_threads;a_spawn_from_an_image_frees_it_when_the_child_ends;netstack_holders_exported_to_children;the_compositor_drives_the_virtio_gpuanda_window_is_drawn_at_the_scale_and_relaid_at_the_next;console_image_boots.hosted_clang_hellois left out. Its clang and LLD would be the largest C++ programs to seal, but the SDK change alters the sysroot the hosted toolchain is keyed on, so the runner would rebuild LLVM first.Review round 1 (
b2172a8d6, then the records-only61007d23a, index) changes onlystart's andseal's comments, the applications page and one host test. So the image, and every guest and metal result at4a9e332e8, is unchanged by it.cargo test -p toyos --lib dl: exit 0, 21 passed (log).an_executables_relro_pages_are_counted_from_its_headergains two cases: a header's segment above anotherPT_LOAD, and a RELRO below the header, which is refused.executable_relrowithout the header segment's start subtracted fails the new case (exit 101).cargo clippy -p toyos --all-targets: exit 0.The head is
5f0467f28, rebased onto main75bdec3cd, which carries #1259 (r4 index).DRIVEN_AND_SHARED,VIRT_*,VIRT_JOBSandextra_root_files. Each is resolved so both sides hold, withvm_map_sharesbeforerelro_sealed.61007d23a. So guest run 43093182539 at61007d23astands. Branchexecutable-relro-read-only-run43093182539keeps that commit reachable until the run ends.PT_LOAD, so the seal still protects the child's private copy.program::lay_outrefuses twoPT_LOADs that share a page, so no shared text page is ever a RELRO page.SYS_VM_PROTECT.cargo test -p toyos --lib dl(23),toyos-checks(52), and clippy ontoyosandtoyos-buildall exit 0 (log).Metal is not a landing condition: the benches are down. The record's records-only commit
61007d23akeeps it open with Owed on metal: the T14'stestcasesshared boot, for one batched boot of main once a bench returns. Main must show:relro_sealed: a store to a relocated word of PT_GNU_RELRO ended its program as a memory fault, which is x86-64's verdict;PT_GNU_RELRO could not be made read-only;testcasesboot before the seal did not have.The images staged for it at
4a9e332e8are deleted. Their request records what was asked.Coordination
map7b's step 1 (#1259) landed first, and this branch is rebased onto it, as above.
Risks
A program that writes its own RELRO after start now faults. Nothing in the tree is known to. Every program starts through this path. On x86-64 only the T14's whole
testcasesshared boot (every discovered Rust test and the C corpus) would show it, and that boot is owed on metal. Until it runs, x86-64 has only the host tests and the shared SDK code behind it, and AArch64 has the job case.🤖 Generated with Claude Code