From d8e04005fbe99b0202c841e743ad882e8b40b071 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 16:33:33 +0200 Subject: [PATCH 1/3] An executable's PT_GNU_RELRO is read-only once it has relocated itself `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 --- docs/src/architecture/applications.md | 6 ++ issues/an-executables-relro-stays-writable.md | 24 ------ sdk/toyos/src/dl/image/mod.rs | 12 +++ sdk/toyos/src/dl/image/tests.rs | 33 +++++++- sdk/toyos/src/dl/mod.rs | 58 +++++++++---- .../guest/rust-tests/src/bin/relro_sealed.rs | 82 +++++++++++++++++++ tests/images/virtjobcase.toml | 2 +- tests/toyos.rs | 10 ++- 8 files changed, 184 insertions(+), 43 deletions(-) delete mode 100644 issues/an-executables-relro-stays-writable.md create mode 100644 tests/guest/rust-tests/src/bin/relro_sealed.rs diff --git a/docs/src/architecture/applications.md b/docs/src/architecture/applications.md index 4812f9fbd02..5db2e3a3689 100644 --- a/docs/src/architecture/applications.md +++ b/docs/src/architecture/applications.md @@ -42,6 +42,12 @@ linking and library loading are userland work shared by the Rust and C runtimes. A program's image is read by userland through its own view and handed to the kernel as memory, never named by a path the kernel resolves. +A program relocates itself as it starts, and once its relocations are applied +it makes read-only the part of its image that only they write, as the loader +does for each library. The protection is the program's defence against its own +stray or hijacked stores: it does this to itself, and one that did not would +gain nothing over any other process. + ## Desktop apps A window's pixels reach the compositor through shared memory. The toolkit and diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md deleted file mode 100644 index 5556ef286bb..00000000000 --- a/issues/an-executables-relro-stays-writable.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# An executable's RELRO stays writable for its whole life - -`PT_GNU_RELRO` names what a program's relocations write once and nothing -writes after: `.data.rel.ro`, `.dynamic`, `.got`, every vtable and every table -of function pointers. A parent lays it out as part of the writable range that -holds it (`toyos::elf::program`), which the kernel maps as asked; nothing reads -an executable's `PT_GNU_RELRO`, and nothing takes the write away once -`toyos::relocate` has run. A stray or hostile write in a running program can -rewrite a vtable. A library's is read-only once the SDK's loader has relocated -it (`sdk/toyos/src/dl/mod.rs`, `protect`); an executable's is not, and the same -`SYS_VM_PROTECT` is what its start would call. - -Owner: the userland loader of -`issues/the-kernel-still-parses-what-userland-writes.md`. - -Exit: once a program has relocated itself its `PT_GNU_RELRO` range is -read-only, through a protection primitive the program calls or a mapping -granular enough to hold it apart, and a guest test's write to it is refused. diff --git a/sdk/toyos/src/dl/image/mod.rs b/sdk/toyos/src/dl/image/mod.rs index 34b63680ef0..e059738d58a 100644 --- a/sdk/toyos/src/dl/image/mod.rs +++ b/sdk/toyos/src/dl/image/mod.rs @@ -68,6 +68,18 @@ pub(crate) fn plan(header: &[u8]) -> Result { Ok(Plan { layout, span }) } +/// The whole pages of the `PT_GNU_RELRO` of the executable `header` begins, +/// as an offset from `header` and a length: `header` is the first byte of +/// the `PT_LOAD` at file offset 0, wherever the image's lowest address is. +pub(crate) fn executable_relro(header: &[u8]) -> Result, Refused> { + let layout = Layout::parse(header, MACHINE).map_err(|e| e.as_str())?; + let Some((lo, hi)) = layout.relro_pages(PAGE) else { return Ok(None) }; + let first = layout.segments().iter().find(|seg| seg.file_offset() == 0); + let first = first.ok_or("ELF: no PT_LOAD maps the executable's header")?; + let at = lo.checked_sub(first.image().start().get()).ok_or("ELF: PT_GNU_RELRO lies below the executable's header")?; + Ok(Some((at as usize, (hi - lo) as usize))) +} + impl Plan { /// A library's segments, which the loader protects as their flags say: /// one both writable and executable is refused rather than given either. diff --git a/sdk/toyos/src/dl/image/tests.rs b/sdk/toyos/src/dl/image/tests.rs index 66a2a76b806..d3fdc5cd977 100644 --- a/sdk/toyos/src/dl/image/tests.rs +++ b/sdk/toyos/src/dl/image/tests.rs @@ -9,7 +9,7 @@ use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; -use super::{plan, relocate, FilePages, Mapped, Pass, Scope}; +use super::{executable_relro, plan, relocate, FilePages, Mapped, Pass, Scope}; use crate::dl::Refused; use crate::elf::RelocError; @@ -304,6 +304,37 @@ fn an_image_off_zero_or_with_segments_sharing_a_page_is_refused() { assert_eq!(plan(&shared[..0x1000]).map(|_| ()).map_err(|e| e.to_string()), Err("ELF: two PT_LOAD segments share a page".to_string())); } +/// An executable's RELRO is where its header says, counted from the header: +/// the header's segment need not be where the image's addresses begin. +#[test] +fn an_executables_relro_pages_are_counted_from_its_header() { + /// `PT_TLS`'s header made `PT_GNU_RELRO` over `[vaddr, vaddr + memsz)`. + fn relro(vaddr: u64, memsz: u64) -> impl FnOnce(&mut [u8]) { + move |b| { + let p = PHDRS + 3 * 56; + b[p..p + 4].copy_from_slice(&0x6474_e552u32.to_le_bytes()); + b[p + 16..p + 24].copy_from_slice(&vaddr.to_le_bytes()); + b[p + 40..p + 48].copy_from_slice(&memsz.to_le_bytes()); + } + } + let pages = |file: &[u8]| executable_relro(&file[..0x1000]).map_err(|e| e.to_string()); + assert_eq!(pages(&object(&[], RELA, relro(DATA, 0x1000))), Ok(Some((0x1000, 0x1000)))); + // Its last, partial page stays writable, and one inside a page is none. + assert_eq!(pages(&object(&[], RELA, relro(DATA, 0xfff))), Ok(None)); + assert_eq!(pages(&object(&[], RELA, |_| {})), Ok(None)); + // Every address 0x1_0000 higher: the header is still the image's first byte. + let lifted = object(&[], RELA, |b| { + relro(DATA + 0x1_0000, 0x1000)(b); + for p in [PHDRS, PHDRS + 56, PHDRS + 2 * 56] { + let vaddr = u64::from_le_bytes(b[p + 16..p + 24].try_into().unwrap()) + 0x1_0000; + b[p + 16..p + 24].copy_from_slice(&vaddr.to_le_bytes()); + } + let entry = u64::from_le_bytes(b[24..32].try_into().unwrap()) + 0x1_0000; + b[24..32].copy_from_slice(&entry.to_le_bytes()); + }); + assert_eq!(pages(&lifted), Ok(Some((0x1000, 0x1000)))); +} + /// A library's segment both writable and executable is refused rather than /// given either protection. #[test] diff --git a/sdk/toyos/src/dl/mod.rs b/sdk/toyos/src/dl/mod.rs index 16e5a7592a6..030582c0802 100644 --- a/sdk/toyos/src/dl/mod.rs +++ b/sdk/toyos/src/dl/mod.rs @@ -397,32 +397,58 @@ unsafe fn construct(inits: &[(usize, usize)]) { } /// Load the libraries the executable at `header` names and bind its symbols -/// to them, then run their constructors: std's and libc's start call this -/// once its thread-local storage is laid out, before anything that could -/// name such a symbol. One whose libraries cannot be had exits 127 with a -/// line on its stderr naming why, as [`crate::relocate`] does. +/// to them, take the write away from its `PT_GNU_RELRO` pages, then run the +/// libraries' constructors: std's and libc's start call this once its +/// thread-local storage is laid out, before anything that could name such a +/// symbol. One whose libraries cannot be had exits 127 with a line on its +/// stderr naming why, as [`crate::relocate`] does. /// /// # Safety /// `header` is the running executable's ELF header, its thread-local storage /// is laid out ([`tls::start`]) and this is its main thread, the only one. pub unsafe fn start(header: *const u8) { - if !crate::relocate::names_a_library() { - return; + let refuse = |line: String| -> ! { + if let Some(stderr) = crate::relocate::stderr() { + let _ = syscall::write(stderr, line.as_bytes()); + } + syscall::exit(127) + }; + let published = match crate::relocate::names_a_library() { + // SAFETY: the caller's. + true => match unsafe { load_needed(header) } { + Ok(published) => Some(published), + Err(why) => refuse(alloc::format!("this program's libraries could not be loaded: {why}\n")), + }, + false => None, + }; + // SAFETY: the caller's, and every relocation of the executable is applied: + // its own by `crate::relocate`, the rest by the load above. + if let Err(why) = unsafe { seal(header) } { + refuse(alloc::format!("this program's PT_GNU_RELRO could not be made read-only: {why}\n")); } - // SAFETY: the caller's. - match unsafe { load_needed(header) } { + if let Some(published) = published { // SAFETY: each a module just published. - Ok(published) => unsafe { construct(&published.inits) }, - Err(why) => { - let line = alloc::format!("this program's libraries could not be loaded: {why}\n"); - if let Some(stderr) = crate::relocate::stderr() { - let _ = syscall::write(stderr, line.as_bytes()); - } - syscall::exit(127) - } + unsafe { construct(&published.inits) }; } } +/// Make the executable's `PT_GNU_RELRO` pages read-only, as [`protect`] does +/// a library's: its vtables, `.got` and `.dynamic`, which a stray or hostile +/// store in the running program could otherwise rewrite. The program does +/// this to itself; one that skipped it would only be without it. +/// +/// # Safety +/// `header` is the running executable's ELF header, and nothing writes its +/// `PT_GNU_RELRO` again. +unsafe fn seal(header: *const u8) -> Result<(), Refused> { + // SAFETY: the caller's; the page the header begins is mapped. + let page = unsafe { core::slice::from_raw_parts(header, HEADER) }; + let Some((at, len)) = image::executable_relro(page)? else { return Ok(()) }; + // SAFETY: whole pages of the executable's own image, which the header's + // segment begins. + unsafe { syscall::vm_protect(header.cast_mut().add(at), len, VmProt::Read) }.map_err(Refused::Memory) +} + /// # Safety /// [`start`]'s. unsafe fn load_needed(header: *const u8) -> Result { diff --git a/tests/guest/rust-tests/src/bin/relro_sealed.rs b/tests/guest/rust-tests/src/bin/relro_sealed.rs new file mode 100644 index 00000000000..eec737f120d --- /dev/null +++ b/tests/guest/rust-tests/src/bin/relro_sealed.rs @@ -0,0 +1,82 @@ +//! Once a program has relocated itself its `PT_GNU_RELRO` pages are +//! read-only: a copy of this program that stores to a word its relocations +//! wrote is ended as a memory fault, and never reaches the line after the +//! store. + +use std::os::toyos::process::ExitStatusExt; +use std::process::Command; + +use toyos_abi::end::{End, Fault}; + +const STORES: &str = "stores"; +/// What the storing copy exits with when the word is in no whole page of its +/// `PT_GNU_RELRO`: the test's premise is gone, not its subject. +const NOT_IN_RELRO: i32 = 3; + +const PT_LOAD: u32 = 1; +const PT_GNU_RELRO: u32 = 0x6474_e552; +const PH_SIZE: usize = 56; +const PAGE: usize = 4096; + +fn answer() -> u32 { + 42 +} + +/// A function's address, which the linker cannot know: a word this program's +/// relocations write, and so one the linker puts in `PT_GNU_RELRO`. +static TABLE: [fn() -> u32; 1] = [answer]; + +fn main() { + match std::env::args().nth(1).as_deref() { + Some(STORES) => stores(), + Some(other) => panic!("relro_sealed: unknown role {other:?}"), + None => holder(), + } +} + +/// The whole pages of this program's `PT_GNU_RELRO`, as addresses. +fn relro_pages() -> Option<(usize, usize)> { + unsafe extern "C" { + static __ehdr_start: u8; + } + let header = &raw const __ehdr_start; + // SAFETY: the image's own header and program header table, which its + // first segment maps. + unsafe { + let phoff = header.add(32).cast::().read_unaligned() as usize; + let phnum = usize::from(header.add(56).cast::().read_unaligned()); + let kind = |i: usize| header.add(phoff + i * PH_SIZE).cast::().read_unaligned(); + let word = |i: usize, at: usize| header.add(phoff + i * PH_SIZE + at).cast::().read_unaligned() as usize; + // The header is the first byte of the `PT_LOAD` at file offset 0. + let first = (0..phnum).find(|&i| kind(i) == PT_LOAD && word(i, 8) == 0)?; + let relro = (0..phnum).find(|&i| kind(i) == PT_GNU_RELRO)?; + let start = header.addr() + word(relro, 16) - word(first, 16); + Some((start & !(PAGE - 1), (start + word(relro, 40)) & !(PAGE - 1))) + } +} + +fn stores() { + let slot = std::hint::black_box((&raw const TABLE).cast_mut().cast::()); + match relro_pages() { + Some((lo, hi)) if (lo..hi).contains(&slot.addr()) => {} + pages => { + eprintln!("relro_sealed: the table at {:#x} is in no whole page of PT_GNU_RELRO ({pages:x?})", slot.addr()); + std::process::exit(NOT_IN_RELRO); + } + } + // SAFETY: none: the store is the test, and the kernel ends this program at it. + unsafe { slot.write_volatile(0) }; + println!("relro_sealed: the store to PT_GNU_RELRO landed"); +} + +fn holder() { + assert_eq!(TABLE[0](), 42, "relro_sealed: the relocated word does not read"); + let exe = std::env::current_exe().expect("relro_sealed: current_exe"); + let status = Command::new(&exe).arg(STORES).status().expect("relro_sealed: its storing copy"); + assert_eq!( + End::from_word(status.into_raw()), + Some(End::Fault(Fault::Memory)), + "relro_sealed: a store to PT_GNU_RELRO did not end the program as a memory fault ({status})", + ); + println!("relro_sealed: a store to a relocated word of PT_GNU_RELRO ended its program as a memory fault"); +} diff --git a/tests/images/virtjobcase.toml b/tests/images/virtjobcase.toml index 94fb02300fd..64a46a17f60 100644 --- a/tests/images/virtjobcase.toml +++ b/tests/images/virtjobcase.toml @@ -22,7 +22,7 @@ restart = true receives = ["power"] # Four minutes from boot rather than one: a job here may run on an emulated # CPU, and a job past the bound reboots the guest with the job named. -args = ["--bound-ms=240000", "preempt", "fp_isolation", "first_entry", "unmap_touch", "debug_refused", "test_rs_ring0_timer_in_syscall", "test_rs_random_draws", "test_rs_wall_clock_now", "test_rs_user_copy_faults", "test_rs_vm_map_shares", "test_rs_abuse_readonly_copyout"] +args = ["--bound-ms=240000", "preempt", "fp_isolation", "first_entry", "unmap_touch", "debug_refused", "test_rs_ring0_timer_in_syscall", "test_rs_random_draws", "test_rs_wall_clock_now", "test_rs_user_copy_faults", "test_rs_vm_map_shares", "test_rs_relro_sealed", "test_rs_abuse_readonly_copyout"] [programs.kernelprobe] diff --git a/tests/toyos.rs b/tests/toyos.rs index afee118c029..4a0d8e1559b 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -388,6 +388,9 @@ const DRIVEN_AND_SHARED: &[&str] = &[ // Its shared run is the x86-64 verdict; `virt_vm_map_shares` builds it // for AArch64 and runs it on that architecture's job case. "vm_map_shares", + // Its shared run is the x86-64 verdict; `virt_jobs_at_el2` builds it for + // AArch64 and runs it on that architecture's job case. + "relro_sealed", ]; /// What the boot's audit of the kernel's tables says when every leaf passed @@ -2918,6 +2921,10 @@ const VIRT_USER_COPY: &str = "user_copy_faults"; /// page mapped executable, which no other AArch64 row reaches. const VIRT_VM_MAP: &str = "vm_map_shares"; +/// A program's `PT_GNU_RELRO` is read-only once it has relocated itself, on +/// AArch64; the shared boot's member on x86-64. +const VIRT_RELRO: &str = "relro_sealed"; + /// The instant `tests/images/virtjobcase.toml`'s RTC starts at, and the same in Unix /// seconds: years from any host's own, so a reading on it came from the RTC. const VIRT_RTC_BASE: &str = "2033-03-07T09:14:25"; @@ -2939,6 +2946,7 @@ const VIRT_JOBS: &[(&str, &str)] = &[ ("test_rs_wall_clock_now", WALL_CLOCK_SAID), ("test_rs_user_copy_faults", "user copies: all checks passed"), ("test_rs_vm_map_shares", "vm_map_shares: an object's pages are mapped where asked"), + ("test_rs_relro_sealed", "relro_sealed: a store to a relocated word of PT_GNU_RELRO ended its program as a memory fault"), ("test_rs_abuse_readonly_copyout", "a syscall writes only where its caller could store"), ]; @@ -2963,7 +2971,7 @@ fn boot_virt_jobs(profile: qemu::Profile) -> QemuInstance { smp: 1, kernel_features: toyos_build::build::TEST_KERNEL, ready_marker: "control registers: SCTLR_EL1=", - extra_root_files: [VIRT_COPYOUT, VIRT_RING0_TIMER, VIRT_RANDOM, VIRT_WALL_CLOCK, VIRT_USER_COPY, VIRT_VM_MAP] + extra_root_files: [VIRT_COPYOUT, VIRT_RING0_TIMER, VIRT_RANDOM, VIRT_WALL_CLOCK, VIRT_USER_COPY, VIRT_VM_MAP, VIRT_RELRO] .map(|name| suite_bin(profile.arch(), name)) .to_vec(), rtc_base: Some(VIRT_RTC_BASE), From 6e8d06656ff4ecf90b3cb90876596eef1524dfc6 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 19:20:58 +0200 Subject: [PATCH 2/3] Review round 1 of #1237: start's contract names the RELRO refusal, and 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 --- docs/src/architecture/applications.md | 4 ++-- sdk/toyos/src/dl/image/tests.rs | 11 +++++++++++ sdk/toyos/src/dl/mod.rs | 8 ++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/src/architecture/applications.md b/docs/src/architecture/applications.md index 5db2e3a3689..9ff703394c6 100644 --- a/docs/src/architecture/applications.md +++ b/docs/src/architecture/applications.md @@ -45,8 +45,8 @@ kernel as memory, never named by a path the kernel resolves. A program relocates itself as it starts, and once its relocations are applied it makes read-only the part of its image that only they write, as the loader does for each library. The protection is the program's defence against its own -stray or hijacked stores: it does this to itself, and one that did not would -gain nothing over any other process. +stray or hijacked stores. The kernel does not enforce it; a program that skipped +it would weaken only itself. ## Desktop apps diff --git a/sdk/toyos/src/dl/image/tests.rs b/sdk/toyos/src/dl/image/tests.rs index d3fdc5cd977..d86deab05ee 100644 --- a/sdk/toyos/src/dl/image/tests.rs +++ b/sdk/toyos/src/dl/image/tests.rs @@ -333,6 +333,17 @@ fn an_executables_relro_pages_are_counted_from_its_header() { b[24..32].copy_from_slice(&entry.to_le_bytes()); }); assert_eq!(pages(&lifted), Ok(Some((0x1000, 0x1000)))); + // The header's segment above another: the writable segment at address 0, + // the header's at 0x1000. + let above = |relro_at: u64| { + object(&[], RELA, |b| { + relro(relro_at, 0x1000)(b); + b[PHDRS + 16..PHDRS + 24].copy_from_slice(&0x1000u64.to_le_bytes()); + b[PHDRS + 56 + 16..PHDRS + 56 + 24].copy_from_slice(&0u64.to_le_bytes()); + }) + }; + assert_eq!(pages(&above(0x1000)), Ok(Some((0, 0x1000)))); + assert_eq!(pages(&above(0)), Err("ELF: PT_GNU_RELRO lies below the executable's header".to_string())); } /// A library's segment both writable and executable is refused rather than diff --git a/sdk/toyos/src/dl/mod.rs b/sdk/toyos/src/dl/mod.rs index 030582c0802..65484ab3097 100644 --- a/sdk/toyos/src/dl/mod.rs +++ b/sdk/toyos/src/dl/mod.rs @@ -400,8 +400,9 @@ unsafe fn construct(inits: &[(usize, usize)]) { /// to them, take the write away from its `PT_GNU_RELRO` pages, then run the /// libraries' constructors: std's and libc's start call this once its /// thread-local storage is laid out, before anything that could name such a -/// symbol. One whose libraries cannot be had exits 127 with a line on its -/// stderr naming why, as [`crate::relocate`] does. +/// symbol. One whose libraries cannot be had, or whose `PT_GNU_RELRO` cannot +/// be made read-only, exits 127 with a line on its stderr naming why, as +/// [`crate::relocate`] does. /// /// # Safety /// `header` is the running executable's ELF header, its thread-local storage @@ -434,8 +435,7 @@ pub unsafe fn start(header: *const u8) { /// Make the executable's `PT_GNU_RELRO` pages read-only, as [`protect`] does /// a library's: its vtables, `.got` and `.dynamic`, which a stray or hostile -/// store in the running program could otherwise rewrite. The program does -/// this to itself; one that skipped it would only be without it. +/// store in the running program could otherwise rewrite. /// /// # Safety /// `header` is the running executable's ELF header, and nothing writes its From 5f0467f287cfb1091229198272eac182e6cc2e68 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 22:00:51 +0200 Subject: [PATCH 3/3] The executable RELRO record stays open for its x86-64 boot, owed on metal `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 --- issues/an-executables-relro-stays-writable.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 issues/an-executables-relro-stays-writable.md diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md new file mode 100644 index 00000000000..026efe7588e --- /dev/null +++ b/issues/an-executables-relro-stays-writable.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# An executable's RELRO stays writable for its whole life + +**Built**: `toyos::dl::start` makes an executable's whole `PT_GNU_RELRO` +pages read-only with `SYS_VM_PROTECT` once every relocation is applied, as +the SDK's loader does a library's. `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 AArch64 job case (`virt_jobs_at_el2`) runs it, and the same row +without the seal is red. On x86-64 it is a member of the T14's `testcases` +shared boot, which no guest row runs. + +**Left**: +- **Owed on metal:** the T14's `testcases` shared boot. Main must show the + whole boot green, every discovered Rust test and every C corpus case among + it; the line `relro_sealed: a store to a relocated word of PT_GNU_RELRO + ended its program as a memory fault`; no program saying `PT_GNU_RELRO could + not be made read-only`; and no exit 127 that main's last `testcases` boot + before the seal did not have. + +Exit: once a program has relocated itself its `PT_GNU_RELRO` range is +read-only, through a protection primitive the program calls or a mapping +granular enough to hold it apart, and a guest test's write to it is refused.