From 818cefd88b4165cf80299138c94e2bbc0eb3c337 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:26:19 +0200 Subject: [PATCH 01/15] File: a relocation in an executable's TLS template reaches no thread's block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel copies an executable's PT_TLS template out of the file at spawn and builds every thread's block from that copy, while it applies the executable's RELATIVE relocations to image pages as they fault in. A RELATIVE inside [PT_TLS vaddr, +filesz) relocates .tdata and never the copies. rust-lld writes one for a #[thread_local] static holding a &'static str (measured on a static PIE of toyos/tests/relocate/fixture.rs built with --cfg tls, both architectures). Measured absent from every shipped program, clang and LLD (roast-v4 §0). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...s-tls-template-reaches-no-threads-block.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md diff --git a/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md b/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md new file mode 100644 index 00000000000..871b43b31c7 --- /dev/null +++ b/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-10 +--- + +# A relocation in an executable's TLS template reaches no thread's block + +The kernel copies an executable's `PT_TLS` template out of the file at spawn +(`kernel/src/loader/mod.rs`, `elf_alloc`) and builds every thread's TLS block +from that copy, while it applies the executable's `RELATIVE` relocations to the +image's pages as they fault in (`kernel/src/elf/index.rs`). A `RELATIVE` whose +`r_offset` lies in `[PT_TLS vaddr, + filesz)` relocates the image's `.tdata` +and never the copies: every thread reads the link-time pointer. + +rust-lld writes one for a `#[thread_local]` static holding a `&'static str`: +`readelf -lr` of such a static PIE for `x86_64-unknown-toyos` reads +`TLS 0x000478 0x2478 … 0x10` and an `R_X86_64_RELATIVE` at `0x2478`. None of +the shipped programs carries one, nor clang or LLD. + +Owner: stage 1 of `issues/the-kernel-still-parses-what-userland-writes.md`. + +Exit: an executable with a relocation in its TLS template is refused, and a +host test on rust-lld's own output of one shows it. From 4c116577f1b845c3330b4dba5e0c4759cf57ec6b Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:26:34 +0200 Subject: [PATCH 02/15] Executables relocate themselves; the kernel reads no executable's dynamic section Stage 1 of Move 1 of issues/the-kernel-still-parses-what-userland-writes.md. toyos::relocate applies an image's own RELATIVE relocations, called first by std's and libc's _start with __ehdr_start's address, found PC-relatively, before anything reads a relocated word. A pure core over the program headers, the dynamic section and the RELA table, all byte slices, answers each write as (vaddr, value); one unsafe wrapper builds the slices from the running image and writes. The core is held to no panic path by clippy's indexing_slicing, panic, unwrap_used, expect_used and arithmetic_side_effects, since a panic formats through pointers that are not relocated yet. A refusal writes one literal to slot 2 and exits 127. It refuses by name DT_NEEDED, a DT_JMPREL with entries, DT_REL, DT_RELR, DT_TEXTREL and DF_TEXTREL, every type but the machine's RELATIVE (read off e_machine, so both architectures' constants build on every host), a write outside a writable PT_LOAD, and a write meeting the PT_TLS template, which the kernel copies from the file into every thread's block and which a write here would never reach: that closes issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md, filed in the commit before this one. The kernel loses the executable half of its loader: read_exe_tables and everything it read (PT_DYNAMIC, DT_RELA, DT_JMPREL, .dynsym, .dynstr, .gnu.hash, .symtab, the section-header search for .rela.dyn), the relocation index and its per-page application in the fault path, the fault record's relocation count, start-up DT_NEEDED loading and binding (loader/symbols.rs, resolve_lib_bind_relocs) and the executable's TLS relocations. The TLS layout at spawn is the executable's module alone, id 1; dlopen numbers from 2. toyos-elf loses the API only that path used (for_executable, ExeReservation, ExeRefusal, FillLattice, FILL_GRANULE, StraddlesFillPage, Rules::fill) and its tests, and the fuzz test its executable mode. The spawn record loses unresolved=, relocs= and deps=. No toolchain or target-spec change: every executable is already a PIE with no PT_INTERP whose relocations are RELATIVE alone. The interval it opens: an executable with a DT_NEEDED does not start, so std_tls is deleted and comes back with the userland loader, as the track now records; dlopen_dedup's two-spellings arm loses the kernel cache spelling it guarded and goes; abuse_elf_loader's spawn cases on tables the kernel no longer reads are answered either way and their child dies. The RELR defect keeps its dlopen half; the package-libraries defect is restated; RELRO staying writable is filed. The comment issues/comments-name-defects-no-issue-holds.md cited goes with case 12's index, and the tree no longer has what it named: KernelAllocator::alloc checks MAX_HEAP_ALLOC before it takes the dlmalloc lock. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/main.rs | 1 - ...a-package-cannot-ship-its-own-libraries.md | 11 +- ...s-tls-template-reaches-no-threads-block.md | 24 - issues/an-executables-relro-stays-writable.md | 22 + .../comments-name-defects-no-issue-holds.md | 18 - ...ernel-still-parses-what-userland-writes.md | 11 + ...-binary-whose-relr-table-it-never-reads.md | 21 +- kernel/src/elf/cache.rs | 1 - kernel/src/elf/index.rs | 182 ------- kernel/src/elf/mod.rs | 11 +- kernel/src/elf/reloc.rs | 29 +- kernel/src/loader/mod.rs | 470 +----------------- kernel/src/loader/symbols.rs | 69 --- kernel/src/loader/tls.rs | 62 +-- kernel/src/process.rs | 37 +- sdk/std/sys/pal/mod.rs | 8 + tests/checks.rs | 16 +- tests/checks/metal.rs | 2 +- .../src/bin/abuse_elf_loader.rs | 156 +++--- .../toyos-rust-tests/src/bin/dlopen_dedup.rs | 34 -- tests/toyos-rust-tests/src/bin/std_tls.rs | 61 --- tests/toyos-rust-tests/tls-lib/Cargo.toml | 2 +- toyos-elf/src/rela.rs | 82 +-- toyos-elf/tests/fuzz.rs | 54 +- toyos-elf/tests/tables.rs | 93 +--- toyos/src/lib.rs | 1 + toyos/src/relocate/mod.rs | 363 ++++++++++++++ toyos/src/relocate/tests.rs | 274 ++++++++++ toyos/tests/relocate/aarch64-applied.elf | Bin 0 -> 3800 bytes toyos/tests/relocate/aarch64-tls.elf | Bin 0 -> 4048 bytes toyos/tests/relocate/aarch64.elf | Bin 0 -> 3800 bytes toyos/tests/relocate/fixture.rs | 59 +++ toyos/tests/relocate/x86_64-applied.elf | Bin 0 -> 3312 bytes toyos/tests/relocate/x86_64-tls.elf | Bin 0 -> 3512 bytes toyos/tests/relocate/x86_64.elf | Bin 0 -> 3312 bytes userland/libc/src/arch/aarch64.rs | 5 + userland/libc/src/arch/x86_64.rs | 4 + 37 files changed, 921 insertions(+), 1262 deletions(-) delete mode 100644 issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md create mode 100644 issues/an-executables-relro-stays-writable.md delete mode 100644 issues/comments-name-defects-no-issue-holds.md delete mode 100644 kernel/src/elf/index.rs delete mode 100644 kernel/src/loader/symbols.rs delete mode 100644 tests/toyos-rust-tests/src/bin/std_tls.rs create mode 100644 toyos/src/relocate/mod.rs create mode 100644 toyos/src/relocate/tests.rs create mode 100755 toyos/tests/relocate/aarch64-applied.elf create mode 100755 toyos/tests/relocate/aarch64-tls.elf create mode 100755 toyos/tests/relocate/aarch64.elf create mode 100644 toyos/tests/relocate/fixture.rs create mode 100755 toyos/tests/relocate/x86_64-applied.elf create mode 100755 toyos/tests/relocate/x86_64-tls.elf create mode 100755 toyos/tests/relocate/x86_64.elf diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index f8c07cfc548..e2effef765d 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -316,7 +316,6 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { let rules = rela::Rules { extent, window: (0, mem_size as u64), - fill: None, tls: None, }; let mut reloc_count = 0u64; diff --git a/issues/a-package-cannot-ship-its-own-libraries.md b/issues/a-package-cannot-ship-its-own-libraries.md index 00fa381e15d..1b379fdf6ec 100644 --- a/issues/a-package-cannot-ship-its-own-libraries.md +++ b/issues/a-package-cannot-ship-its-own-libraries.md @@ -7,11 +7,12 @@ opened: 2026-09-27 # A package cannot ship its own libraries A program on a file server is spawned from a memory image its parent read -(`SpawnArgs::image`), and `kernel/src/loader/mod.rs`'s `load_needed_libs` then -finds its `DT_NEEDED` libraries in `/system/lib` alone: the kernel opens no path -a file server holds. A package under `/apps//` whose binary needs a -library it carries beside it fails to spawn, where before the file servers -moved out of the kernel the executable's own directory was searched first. +(`SpawnArgs::image`), and nothing loads a `DT_NEEDED` library for it: the +kernel loads none at spawn, and an executable that names one is refused at its +own start (`toyos::relocate`) until the userland loader of +`issues/the-kernel-still-parses-what-userland-writes.md` loads them. A package +under `/apps//` whose binary needs a library it carries beside it fails +to start. ## Exit condition diff --git a/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md b/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md deleted file mode 100644 index 871b43b31c7..00000000000 --- a/issues/a-relocation-in-an-executables-tls-template-reaches-no-threads-block.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# A relocation in an executable's TLS template reaches no thread's block - -The kernel copies an executable's `PT_TLS` template out of the file at spawn -(`kernel/src/loader/mod.rs`, `elf_alloc`) and builds every thread's TLS block -from that copy, while it applies the executable's `RELATIVE` relocations to the -image's pages as they fault in (`kernel/src/elf/index.rs`). A `RELATIVE` whose -`r_offset` lies in `[PT_TLS vaddr, + filesz)` relocates the image's `.tdata` -and never the copies: every thread reads the link-time pointer. - -rust-lld writes one for a `#[thread_local]` static holding a `&'static str`: -`readelf -lr` of such a static PIE for `x86_64-unknown-toyos` reads -`TLS 0x000478 0x2478 … 0x10` and an `R_X86_64_RELATIVE` at `0x2478`. None of -the shipped programs carries one, nor clang or LLD. - -Owner: stage 1 of `issues/the-kernel-still-parses-what-userland-writes.md`. - -Exit: an executable with a relocation in its TLS template is refused, and a -host test on rust-lld's own output of one shows it. diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md new file mode 100644 index 00000000000..0420635d879 --- /dev/null +++ b/issues/an-executables-relro-stays-writable.md @@ -0,0 +1,22 @@ +--- +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. The kernel maps it as the writable `PT_LOAD` that holds +it, reads no `PT_GNU_RELRO` (`rg -i relro kernel/src toyos-elf/src` matches +nothing), and gives a program no way to take the write away once +`toyos::relocate` has run. A stray or hostile write in a running program can +rewrite a vtable. + +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/issues/comments-name-defects-no-issue-holds.md b/issues/comments-name-defects-no-issue-holds.md deleted file mode 100644 index 45d1addfba1..00000000000 --- a/issues/comments-name-defects-no-issue-holds.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-10-04 ---- - -# A comment names a defect that no issue file holds - -It cited an area directory, not a file, and flattening the tracker dropped the -pointer because no file under that area named the subject: - -- `tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs`, "the actuator for the - allocator-lock defect": a >2 MiB `KernelAllocator::alloc` assert fires while - the dlmalloc lock is held, and the comment says the defect "has stayed open". - No issue names it. - -**Exit condition:** it is either filed as a defect, if the tree still has it, -or its comment says it as an invariant and stops calling it a standing defect. diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index 4574801b669..a3aac811880 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -23,6 +23,17 @@ binary can only corrupt itself. Five syscalls retire, and the crafted-ELF corpus becomes the negative gate against a kernel that no longer parses most of it. +**Stage 1 is built: an executable relocates itself** (`toyos::relocate`, +called first by std's and libc's `_start`), and the kernel reads no +executable's dynamic section, symbol tables or relocations, and loads no +library at spawn. Left in Ring 0: `dlopen` and its four sibling syscalls, the +TLS block, and the program-header parse. **The interval it opens, owed by the +stage that brings the userland loader:** an executable with a `DT_NEEDED` +does not start (it is refused at its own start, exit 127), so `std_tls`, the +guest test of a startup library's TLS beside the executable's, is deleted at +the commit that lands stage 1 and comes back with that stage. Nothing shipped +links a library at load time. + The evidence that it is worth doing: nine dated commits over eleven days fixed userland-reachable kernel defects in this code, six of them machine-wide panics; seven of the loader's twelve bounds are over quantities a workload sets and two diff --git a/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md b/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md index 7e2bc0aed8e..b5d45df11c6 100644 --- a/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md +++ b/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md @@ -4,21 +4,20 @@ kind: defect opened: 2026-09-26 --- -# The program loader runs a binary whose `DT_RELR` table it never reads +# `dlopen` loads a library whose `DT_RELR` table it never reads -`toyos_elf::dynamic::Dynamic::parse` decodes the tags the loader acts on and -drops every other one (`_ => {}`), so a program or library linked with +`toyos_elf::dynamic::Dynamic::parse` decodes the tags the kernel's `dlopen` +acts on and drops every other one (`_ => {}`), so a library linked with `-z pack-relative-relocs` loads with its relative relocations — the ones `DT_RELR`/`DT_RELRSZ` name, and that `DT_RELA` then no longer holds — never applied. `DT_REL` and `DT_TEXTREL` are dropped the same way. The process runs -with every pointer in its data still the link-time one and dies somewhere -after, or does not die. +with every pointer in the library's data still the link-time one and dies +somewhere after, or does not die. Nothing in the tree links that way today: rustc passes `rust-lld` no -`--pack-dyn-relocs`, and LLD's default is none. The bootloader refuses the -kernel's equivalent (`SHT_RELR`) by name; the program loader has no such -refusal, and a binary is input from outside the kernel. +`--pack-dyn-relocs`, and LLD's default is none. An executable carrying any of +the three is refused at its own start (`toyos::relocate`); a library is input +from outside the kernel, and `dlopen` has no such refusal. -Exit: a dynamic section naming a relocation form the loader does not apply is -refused at spawn and at `dlopen`, and a crafted binary carrying `DT_RELR` is the -test. +Exit: a dynamic section naming a relocation form `dlopen` does not apply is +refused at `dlopen`, and a crafted library carrying `DT_RELR` is the test. diff --git a/kernel/src/elf/cache.rs b/kernel/src/elf/cache.rs index cf5a7fa632b..2f9b5bc520a 100644 --- a/kernel/src/elf/cache.rs +++ b/kernel/src/elf/cache.rs @@ -117,7 +117,6 @@ impl Snapshot { LoadedLib { memory, user_base, - phys_base: self.image.phys(), image: self.image, dynsym: self.dynsym, dynstr: self.dynstr, diff --git a/kernel/src/elf/index.rs b/kernel/src/elf/index.rs deleted file mode 100644 index 78dfe16cf20..00000000000 --- a/kernel/src/elf/index.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Pre-computed ELF relocations, applied per page as it is demand-faulted. -//! -//! Both `parse_rela_entries` and `RelocationIndex::with_capacity` sum two -//! independently-bounded counts and check the sum, since neither bound alone -//! catches the other's overflow. - -use alloc::vec::Vec; - -use crate::mm::{KernelSlice, MAX_HEAP_ALLOC}; -use toyos_abi::syscall::SyscallError; -use toyos_elf::rela::{self, ExeRefusal, Rules}; -use toyos_elf::{ImageOffset, Op, RelaCounts, RelaTable, RelocError, SymIndex, SymTab, TlsRef}; - -/// Relocation entries the loader needs, grouped by what it does with them; -/// each is `(r_offset, what it names)`, parsed. -pub struct ParsedRelaEntries { - /// `RELATIVE`: the position in the image the slot points at. - pub relative: Vec<(u64, ImageOffset)>, - /// `GLOB_DAT` and `JUMP_SLOT`. - pub glob_dat: Vec<(u64, SymIndex)>, - pub tpoff64: Vec<(u64, TlsRef)>, - pub tpoff32: Vec<(u64, TlsRef)>, -} - -/// The widest record any group keeps: a ceiling sized on it holds whichever -/// group turns out to be the whole table. -const WIDEST: usize = { - let (a, b, c) = ( - core::mem::size_of::<(u64, ImageOffset)>(), - core::mem::size_of::<(u64, SymIndex)>(), - core::mem::size_of::<(u64, TlsRef)>(), - ); - if a > b && a > c { a } else if b > c { b } else { c } -}; - -/// Why an executable's relocations are refused. -pub enum Refused { - Counts(ExeRefusal), - Entry(RelocError), -} - -impl Refused { - pub const fn as_str(&self) -> &'static str { - match self { - Refused::Counts(e) => e.as_str(), - Refused::Entry(e) => e.as_str(), - } - } - - pub const fn error(&self) -> SyscallError { - match self { - Refused::Counts(ExeRefusal::TooLarge) => SyscallError::ResourceExhausted, - Refused::Counts(ExeRefusal::TlsDescriptor) | Refused::Entry(_) => { - SyscallError::InvalidArgument - } - } - } -} - -/// Both relocation tables, parsed against `rules` and grouped, reserved -/// exactly from what `RelaCounts::for_executable` allows — or the first -/// refusal, before anything is kept. -pub fn parse_rela_entries( - rela_data: &[u8], - jmprel_data: &[u8], - rules: &Rules, - symbols: SymTab<'_>, -) -> Result { - let entries = || { - RelaTable::new(rela_data, crate::arch::ELF_MACHINE) - .iter() - .chain(RelaTable::new(jmprel_data, crate::arch::ELF_MACHINE).iter()) - }; - let counts = RelaCounts::of(entries()); - let reserve = counts.for_executable(WIDEST, MAX_HEAP_ALLOC).map_err(Refused::Counts)?; - let mut out = ParsedRelaEntries { - relative: Vec::with_capacity(reserve.relative), - glob_dat: Vec::with_capacity(reserve.bind), - tpoff64: Vec::with_capacity(reserve.tpoff64), - tpoff32: Vec::with_capacity(reserve.tpoff32), - }; - for raw in entries() { - let Some(r) = rela::parse(raw, rules, symbols).map_err(Refused::Entry)? else { continue }; - match r.op() { - Op::Relative(target) => out.relative.push((r.offset(), target)), - Op::Bind(sym) => out.glob_dat.push((r.offset(), sym)), - Op::Tpoff64(t) => out.tpoff64.push((r.offset(), t)), - Op::Tpoff32(t) => out.tpoff32.push((r.offset(), t)), - Op::DtpMod64(_) | Op::DtpOff64(_) => {} - } - } - Ok(out) -} - -/// Pre-computed writes, sorted by offset so a page's share is one binary search away. -pub struct RelocationIndex { - /// `RELATIVE`, `GLOB_DAT` and `TPOFF64` patches — all 8 bytes. - entries_u64: Vec<(u64, u64)>, - /// `TPOFF32` patches a 4-byte immediate in place. - entries_i32: Vec<(u64, i32)>, -} - -impl RelocationIndex { - /// Reserve exactly, or `None` when either collection would not fit one kernel allocation. - pub fn with_capacity(u64_count: usize, i32_count: usize) -> Option { - let fits = - |n: usize, width: usize| n.checked_mul(width).is_some_and(|b| b <= MAX_HEAP_ALLOC); - if !fits(u64_count, core::mem::size_of::<(u64, u64)>()) - || !fits(i32_count, core::mem::size_of::<(u64, i32)>()) - { - return None; - } - Some(Self { - entries_u64: Vec::with_capacity(u64_count), - entries_i32: Vec::with_capacity(i32_count), - }) - } - - pub fn add_u64(&mut self, offset: u64, value: u64) { - self.entries_u64.push((offset, value)); - } - - pub fn add_i32(&mut self, offset: u64, value: i32) { - self.entries_i32.push((offset, value)); - } - - /// Must be called once every entry has been added. - pub fn finalize(&mut self) { - self.entries_u64.sort_unstable_by_key(|&(off, _)| off); - self.entries_i32.sort_unstable_by_key(|&(off, _)| off); - } - - /// Applies the writes inside `[page_offset, page_offset + page.size())`, returning how many landed. - /// The bound comes from `page.size()`, not an assumed 4096, so it always matches the caller's allocation. - /// A relocation straddling the page's far edge is skipped, not clipped — the rest belongs to the next page. - pub fn apply_to_page(&self, page_offset: u64, page: KernelSlice) -> usize { - let end_offset = page_offset + page.size() as u64; - let mut count = 0usize; - - let start = self.entries_u64.partition_point(|&(off, _)| off < page_offset); - for &(r_offset, value) in &self.entries_u64[start..] { - if r_offset >= end_offset { - break; - } - let within_page = (r_offset - page_offset) as usize; - if within_page + 8 <= page.size() { - // SAFETY: `within_page + 8 <= page.size()` was just checked, and the frame isn't mapped anywhere yet. - unsafe { page.write::(within_page, value) }; - count += 1; - } - } - - let start = self.entries_i32.partition_point(|&(off, _)| off < page_offset); - for &(r_offset, value) in &self.entries_i32[start..] { - if r_offset >= end_offset { - break; - } - let within_page = (r_offset - page_offset) as usize; - if within_page + 4 <= page.size() { - // SAFETY: same as the loop above, for 4 bytes. - unsafe { page.write::(within_page, value) }; - count += 1; - } - } - count - } - - /// Whether any relocation falls inside `[page_offset, page_offset + 4096)`. - pub fn has_relocs_in_page(&self, page_offset: u64) -> bool { - let end_offset = page_offset + 4096; - let start_u64 = self.entries_u64.partition_point(|&(off, _)| off < page_offset); - if start_u64 < self.entries_u64.len() && self.entries_u64[start_u64].0 < end_offset { - return true; - } - let start_i32 = self.entries_i32.partition_point(|&(off, _)| off < page_offset); - start_i32 < self.entries_i32.len() && self.entries_i32[start_i32].0 < end_offset - } - - pub fn len(&self) -> usize { - self.entries_u64.len() + self.entries_i32.len() - } -} diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 98a1233f6e8..436ed499124 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -11,14 +11,12 @@ #![warn(clippy::undocumented_unsafe_blocks)] mod cache; -mod index; mod reloc; pub use cache::{cache_loaded_lib, try_clone_cached, CachedRelocs}; -pub use index::{parse_rela_entries, ParsedRelaEntries, RelocationIndex}; pub use reloc::{ - apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, compute_tpoff, - rebase_relative_relocs, resolve_dlopen_relocs, resolve_lib_bind_relocs, tpoff32_value, + apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, rebase_relative_relocs, + resolve_dlopen_relocs, }; use crate::mm::{align_2m_checked, KernelSlice, MAX_HEAP_ALLOC, PAGE_2M, PAGE_BYTES}; @@ -87,8 +85,6 @@ pub struct TlsModuleInfo<'a> { pub struct LoadedLib { pub memory: LibMemory, pub user_base: UserAddr, - /// Physical base address, for page table mappings. - pub phys_base: u64, /// Bounds-checked view of the whole loaded image. pub image: KernelSlice, /// `.dynsym`, clamped to the entries it actually holds. @@ -469,8 +465,6 @@ pub fn load_shared_lib( let rules = rela::Rules { extent, window, - // A library's image is written contiguously, with no fill-page edge. - fill: None, tls: layout.tls(), }; // Every entry is parsed here, and a refusal drops the image this pass has @@ -491,7 +485,6 @@ pub fn load_shared_lib( LoadedLib { memory: LibMemory::Owned(alloc), user_base: UserAddr::new(base_phys), - phys_base: base_phys, image, dynsym, dynstr, diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 3f5a7ff3e6e..9081645f5fe 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -14,7 +14,6 @@ //! reference whose `S + A` leaves the defining module's segment is refused. use super::{relocated_symbol, CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; -use crate::UserAddr; use toyos_elf::sym::{Sym, SymTab}; use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef, TlsSegment}; @@ -111,30 +110,6 @@ pub fn resolve_dlopen_relocs(lib: &LoadedLib, other_libs: &[LoadedLib]) -> u64 { unresolved } -/// Bind a startup library's `GLOB_DAT`/`JUMP_SLOT` slots, preferring the -/// executable's own exports; answers how many it left unresolved. -pub fn resolve_lib_bind_relocs( - lib: &LoadedLib, - exe_sym_map: &alloc::collections::BTreeMap<&str, UserAddr>, - libs: &[LoadedLib], -) -> u64 { - let symbols = lib.symbols(); - let mut unresolved = 0; - for (offset, sym) in lib.bind_entries() { - let name = relocated_symbol(symbols, sym).name_in(symbols.strings()); - let resolved = exe_sym_map - .get(name) - .copied() - .or_else(|| libs.iter().find_map(|other| other.resolve(name))); - match resolved { - // SAFETY: rela::tables_outside_window refused any image whose tables meet the window these writes land in. - Some(addr) => unsafe { lib.write_at::(offset, addr.raw()) }, - None => unresolved += 1, - } - } - unresolved -} - /// Apply `R_X86_64_TPOFF64` and `R_X86_64_TPOFF32`: the initial-exec TLS /// model, a fixed offset from the thread pointer. Answers how many references /// name a symbol no module defines; each is written as zero. @@ -181,7 +156,7 @@ pub fn apply_tpoff_relocs( /// A `TPOFF32`'s field is 32 bits the instruction sign-extends; a value outside /// them names some other address than the one resolved. -pub fn tpoff32_value(tpoff: i64) -> Result { +fn tpoff32_value(tpoff: i64) -> Result { i32::try_from(tpoff).map_err(|_| RelocError::TpoffOverflows) } @@ -284,7 +259,7 @@ fn resolve_tls_ref( } /// `S + A - tp` for one initial-exec reference, or `None` for a symbol no module defines. -pub fn compute_tpoff( +fn compute_tpoff( r: TlsRef, own_base_offset: usize, own_tls: Option, diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 04e22e8391a..4264e1eb0c1 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -1,22 +1,22 @@ //! Building a process out of an ELF file. //! -//! Segment contents are demand-paged through a relocation index the fault -//! handler applies per page; shared libraries are the exception and are -//! mapped eagerly since they are shared between processes. +//! The kernel reads an executable's program headers and its `PT_TLS` +//! template, demand-pages its `PT_LOAD`s and jumps to its entry; it reads no +//! dynamic section, applies no relocation and loads no library at spawn. The +//! program relocates itself (`toyos::relocate`). //! //! Every number the file names is untrusted: a refusal is //! `SyscallError::{InvalidArgument, ResourceExhausted}`, never a panic. //! -//! A spawn that lands writes one record, `spawn: pid=N unresolved=N -//! (…ms)`, once the process is in the table and placed; a spawn that is -//! refused writes one, naming why. Nothing is said on the way, by this file or -//! by `crate::elf` under it (`crate::process`'s header). +//! A spawn that lands writes one record, `spawn: pid=N (…ms)`, once +//! the process is in the table and placed; a spawn that is refused writes +//! one, naming why. Nothing is said on the way, by this file or by +//! `crate::elf` under it (`crate::process`'s header). // `warn`, not `deny`: the rest of the kernel is not yet swept for undocumented unsafe blocks. #![warn(clippy::undocumented_unsafe_blocks)] mod start; -mod symbols; mod tls; pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN}; @@ -40,10 +40,7 @@ use crate::sync::Lock; use crate::{scheduler, vfs, UserAddr}; use toyos_abi::handle::Rights; use toyos_abi::syscall::SyscallError; -use toyos_elf::section::SectionTable; -use toyos_elf::sym::{self, SymTab}; -use toyos_elf::rela::{FillLattice, Rules, FILL_GRANULE}; -use toyos_elf::{Layout, RelocError, TlsSegment}; +use toyos_elf::{Layout, TlsSegment}; use toyos_symbols::frame::BuildId; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -51,13 +48,6 @@ const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB /// User virtual address space starts at 1 TB, above any direct-mapped physical RAM. const USER_VM_BASE: u64 = 0x100_0000_0000; -/// The most of a `.gnu.hash` table the loader will read off an executable. -/// -/// A table exceeding this yields no symbol count at all — the loader falls -/// back to the `DT_SYMTAB`/`DT_STRTAB` gap rather than a short count nothing -/// downstream could tell from a real one. -const MAX_GNU_HASH_BYTES: u64 = 64 * 1024; - /// Read a byte range from a file through the page cache. /// /// Returns only the part of the request the file actually holds — callers @@ -93,21 +83,6 @@ pub(crate) fn read_file_range( result } -/// [`read_file_range`] for a length the ELF declared. -/// -/// `None` above [`crate::mm::MAX_HEAP_ALLOC`]: past that the allocation would -/// assert in the heap's page source rather than fail, so this refuses instead -/// of clamping to a table nothing downstream could tell was short. -fn read_elf_table( - backing: &dyn crate::file_backing::FileBacking, - offset: u64, - len: usize, -) -> Option> { - if len > crate::mm::MAX_HEAP_ALLOC { - return None; - } - Some(read_file_range(backing, offset, len)) -} /// Insert one demand-paged region per `PT_LOAD` segment. /// @@ -184,161 +159,6 @@ fn segment_prot(seg: &toyos_elf::Segment) -> crate::mm::policy::Prot { } } -/// Everything the executable's `PT_DYNAMIC` names, resolved to file offsets -/// and read. -struct ExeTables { - /// `DT_NEEDED` values, as offsets into `dynstr`. - needed: Vec, - dynstr: Vec, - dynsym: Vec, - relas: elf::ParsedRelaEntries, -} - -impl ExeTables { - fn symbols(&self) -> SymTab<'_> { - SymTab::new(&self.dynsym, &self.dynstr) - } -} - -/// Turn a `DT_*` vaddr into a file offset, or refuse the binary. -/// -/// `Err` when the vaddr lies below every `PT_LOAD`: there is no file offset for it. -fn file_off(layout: &Layout, path: &str, what: &str, vaddr: u64) -> Result { - match layout.vaddr_to_file_offset(vaddr) { - Some(off) => Ok(off), - None => { - log!("spawn: {}: {} vaddr {:#x} is in or near no PT_LOAD segment", path, what, vaddr); - Err(SyscallError::InvalidArgument) - } - } -} - -/// Read a table whose length the file declared, or refuse the binary. -/// -/// `Err` above one kernel allocation, where the `Vec` would instead panic in -/// the heap's page source. -fn table( - backing: &dyn crate::file_backing::FileBacking, - path: &str, - what: &str, - off: u64, - len: usize, -) -> Result, SyscallError> { - match read_elf_table(backing, off, len) { - Some(v) => Ok(v), - None => { - log!("spawn: {}: {} declares {} bytes, past one kernel allocation", path, what, len); - Err(SyscallError::ResourceExhausted) - } - } -} - -fn read_exe_tables( - backing: &dyn crate::file_backing::FileBacking, - layout: &Layout, - path: &str, -) -> Result { - let (dyn_info, needed) = match layout.dynamic() { - Some(dynamic) => { - let data = table(backing, path, "PT_DYNAMIC", dynamic.file_offset(), dynamic.image().len() as usize)?; - let mut needed = Vec::new(); - needed.reserve_exact(data.len() / toyos_elf::dynamic::ENTRY_SIZE); - needed.extend(toyos_elf::Dynamic::needed(&data)); - (toyos_elf::Dynamic::parse(&data), needed) - } - None => (toyos_elf::Dynamic::default(), Vec::new()), - }; - - let rela_data = match dyn_info.rela { - Some(t) => { - let off = file_off(layout, path, "DT_RELA", t.vaddr)?; - table(backing, path, "DT_RELASZ", off, t.size as usize)? - } - // No PT_DYNAMIC: `.rela.dyn` is found through section headers by shape, not name. - None if layout.dynamic().is_none() => rela_dyn_from_sections(backing, layout, path)?, - None => Vec::new(), - }; - let jmprel_data = match dyn_info.jmprel { - Some(t) => { - let off = file_off(layout, path, "DT_JMPREL", t.vaddr)?; - table(backing, path, "DT_PLTRELSZ", off, t.size as usize)? - } - None => Vec::new(), - }; - let symtab_file_off = match dyn_info.symtab { - Some(vaddr) => Some(file_off(layout, path, "DT_SYMTAB", vaddr)?), - None => None, - }; - let dynstr = match dyn_info.strtab_table() { - Some(t) => { - let off = file_off(layout, path, "DT_STRTAB", t.vaddr)?; - table(backing, path, "DT_STRSZ", off, t.size as usize)? - } - None => Vec::new(), - }; - let dynsym = match (symtab_file_off, exe_sym_count(backing, layout, &dyn_info, path)?) { - (Some(off), n) if n > 0 => table(backing, path, "symbol count", off, n * sym::ENTRY_SIZE)?, - _ => Vec::new(), - }; - - // Parsed like a library's but for the window and the fill page: the - // executable's writes land anywhere in its own image, one demand-fault page - // at a time, so a crossing write is refused, not silently dropped. - let extent = layout.extent(); - let rules = Rules { - extent, - window: (extent.min(), extent.max()), - fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), - tls: layout.tls(), - }; - let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules, SymTab::new(&dynsym, &dynstr)).map_err(|refused| { - log!("spawn: {}: {}", path, refused.as_str()); - refused.error() - })?; - - Ok(ExeTables { needed, dynstr, dynsym, relas }) -} - -/// `.dynsym`'s entry count, by the rule `Dynamic::sym_count` states. -fn exe_sym_count( - backing: &dyn crate::file_backing::FileBacking, - layout: &Layout, - dyn_info: &toyos_elf::Dynamic, - path: &str, -) -> Result { - let gnu_hash = match dyn_info.gnu_hash { - Some(vaddr) => { - let off = file_off(layout, path, "DT_GNU_HASH", vaddr)?; - let len = layout.file_bytes_from(vaddr).unwrap_or(0).min(MAX_GNU_HASH_BYTES) as usize; - Some(read_file_range(backing, off, len)) - } - None => None, - }; - Ok(dyn_info.sym_count(gnu_hash.as_deref()).unwrap_or_else(|gap| { - log!("spawn: {}: .gnu.hash does not describe a symbol count in its {} bytes", path, gnu_hash.as_ref().map_or(0, |t| t.len())); - gap - })) -} - -/// `.rela.dyn` located through section headers, for a file with no `PT_DYNAMIC`. -fn rela_dyn_from_sections( - backing: &dyn crate::file_backing::FileBacking, - layout: &Layout, - path: &str, -) -> Result, SyscallError> { - let Some(sections) = layout.section_headers() else { - return Ok(Vec::new()); - }; - let shdrs = table(backing, path, "e_shnum", sections.file_offset, sections.byte_len())?; - let mut first = |off: u64| { - let head = read_file_range(backing, off, toyos_elf::rela::ENTRY_SIZE); - toyos_elf::RelaTable::new(&head, crate::arch::ELF_MACHINE).get(0) - }; - match SectionTable::new(&shdrs).rela_dyn(&mut first) { - Some((off, size)) => table(backing, path, "SHT_RELA sh_size", off, size as usize), - None => Ok(Vec::new()), - } -} /// Load a program and place its main thread under `parent`, answering its pid /// and what `commit` left its caller holding of it. `commit` builds the @@ -346,9 +166,7 @@ fn rela_dyn_from_sections( /// left to refuse. /// /// `path` is the program, opened here — or, with `image`, the bytes the caller -/// read from it itself, and then nothing opens it and its libraries come from -/// `/system/lib` alone, since the directory it names is no volume of this -/// kernel's. `argv[0]` is only the name the child goes by. +/// read from it itself, and then nothing opens it. `argv[0]` is only the name the child goes by. /// /// `Refusal`, not `-> !`, is the error type: every failure below owns a /// partly built process (address space, stack, kernel stack), and nothing @@ -370,7 +188,6 @@ pub fn spawn( let admission = Admission::ask(parent)?; let t0 = crate::clock::nanos_since_boot(); - let from_image = image.is_some(); let backing: Arc = match image { Some(image) => image, None => { @@ -408,33 +225,15 @@ pub fn spawn( // Where the image's first byte lands: every `ImageOffset` the file's // numbers were parsed into is added to it, and its span fits above it. let image_start = UserAddr::new(USER_VM_BASE); - // `SYS_QUERY_MODULES` answers with the mapped table, so an image without - // one has no true answer to give. + // The program reads the mapped table to relocate itself, and + // `SYS_QUERY_MODULES` answers with it: an image without one has neither. let Some(phdrs) = layout.program_headers() else { log!("spawn: {}: no PT_LOAD maps the program header table", path); return Err(SyscallError::InvalidArgument.into()); }; - let exe = read_exe_tables(backing.as_ref(), &layout, path)?; let t1 = crate::clock::nanos_since_boot(); - // Reserved from the counts, not grown: these are exact upper bounds on `add_u64` calls. - let u64_writes = - exe.relas.relative.len() + exe.relas.glob_dat.len() + exe.relas.tpoff64.len(); - let Some(mut reloc_index) = - elf::RelocationIndex::with_capacity(u64_writes, exe.relas.tpoff32.len()) - else { - log!("spawn: {}: {} relocations do not fit one index", path, u64_writes); - return Err(SyscallError::ResourceExhausted.into()); - }; - for &(r_offset, target) in &exe.relas.relative { - reloc_index.add_u64(r_offset, (image_start + target.get()).raw()); - } - - let t2 = crate::clock::nanos_since_boot(); - let mut loaded_libs = load_needed_libs(&exe, path, from_image)?; - let t_deps = crate::clock::nanos_since_boot(); - // ELF segments are demand-faulted; the address space starts with the clock page alone. let Some(mut space) = crate::mm::paging::AddressSpace::new_user() else { log!("spawn: {}: no user PCID free — too many live address spaces", path); @@ -444,53 +243,6 @@ pub fn spawn( let child_pt: PageTables = Arc::new(Lock::new(space)); insert_elf_regions(&mut child_pt.lock(), &layout, image_start, &backing)?; - // Libraries get user addresses before any relocation is written: RELATIVE - // and GLOB_DAT compute a GOT value as `user_base + addend`/`st_value`. - map_libs(&child_pt, &mut loaded_libs, path)?; - for lib in &loaded_libs.libs { - if lib.user_base.raw() != lib.phys_base { - elf::rebase_relative_relocs(lib); - } - } - - // References no module defines. The file chooses how many, so they are a count in the spawn's record and never a record each. - let mut unresolved = 0u64; - if !loaded_libs.libs.is_empty() { - // A PIE without `--export-dynamic` exports nothing through `.dynsym`; - // read `.symtab` only when that lookup came back empty. - let fallback = exe - .symbols() - .defined() - .next() - .is_none() - .then(|| symbols::read_symtab(backing.as_ref(), &layout)) - .flatten(); - let exe_sym_map = match &fallback { - Some((syms, strs)) => { - symbols::static_map(&SymTab::new(syms, strs), image_start, extent) - } - None => symbols::dynamic_map(&exe.symbols(), image_start, extent), - }; - let exe_sym_map = exe_sym_map.map_err(|refused| { - log!("spawn: {}: {}", path, refused.as_str()); - SyscallError::InvalidArgument - })?; - for lib in &loaded_libs.libs { - unresolved += elf::resolve_lib_bind_relocs(lib, &exe_sym_map, &loaded_libs.libs); - } - - for &(r_offset, r_sym) in &exe.relas.glob_dat { - if r_sym.get() == 0 { - continue; - } - let name = elf::relocated_symbol(exe.symbols(), r_sym).name_in(&exe.dynstr); - match loaded_libs.libs.iter().find_map(|lib| lib.resolve(name)) { - Some(addr) => reloc_index.add_u64(r_offset, addr.raw()), - None => unresolved += 1, - } - } - } - // Mapped eagerly, not demand-paged: every process touches the stack immediately. let stack_pages = match PageAlloc::new(USER_STACK_SIZE) { Some(a) => a, @@ -543,24 +295,11 @@ pub fn spawn( None => None, }; - let Some((tls_modules, tls, next_tls_module_id)) = - tls::build_tls_layout(&loaded_libs.libs, &layout, exe_tls_template.as_ref()) - else { - log!("spawn: {}: the TLS modules do not fit one block", path); + let Some((tls_modules, tls)) = tls::build_tls_layout(&layout, exe_tls_template.as_ref()) else { + log!("spawn: {}: the TLS module does not fit one block", path); return Err(SyscallError::ResourceExhausted.into()); }; - match apply_tls_relocs(&exe, &layout, &loaded_libs.libs, &tls_modules, tls, &mut reloc_index) { - Ok(more) => unresolved += more, - Err(refused) => { - log!("spawn: {}: {}", path, refused.as_str()); - return Err(SyscallError::InvalidArgument.into()); - } - } - - reloc_index.finalize(); - let reloc_index = if reloc_index.len() > 0 { Some(Arc::new(reloc_index)) } else { None }; - let Some((tls_pages, thread_pointer, _)) = tls::TlsBlock::build(&tls_modules, tls).and_then(|b| b.publish(&child_pt)) else { @@ -598,8 +337,6 @@ pub fn spawn( } }; - - let NeededLibs { libs: loaded_libs, paths: lib_paths } = loaded_libs; let pid = admission.pid(); let object = crate::object::process::ProcessObject::new(pid); // The point of no return: every failure above answers the caller with its @@ -613,17 +350,16 @@ pub fn spawn( elf_alloc: exe_tls_template, tls_modules, tls, - next_tls_module_id, + next_tls_module_id: tls::FIRST_DLOPEN_MODULE, dynamic_tls_blocks: alloc::collections::BTreeMap::new(), - loaded_libs, - reloc_index, + loaded_libs: Vec::new(), elf_base: UserAddr::new(base), exe_eh_frame_hdr: layout .eh_frame_hdr() .map_or((0, 0), |r| ((image_start + r.start().get()).raw(), r.len())), exe_vaddr_max: image_end, exe_phdrs: ((image_start + phdrs.image().start().get()).raw(), phdrs.count()), - lib_paths, + lib_paths: Vec::new(), }, mmap_regions: Vec::new(), pipe_maps: Vec::new(), @@ -686,180 +422,12 @@ pub fn spawn( crate::process::debug_hold_marked_spawn(parent, &object); let t3 = crate::clock::nanos_since_boot(); - log!("spawn: {} pid={} unresolved={} (layout={}ms relocs={}ms deps={}ms tls={}ms total={}ms)", - path, pid, unresolved, - (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000, (t_deps - t2) / 1_000_000, - (t_tls - t_deps) / 1_000_000, (t3 - t0) / 1_000_000); + log!("spawn: {} pid={} (layout={}ms tls={}ms total={}ms)", + path, pid, (t1 - t0) / 1_000_000, (t_tls - t1) / 1_000_000, (t3 - t0) / 1_000_000); Ok((pid, held)) } -/// The libraries an executable's `DT_NEEDED` entries name, and the paths they were found at. -struct NeededLibs { - libs: Vec, - paths: Vec, -} - -/// The most distinct libraries one executable may pull in; each is a private -/// 2 MiB window, so a `DT_NEEDED` list naming more is refused rather than loaded. -const MAX_NEEDED_LIBS: usize = 64; - -/// Load each distinct `DT_NEEDED` library, from the executable's own directory -/// first and `/system/lib` second; an image's from `/system/lib` alone. -fn load_needed_libs(exe: &ExeTables, path: &str, from_image: bool) -> Result { - let mut out = NeededLibs { libs: Vec::new(), paths: Vec::new() }; - if exe.needed.is_empty() { - return Ok(out); - } - let exe_dir = match from_image { - true => "/system/lib", - false => path.rsplit_once('/').map(|(dir, _)| dir).unwrap_or(""), - }; - - // Collapse duplicates to the distinct set and bound it: a repeat resolves to - // one library `elf/cache.rs` holds one window for, so it buys no second one. - let mut distinct: Vec<&str> = Vec::new(); - for &name_offset in &exe.needed { - // An offset outside the string table yields an empty name, not a bounds failure. - let lib_name = toyos_elf::cstr(&exe.dynstr, name_offset); - if lib_name.is_empty() || distinct.contains(&lib_name) { - continue; - } - if distinct.len() == MAX_NEEDED_LIBS { - log!("spawn: {}: more than {} distinct DT_NEEDED libraries", path, MAX_NEEDED_LIBS); - return Err(SyscallError::ResourceExhausted); - } - distinct.push(lib_name); - } - out.libs.reserve_exact(distinct.len()); - out.paths.reserve_exact(distinct.len()); - - for lib_name in distinct { - // Which spelling opens is decided before the cache is consulted, and is - // the key from here on: keyed by the exe-dir string it never found, a - // library loaded through the fallback was mapped a second time by any - // later `dlopen("/system/lib/…")`. Fallback only for NotFound — any other error - // would repeat on `/system/lib` too and produce a misleading second log line. - let (so_backing, id, lib_path) = { - let in_exe_dir = alloc::format!("{}/{}", exe_dir, lib_name); - let opened = vfs::lock().open_backing_identified(&in_exe_dir); - match opened { - Ok((b, id)) => (b, id, in_exe_dir), - Err(SyscallError::NotFound) => { - let fallback = alloc::format!("/system/lib/{}", lib_name); - match vfs::lock().open_backing_identified(&fallback) { - Ok((b, id)) => (b, id, fallback), - Err(e) => { - log!("spawn: {}: failed to load {}: {e}", path, lib_name); - return Err(e); - } - } - } - Err(e) => { - log!("spawn: {}: failed to load {}: {e}", path, lib_name); - return Err(e); - } - } - }; - - match elf::try_clone_cached(&lib_path, id) { - Ok(Some(lib)) => { - out.paths.push(lib_path); - out.libs.push(lib); - continue; - } - Err(e) => { - log!( - "spawn: {}: {}: the cached image is stale — the file behind this path changed \ - since it was loaded, and the image cannot be replaced while a process has it \ - mapped", - path, lib_path - ); - return Err(e); - } - Ok(None) => {} - } - - match elf::load_shared_lib(so_backing.as_ref()) { - Ok((lib, rw_offset, rw_size)) => { - out.libs.push(elf::cache_loaded_lib(&lib_path, id, lib, rw_offset, rw_size)?); - out.paths.push(lib_path); - } - Err(e) => { - log!("spawn: {}: failed to load {}: {}", path, lib_name, e); - return Err(SyscallError::NotFound); - } - } - } - Ok(out) -} - -/// Give every library a virtual address in the child and map its pages there. -/// -/// A cached library's read-only pages are the cache's own; only its writable -/// window is private to this process. -fn map_libs( - child_pt: &PageTables, - loaded: &mut NeededLibs, - path: &str, -) -> Result<(), SyscallError> { - for lib in &mut loaded.libs { - let Some(vaddr) = lib.map_into(child_pt) else { - log!("spawn: {}: out of virtual address space for a library", path); - return Err(SyscallError::ResourceExhausted); - }; - lib.user_base = vaddr; - } - Ok(()) -} - -/// Apply the TLS relocations of every startup library, and index the executable's. -/// -/// Libraries' land directly; the executable's go into the relocation index -/// because its pages do not exist yet. Answers how many references name a -/// symbol no module defines. -fn apply_tls_relocs( - exe: &ExeTables, - layout: &Layout, - loaded_libs: &[elf::LoadedLib], - tls_modules: &[elf::TlsModule], - tls: toyos_elf::tls::Static, - reloc_index: &mut elf::RelocationIndex, -) -> Result { - let tls_info = elf::TlsModuleInfo { libs: loaded_libs, modules: tls_modules }; - let mut unresolved = 0; - for lib in loaded_libs { - // Matched by template pointer, unique per lib; a lib without TLS matches nothing. - let module = tls_modules.iter().find(|m| m.template == lib.tls_template); - let base_offset = module.map_or(0, |m| m.base_offset); - // Initial-exec: references to TLS in the static block. - unresolved += elf::apply_tpoff_relocs(lib, base_offset, tls, &tls_info)?; - // General-dynamic: this lib's own TLS, reached through the DTV. - if let Some(m) = module { - unresolved += elf::apply_dtpoff_relocs(lib, &tls_info)?; - unresolved += elf::apply_dtpmod_relocs(lib, m.module_id, &tls_info); - } - } - - let exe_base_offset = tls_modules - .iter() - .find(|m| m.module_id == 1) - .map_or(0, |m| m.base_offset); - let exe_tpoff = - |r| elf::compute_tpoff(r, exe_base_offset, layout.tls(), exe.symbols(), tls, &tls_info); - for &(r_offset, r) in &exe.relas.tpoff64 { - let value = exe_tpoff(r)?; - unresolved += u64::from(value.is_none()); - reloc_index.add_u64(r_offset, value.unwrap_or(0) as u64); - } - for &(r_offset, r) in &exe.relas.tpoff32 { - let value = exe_tpoff(r)?; - unresolved += u64::from(value.is_none()); - reloc_index.add_i32(r_offset, elf::tpoff32_value(value.unwrap_or(0))?); - } - Ok(unresolved) -} - /// The one program the kernel starts. `src/build.rs` puts this binary in every /// image, so a missing one is a bad build, not a different boot. pub const SUPERVISOR_PATH: &str = "/system/bin/supervisor"; diff --git a/kernel/src/loader/symbols.rs b/kernel/src/loader/symbols.rs deleted file mode 100644 index 1205453d9bd..00000000000 --- a/kernel/src/loader/symbols.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! The executable's exported symbols, for binding a library's `GLOB_DAT` and -//! `JUMP_SLOT` slots against. -//! -//! Nothing here owns a string: the maps borrow the caller's tables and die -//! with the spawn that built them. - -use alloc::vec::Vec; -use alloc::collections::BTreeMap; - -use super::read_elf_table; -use crate::file_backing::FileBacking; -use crate::UserAddr; -use toyos_elf::section::{SectionTable, SHT_SYMTAB}; -use toyos_elf::sym::{SymTab, STT_TLS}; -use toyos_elf::{Error, Extent, Layout}; - -/// Every defined, named symbol in `.dynsym`, at its runtime address: no -/// binding filter, since being in `.dynsym` and defined is the export. -pub fn dynamic_map<'a>( - symbols: &SymTab<'a>, - image_start: UserAddr, - extent: Extent, -) -> Result, Error> { - map(symbols, image_start, extent, |_| true) -} - -/// The same over `.symtab`, which also holds locals no other module may -/// bind to. -pub fn static_map<'a>( - symbols: &SymTab<'a>, - image_start: UserAddr, - extent: Extent, -) -> Result, Error> { - map(symbols, image_start, extent, |s: &toyos_elf::Sym| s.is_exported()) -} - -/// A thread-local symbol has no address to bind a slot to, so it is not in -/// the map; any other one whose value is outside the image refuses it. -fn map<'a>( - symbols: &SymTab<'a>, - image_start: UserAddr, - extent: Extent, - keep: impl Fn(&toyos_elf::Sym) -> bool, -) -> Result, Error> { - let mut map = BTreeMap::new(); - for (i, sym) in symbols.defined() { - let name = symbols.name(i); - if name.is_empty() || !keep(&sym) || sym.kind() == STT_TLS { - continue; - } - let at = sym.address(extent).ok_or(Error::SymbolOutsideImage)?; - map.insert(name, image_start + at.get()); - } - Ok(map) -} - -/// `.symtab` and its `.strtab`, read whole — the fallback for a PIE that -/// exports nothing through `.dynsym`. `None` is an executable with no such -/// tables, or tables past one kernel allocation: it then exports nothing, and -/// what a library wanted of it is in the spawn's `unresolved=`. -pub fn read_symtab(backing: &dyn FileBacking, layout: &Layout) -> Option<(Vec, Vec)> { - let table = layout.section_headers()?; - let shdrs = super::read_file_range(backing, table.file_offset, table.byte_len()); - let (syms, strs) = SectionTable::new(&shdrs).symbols(SHT_SYMTAB)?; - Some(( - read_elf_table(backing, syms.offset, syms.size as usize)?, - read_elf_table(backing, strs.offset, strs.size as usize)?, - )) -} diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index c459287c2a9..942323fe99d 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -159,52 +159,28 @@ unsafe fn rebase(frames: &Unpublished, tp_offset: usize, at: UserAddr) { } } -/// One combined block for every startup module; `None` when they do not fit, since a missing module would mean relocations resolving against a block that is not there. -/// The executable's module goes where its linker resolved its own accesses: next to the thread -/// pointer, last in variant II and first in variant I. +/// The id the first `dlopen`ed module with TLS takes: the executable's is 1. +pub const FIRST_DLOPEN_MODULE: u64 = 2; + +/// The static block's one module, the executable's, and its layout; no module for an executable +/// without TLS, and `None` when it does not fit. The module goes where its linker resolved its own +/// accesses: ending at the thread pointer in variant II, starting at it in variant I. pub fn build_tls_layout( - loaded_libs: &[crate::elf::LoadedLib], layout: &Layout, exe_tls_template: Option<&OwnedAlloc>, -) -> Option<(alloc::vec::Vec, Static, u64)> { - // (template, memsz, placed bytes, align, module id). Module id 1 is the executable's; - // libraries start at 2. - let exe = match layout.tls().and_then(TlsSegment::occupied) { - None => None, - Some(tls) => { - let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); - // Variant II's executable ends at the thread pointer at its extent, not its `memsz`: - // its linker fixed every local-exec offset against the rounded size. - let placed = match VARIANT { - Variant::II => toyos_elf::tls::exe_extent(memsz, align)?, - Variant::I => memsz, - }; - Some((exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)), memsz, placed, align, 1)) - } - }; - let with_tls = || { - loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, lib.tls()?.occupied()?))) +) -> Option<(alloc::vec::Vec, Static)> { + let Some(tls) = layout.tls().and_then(TlsSegment::occupied) else { + return Some((alloc::vec::Vec::new(), Static::new(VARIANT, 0, 1, 1)?)); }; - let libs = with_tls().zip(2u64..).map(|((template, tls), id)| { - let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); - (template, memsz, memsz, align, id) - }); - let next_module_id = 2 + with_tls().count() as u64; - let order: alloc::vec::Vec<_> = match VARIANT { - Variant::II => libs.chain(exe).collect(), - Variant::I => exe.into_iter().chain(libs).collect(), + let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); + // Variant II's executable ends at the thread pointer at its extent, not its `memsz`: + // its linker fixed every local-exec offset against the rounded size. + let placed = match VARIANT { + Variant::II => toyos_elf::tls::exe_extent(memsz, align)?, + Variant::I => memsz, }; - - let mut modules = alloc::vec::Vec::with_capacity(order.len()); - let mut cursor = 0usize; - let mut max_align = 1usize; - for (template, memsz, placed, align, module_id) in order.iter().copied() { - let (base_offset, next) = toyos_elf::tls::place_module(cursor, placed, align)?; - cursor = next; - max_align = max_align.max(align); - modules.push(TlsModule { template, memsz, base_offset, module_id, is_static: true }); - } - let first_align = order.first().map_or(1, |&(_, _, _, align, _)| align); - let tls = Static::new(VARIANT, cursor, max_align, first_align)?; - Some((modules, tls, next_module_id)) + let (base_offset, end) = toyos_elf::tls::place_module(0, placed, align)?; + let template = exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)); + let module = TlsModule { template, memsz, base_offset, module_id: 1, is_static: true }; + Some((alloc::vec![module], Static::new(VARIANT, end, align.max(1), align)?)) } diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 0129cec1200..ea26c95d502 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -461,8 +461,7 @@ pub struct PageFaultRecord { pub fault_addr: u64, pub page_elf_offset: u64, pub block_idx: u32, - pub reloc_count: u16, - // bit 0 writable, 1 has_relocs, 2 anonymous, 3 beyond_extent, 4 executable; 0 and 4 come from one `Prot` and are never both set (W^X). + // bit 0 writable, 2 anonymous, 3 beyond_extent, 4 executable; 0 and 4 come from one `Prot` and are never both set (W^X). pub flags: u16, pub duration_us: u16, // microseconds spent handling this fault } @@ -478,8 +477,7 @@ impl PageFaultTrace { pub fn new() -> Self { Self { entries: [PageFaultRecord { - fault_addr: 0, page_elf_offset: 0, block_idx: 0, - reloc_count: 0, flags: 0, duration_us: 0, + fault_addr: 0, page_elf_offset: 0, block_idx: 0, flags: 0, duration_us: 0, }; 32], write_pos: 0, total: 0, @@ -514,8 +512,6 @@ pub struct ElfInfo { pub dynamic_tls_blocks: alloc::collections::BTreeMap<(Tid, u64), MappedPages>, /// Dynamically loaded shared libraries (indexed by dlopen handle). pub loaded_libs: Vec, - /// RELATIVE relocation index for demand-paged ELF (applied per-page on fault). - pub reloc_index: Option>, pub elf_base: UserAddr, /// The executable's `.eh_frame_hdr` as (address, size), `(0, 0)` without one. pub exe_eh_frame_hdr: (u64, u64), @@ -540,7 +536,6 @@ impl ElfInfo { next_tls_module_id: 1, dynamic_tls_blocks: alloc::collections::BTreeMap::new(), loaded_libs: Vec::new(), - reloc_index: None, elf_base: UserAddr::new(0), exe_eh_frame_hdr: (0, 0), exe_vaddr_max: 0, @@ -1107,7 +1102,6 @@ fn teardown_resources( data.mmap_regions.clear(); data.pipe_maps.clear(); data.demand_pages.clear(); - data.elf.reloc_index = None; Consumed { peak_memory: data.peak_memory, @@ -1472,14 +1466,13 @@ pub fn handle_page_fault(fault_addr: u64, _error_code: u64) -> bool { let mut data = data_arc.lock(); - let reloc_index = data.elf.reloc_index.clone(); let elf_base = data.elf.elf_base.raw(); let page_alloc = match PageAlloc::new(page_2m as usize) { Some(a) => a, None => return false, }; - // A bounds-checked window, not a bare `*mut u8`: both fills below are the kernel's own arithmetic, unchecked until something bounds it. + // A bounds-checked window, not a bare `*mut u8`: the fill below is the kernel's own arithmetic, unchecked until something bounds it. let page = page_alloc.window(); // Multiple segments (e.g. .text/.rodata) can share a 2MB range. @@ -1519,23 +1512,6 @@ pub fn handle_page_fault(fault_addr: u64, _error_code: u64) -> bool { } } - - let mut total_relocs = 0u16; - if let Some(ref ri) = reloc_index { - let mut offset = 0u64; - while offset < page_2m { - let page_elf_offset = (region_start + offset).wrapping_sub(elf_base); - if ri.has_relocs_in_page(page_elf_offset) { - // `subslice` bounds `offset + 4096` against the frame's own size; `apply_to_page` bounds every write against the window it's handed. - total_relocs = total_relocs.saturating_add( - ri.apply_to_page(page_elf_offset, page.subslice(offset as usize, 4096)) as u16, - ); - } - offset += 4096; - } - } - - // No invalidation: the fault means the PDE was absent, so nothing is cached from it (`map_window` derives that from the entry it replaces). // Claimed under the install lock, not the decide-to-fill one: a racing sibling can reach here too and lose the install race; the loser's fill is thrown away rather than both being installed, since no shootdown is issued from a fault. let installed = addr_space.lock().map_window_if_absent( @@ -1578,7 +1554,6 @@ pub fn handle_page_fault(fault_addr: u64, _error_code: u64) -> bool { fault_addr, page_elf_offset: region_start.wrapping_sub(elf_base), block_idx: (region_start / PAGE_2M) as u32, - reloc_count: total_relocs, flags: match fault_prot { Prot::Read => 0, Prot::ReadWrite => 1, @@ -1618,15 +1593,13 @@ pub fn dump_crash_diagnostics(fault_addr: u64, rip: u64) { if rec.fault_addr == 0 { continue; } let mut flag_str = [b' '; 5]; if rec.flags & 1 != 0 { flag_str[0] = b'W'; } // writable - if rec.flags & 2 != 0 { flag_str[1] = b'R'; } // has_relocs if rec.flags & 4 != 0 { flag_str[2] = b'A'; } // anonymous if rec.flags & 8 != 0 { flag_str[3] = b'Z'; } // beyond extent (zero) // Never beside `W` (one `Prot`'s variants); both set means lost W^X. if rec.flags & 16 != 0 { flag_str[4] = b'X'; } // executable let flags = core::str::from_utf8(&flag_str).unwrap_or("????"); - log!(" fault={:#x} elf_off={:#x} blk={} relocs={} {}us [{}]", - rec.fault_addr, rec.page_elf_offset, rec.block_idx, - rec.reloc_count, rec.duration_us, flags); + log!(" fault={:#x} elf_off={:#x} blk={} {}us [{}]", + rec.fault_addr, rec.page_elf_offset, rec.block_idx, rec.duration_us, flags); } } diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index b1a603d1681..d06fc276644 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -42,15 +42,19 @@ pub(crate) static ARGV: AtomicUsize = AtomicUsize::new(0); // *const *const u8 a // Stack layout at entry (set up by kernel), with the stack pointer 16-byte aligned: // [sp] = argc // [sp+8] = argv[0], argv[1], ..., NULL +// The image relocates itself first, from its own header's address. #[cfg(target_arch = "x86_64")] #[unsafe(no_mangle)] #[unsafe(naked)] unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( + "lea rdi, [rip + __ehdr_start]", + "call {relocate}", "mov rdi, [rsp]", "lea rsi, [rsp + 8]", "call {start_rust}", "ud2", + relocate = sym toyos::relocate::relocate_self, start_rust = sym start_rust, ); } @@ -60,6 +64,9 @@ unsafe extern "C" fn _start() -> ! { #[unsafe(naked)] unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( + "adrp x0, __ehdr_start", + "add x0, x0, :lo12:__ehdr_start", + "bl {relocate}", "ldr x0, [sp]", "add x1, sp, #8", // The outermost frame record: a backtrace ends here. @@ -67,6 +74,7 @@ unsafe extern "C" fn _start() -> ! { "mov x30, xzr", "bl {start_rust}", "brk #0x1", + relocate = sym toyos::relocate::relocate_self, start_rust = sym start_rust, ); } diff --git a/tests/checks.rs b/tests/checks.rs index f48dc76f464..ba954951f80 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -1120,25 +1120,25 @@ mod checks { assert!(xhci_xecp(&silent).is_err()); } - /// `dlopen_dedup` reads `test_rs_std_tls` by path. + /// A member that reads another test binary by path. #[test] fn a_shared_chunk_stages_every_binary_its_members_name() { - let source = "const NEEDS_A_LIB: &str = \"/system/bin/test_rs_std_tls\";\n"; - let bins: Vec<(String, Vec)> = ["dlopen_dedup", "std_tls", "fs_large_file", "libfoo.so"] + let source = "const CHILD: &str = \"/system/bin/test_rs_child\";\n"; + let bins: Vec<(String, Vec)> = ["parent", "child", "fs_large_file", "libfoo.so"] .iter() .map(|name| ((*name).to_string(), Vec::new())) .collect(); - let jobs = vec!["test_rs_dlopen_dedup".to_string()]; + let jobs = vec!["test_rs_parent".to_string()]; let staged = |text: &str| -> Vec { metal::reached(text, &jobs, &bins).into_iter().map(|(path, _)| path).collect() }; assert_eq!( - staged(&format!("test_rs_dlopen_dedup\n{source}")), - ["bin/test_rs_std_tls", "lib/libfoo.so"] + staged(&format!("test_rs_parent\n{source}")), + ["bin/test_rs_child", "lib/libfoo.so"] ); // A longer name is another binary's. - let longer = source.replace("test_rs_std_tls", "test_rs_std_tls_dlopen"); - assert_eq!(staged(&format!("test_rs_dlopen_dedup\n{longer}")), ["lib/libfoo.so"]); + let longer = source.replace("test_rs_child", "test_rs_child_two"); + assert_eq!(staged(&format!("test_rs_parent\n{longer}")), ["lib/libfoo.so"]); } /// `03_struct`'s output as `ccheck` prints it: the expectation the corpus diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index 2b950d83cca..72c4a6f4482 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -391,7 +391,7 @@ fn member(job: &str, adds_ms: u64) -> metal::Member { pub fn a_failing_shared_member_fails_itself_alone() { let dir = toyos_tmpdir::TempDir::new("metal-readbacks"); let root = toyos_tmpdir::TempDir::new("metal-records"); - let jobs = ["test_rs_std_tls", "test_rs_fs_large_file"]; + let jobs = ["test_rs_std_tls_multi_crate", "test_rs_fs_large_file"]; let exits = |code: i32| { format!( "{BOOTED}[2026-09-29 18:22:41 2.310 cpu3 kernel] exit: {} pid=12 code=0 cpu=4ms\n\ diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index 1dbf8df013d..71f9018180d 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -2,16 +2,20 @@ //! kernel's own address-space rules forbid. //! //! `abuse_elf_segments` covers the sizes a program header declares. This one -//! covers what the loader *derives* from them: the file offset a `DT_*` vaddr -//! maps to, the address a segment lands at once the image is rebased to -//! `USER_VM_BASE`, the TLS block's layout, and every table whose length the -//! file gets to name. Each case is a kernel panic or a forbidden mapping -//! before the fix; each must be an error return after it, with the kernel -//! intact. +//! covers what the loader *derives* from them: the address a segment lands at +//! once the image is rebased to `USER_VM_BASE`, the TLS block's layout, and +//! what `dlopen` reads of a library's tables. Each case is a kernel panic or +//! a forbidden mapping before the fix; each must be an error return after it, +//! with the kernel intact. +//! +//! An executable's dynamic section is the program's own +//! (`toyos::relocate`): the spawn cases crafted in one are answered either way, +//! the kernel lives, and a child the kernel starts dies ([`dies`]). use std::fs; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; +use toyos_abi::RawHandle; /// Where the child starts: `SpawnArgs` names a working directory or the spawn is refused. const CWD: &str = "/"; @@ -226,7 +230,7 @@ fn write_file_in(dir: &str, name: &str, bytes: &[u8]) -> String { path } -fn spawn_path(path: &str) -> Result { +fn spawn_path(path: &str) -> Result { let argv = format!("{path}\0"); unsafe { syscall::spawn(&SpawnArgs { @@ -249,18 +253,17 @@ fn spawn_path(path: &str) -> Result { place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } - .map(|pid| pid.0 as u64) } -fn spawn_result(name: &str, bytes: &[u8]) -> Result { +fn spawn_result(name: &str, bytes: &[u8]) -> Result { spawn_path(&write_file(name, bytes)) } /// Spawn must refuse this file. A success is as much a failure as a panic: /// every case here describes a process the kernel cannot safely build. -fn refused(name: &str, outcome: Result) { +fn refused(name: &str, outcome: Result) { match outcome { - Ok(pid) => panic!("{name}: spawn returned pid {pid} for an ELF that must be refused"), + Ok(child) => panic!("{name}: spawn started {child:?} for an ELF that must be refused"), Err(e) => assert!( matches!(e, SyscallError::InvalidArgument | SyscallError::ResourceExhausted), "{name}: spawn gave {e:?}", @@ -280,7 +283,7 @@ fn image_object(bytes: &[u8]) -> toyos::shm::SharedMemory { /// The same bytes handed to the kernel by handle, as a spawn from a file /// server's volume hands them: the image route to the same loader. -fn spawn_image(path: &str, bytes: &[u8]) -> Result { +fn spawn_image(path: &str, bytes: &[u8]) -> Result { let argv = format!("{path}\0"); let object = image_object(bytes); unsafe { @@ -304,7 +307,6 @@ fn spawn_image(path: &str, bytes: &[u8]) -> Result { place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } - .map(|pid| pid.0 as u64) } /// Refused whether the kernel opens the file or is handed its bytes. @@ -313,16 +315,29 @@ fn spawn_refused(name: &str, bytes: &[u8]) { refused(&format!("{name} as an image"), spawn_image(&format!("{DIR}/{name}"), bytes)); } -/// Refused where the kernel opens the file, whose `DT_NEEDED` library is -/// written beside it. The image route's answer is the kernel's *first* -/// refusal on that route: `image_expected` is `NotFound` when nothing rejects -/// the file before `DT_NEEDED` is walked from `/system/lib` alone and finds -/// nothing there, `InvalidArgument` when a table check upstream of that walk -/// refuses first. -fn spawn_refused_beside_its_library(name: &str, bytes: &[u8], image_expected: SyscallError) { - refused(name, spawn_result(name, bytes)); - let image = spawn_image(&format!("{DIR}/{name}"), bytes); - assert_eq!(image, Err(image_expected), "{name} as an image"); +/// The kernel answers this spawn and lives, and a child it started dies. These +/// files are crafted in a dynamic section the kernel no longer reads: a +/// program relocates itself (`toyos::relocate`), and each of these enters at +/// bytes that are no program, so the child dies before it reads one. +fn dies(name: &str, outcome: Result) { + match outcome { + Ok(child) => { + // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. + let child = unsafe { toyos::process::Process::from_raw(child) }; + let code = child.wait().unwrap_or_else(|e| panic!("{name}: wait for the child: {e:?}")); + assert_ne!(code, 0, "{name}: a child entering at bytes that are no program exited 0"); + } + Err(e) => assert!( + matches!(e, SyscallError::InvalidArgument | SyscallError::ResourceExhausted), + "{name}: spawn gave {e:?}", + ), + } +} + +/// [`dies`], whether the kernel opens the file or is handed its bytes. +fn spawn_dies(name: &str, bytes: &[u8]) { + dies(name, spawn_result(name, bytes)); + dies(&format!("{name} as an image"), spawn_image(&format!("{DIR}/{name}"), bytes)); } /// Load it and throw it away. These cases are about a *walk* the loader does, @@ -357,10 +372,9 @@ fn main() { fs::create_dir_all(DIR).expect("create /tmp/abuse_loader_exe"); fs::create_dir_all(BIG_DIR).expect("create /tmp/abuse_loader"); - // 1. A DT_* vaddr below every PT_LOAD. `vaddr_to_file_offset` searched for - // the nearest segment at or below it and panicked outright when there - // was none — the one entry in this class with no failure path at all. - spawn_refused( + // 1. A DT_* vaddr below every PT_LOAD, which `vaddr_to_file_offset` panicked + // on outright when the kernel read an executable's tables. + spawn_dies( "rela_below_image", &Elf::new(0x4000) .ph(Phdr::load(0, 0x1000, 0x3000, 0x3000, PF_R | PF_W)) @@ -449,15 +463,10 @@ fn main() { .build(), ); - // 8. A section header table of 2.4 MiB, in a file big enough to hold it. - // The loader reads a declared table into one `Vec`, and dlmalloc serves - // an allocation that size by asking its page source for a 4 MiB - // granule — `mm/alloc.rs`'s assert, in syscall context. - // - // The file has to be real: `read_file_range` clamps the declared length - // to what the file holds, so a 16 KiB file with a 2.4 MiB `e_shnum` - // reaches a 16 KiB allocation and proves nothing. - refused( + // 8. A section header table of 2.4 MiB, in a file big enough to hold it, + // which the kernel read into one `Vec` past `mm/alloc.rs`'s assert when + // it looked for an executable's `.rela.dyn` and `.symtab` there. + dies( "shnum_past_heap", spawn_path(&write_file_in( BIG_DIR, @@ -468,7 +477,7 @@ fn main() { // 9. Same ceiling, reached through DT_STRSZ instead — a size no section // header has to agree with, in a file that can back it. - refused( + dies( "strsz_past_heap", spawn_path(&write_file_in( BIG_DIR, @@ -499,23 +508,9 @@ fn main() { .build(), ); - // 12. Two relocation tables the ceiling *accepts*, whose derived index - // does not fit. This is the gap in cases 8 and 9: they prove an input - // over the ceiling is refused, and say nothing about an input under it - // from which the kernel derives something over. - // - // `DT_RELASZ` and `DT_PLTRELSZ` are bounded separately at - // MAX_HEAP_ALLOC and both feed one `RelocationIndex`, so two tables of - // 87,210 entries each are 174,420 entries of 16 bytes = 2.7 MiB in a - // single Vec. Under the old growth-by-doubling that overshot to 4 MiB - // on the push. - // - // This case is also the actuator for the allocator-lock defect: - // the >2 MiB assert fires inside - // `KernelAllocator::alloc` *while it holds the dlmalloc lock*, so the - // recovered CPU's next allocation spins on a lock the dead thread - // still owns. Nothing else in the suite stages that, which is part of - // why it has stayed open -- do not build a second one. + // 12. Two relocation tables the ceiling accepted, whose derived index did + // not fit: `DT_RELASZ` and `DT_PLTRELSZ` were bounded apart and both + // fed one index the kernel built at spawn, 2.7 MiB in a single `Vec`. { const N: usize = 87_210; // MAX_HEAP_ALLOC / 24, the most either table can declare const SZ: usize = N * 24; @@ -536,17 +531,16 @@ fn main() { bytes[at..at + 8].copy_from_slice(&((i as u64) * 8).to_le_bytes()); bytes[at + 8..at + 16].copy_from_slice(&R_X86_64_RELATIVE.to_le_bytes()); } - refused( + dies( "rela_index_past_heap", spawn_path(&write_file_in(BIG_DIR, "rela_index_past_heap", &bytes)), ); } - // 12b. More distinct DT_NEEDED names than the loader will load: each is one - // private window, so above the cap it is refused before any open — where - // the unbounded loader reached an open of a missing name and got NotFound. + // 12b. More distinct DT_NEEDED names than the kernel loaded at spawn, each + // one private window. { - const N: usize = 65; // MAX_NEEDED_LIBS (64) + 1, all distinct + const N: usize = 65; // one past the kernel's 64, all distinct const STRTAB: usize = 0x2000; let mut tags: Vec<(i64, u64)> = vec![(DT_STRTAB, STRTAB as u64), (DT_STRSZ, (N * 4) as u64)]; @@ -559,7 +553,7 @@ fn main() { elf = elf.poke(STRTAB + off, &[b'a' + (i % 26) as u8, b'A' + (i / 26) as u8]); tags.push((DT_NEEDED, off as u64)); } - spawn_refused("too_many_needed_libs", &elf.dynamic(0x1000, &tags).build()); + spawn_dies("too_many_needed_libs", &elf.dynamic(0x1000, &tags).build()); } // 11. A DTPMOD64 relocation naming a TLS symbol no loaded module defines. @@ -568,8 +562,8 @@ fn main() { dlopen_survives("dtpmod_unresolved.so", &so_with_dtpmod()); // 13. A RELATIVE write beginning in a page's last seven bytes: r_offset - // 0x1FFE + 8 crosses 0x2000. Dropped by the per-page applier; refused now. - spawn_refused( + // 0x1FFE + 8 crosses 0x2000, which the kernel's per-page applier dropped. + spawn_dies( "reloc_straddles_fill_page", &Elf::new(0x4000) .ph(Phdr::load(0, 0, 0x4000, 0x4000, PF_R | PF_W)) @@ -606,7 +600,7 @@ fn main() { f13_cross_module_addend_is_kept(); // 19. A relocation naming a symbol the executable's short-read `.dynsym` does - // not hold is refused by name. + // not hold, which the kernel refused by name while it read one. globdat_past_short_dynsym(); // 20. The apply-time TLS refusals, each named by its reason in the log. @@ -748,36 +742,33 @@ fn so_with_non_zero_vaddr_min() -> Vec { /// Every file-chosen value the loader turns into an address or a thread-pointer /// offset, set to one no image holds: a `RELATIVE` addend, a TPOFF addend, a -/// symbol's `st_value`, `DT_INIT_ARRAY`. Each is refused; the kernel living -/// through them is what the checks at the end of `main` assert. +/// symbol's `st_value`, `DT_INIT_ARRAY`. Each is refused where the kernel reads +/// it, a library's, and dies where it no longer does, an executable's; the +/// kernel living through them is what the checks at the end of `main` assert. fn values_are_bounded_by_the_image() { // RELATIVE `B + A` with `A = i64::MAX`: `USER_VM_BASE + A` overflowed. - spawn_refused( + spawn_dies( "relative_addend_past_image", &exe_with(&[], &[(0x2000, R_X86_64_RELATIVE, i64::MAX)], None).build(), ); // TPOFF64 against the executable's own 16-byte PT_TLS, `A = i64::MIN`: // `tls::Static::tpoff` subtracted with overflow. - spawn_refused( + spawn_dies( "tpoff_addend_past_tls", &exe_with(&[], &[(0x2000, R_X86_64_TPOFF64, i64::MIN)], Some(16)).build(), ); // An export at `0xFFFF_FFFF_FFFF_F000` in an executable that needs a - // library: the loader builds a map of the exe's exports for the library's - // slots to bind against, and that map added the value to the load base. The - // dependency is written beside the exe, where `DT_NEEDED` resolves first. + // library: the kernel built a map of the exe's exports for the library's + // slots to bind against, and that map added the value to the load base. { let dep = "export_dep.so"; - write_file(dep, &so_with(&[], &[], None)); // `.dynstr` at 0x1800 holds "far\0\0"; the export names the first. let name_at = 1 + FAR.len() as u64 + 1; let exe = exe_with(&[(DT_NEEDED, name_at)], &[], None) .sym(0x1418, 1, (STB_GLOBAL << 4) | STT_FUNC, 1, FAR_VALUE) .poke(0x1801, FAR.as_bytes()) .poke(0x1800 + name_at as usize, dep.as_bytes()); - // The export map is built after `load_needed_libs` (`kernel/src/loader/mod.rs`), so the - // image route never reaches it: the dependency missing from `/system/lib` answers first. - spawn_refused_beside_its_library("export_past_image", &exe.build(), SyscallError::NotFound); + spawn_dies("export_past_image", &exe.build()); } dlopen_refused("so_relative_addend_past_image.so", &so_with(&[], &[(0x1400, 0, R_X86_64_RELATIVE, i64::MAX)], None)); @@ -915,14 +906,15 @@ fn tls_defs_so(name: &[u8; 4], memsz: u64) -> Vec { /// The apply-time TLS refusals, which no `r_sym == 0` case reaches because /// `rela::parse` refuses those before either apply pass runs. Each refuses a /// *resolved* `S + A` outside the defining module's `PT_TLS`, named -/// `TLS_OUTSIDE_SEGMENT` beside the file in the kernel log the harness checks. +/// `TLS_OUTSIDE_SEGMENT` beside the file in the kernel log; the executable's +/// spawn dies instead, its tables its own. fn tls_apply_time_refusals_are_reached() { // dlopen, the module's own symbol: `apply_tpoff_relocs` refuses, and the // mapping guard takes the library back down. dlopen_refused("tls_apply_refs.so", &so_tls_ref_past_segment()); - // spawn, the executable's own symbol: `apply_tls_relocs` refuses. - spawn_refused("tls_apply_spawn", &exe_tls_ref_past_segment()); + // spawn, the executable's own symbol, which `apply_tls_relocs` refused. + spawn_dies("tls_apply_spawn", &exe_tls_ref_past_segment()); // dlopen, another module's symbol: `S + A` (8 + 0x140) leaves the defining // module's 0x20-byte `PT_TLS`. Named apart from f13's `xtls`, which stays @@ -936,15 +928,12 @@ fn tls_apply_time_refusals_are_reached() { // `S + A - tp` leaves an `i64`. const PAST_I64: u64 = 0x8000_0000_0000_0010; let dep = "tpoff_overflow_dep.so"; - write_file(dep, &tls_defs_so(b"wtls", PAST_I64)); // `.dynstr` at 0x1800 holds "wtls\0\0". let exe = exe_with(&[(DT_NEEDED, 6)], &[(0x2000, (1u64 << 32) | R_X86_64_TPOFF64, i64::MAX)], None) .sym(0x1418, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) .poke(0x1801, b"wtls\0") .poke(0x1806, dep.as_bytes()); - // `apply_tls_relocs` (`kernel/src/loader/mod.rs`) runs after `load_needed_libs`, so the image - // route never reaches it: the dependency missing from `/system/lib` answers first. - spawn_refused_beside_its_library("tpoff_overflow_spawn", &exe.build(), SyscallError::NotFound); + spawn_dies("tpoff_overflow_spawn", &exe.build()); let defs = write_file("tpoff_overflow_defs.so", &tls_defs_so(b"vtls", PAST_I64)); let lib_defs = unsafe { libloading::Library::new(&defs) }.expect("dlopen tpoff_overflow_defs.so"); @@ -957,7 +946,6 @@ fn tls_apply_time_refusals_are_reached() { /// `GLOB_DAT` naming symbol 469, the first index past the whole entries. fn globdat_past_short_dynsym() { let dep = "globdat_dep.so"; - write_file(dep, &so_with(&[], &[], None)); // nbuckets 1, symoffset 1000, bloom_size 1, bloom_shift 0, the bloom word // and bucket 0: a bucket below `symoffset` makes `symoffset` the count. let mut gnu_hash = Vec::new(); @@ -971,9 +959,7 @@ fn globdat_past_short_dynsym() { ) .poke(0x1801, dep.as_bytes()) .poke(0x3000, &gnu_hash); - // `rela::parse` (`toyos-elf/src/rela.rs`) bounds `r_sym` against the exe's own `.dynsym` - // before `load_needed_libs` ever runs. - spawn_refused_beside_its_library("globdat_past_dynsym", &exe.build(), SyscallError::InvalidArgument); + spawn_dies("globdat_past_dynsym", &exe.build()); } /// A shared object defining its own `xtls` (`STT_TLS`, offset 8) in a 0x20-byte diff --git a/tests/toyos-rust-tests/src/bin/dlopen_dedup.rs b/tests/toyos-rust-tests/src/bin/dlopen_dedup.rs index 672ea2b6d05..04a0bc98658 100644 --- a/tests/toyos-rust-tests/src/bin/dlopen_dedup.rs +++ b/tests/toyos-rust-tests/src/bin/dlopen_dedup.rs @@ -15,47 +15,13 @@ const LIB: &[u8] = b"/system/lib/libtls_dlopen_lib.so"; const OTHER_LIB: &[u8] = b"/system/lib/libtls_multi_crate.so"; const LOADS: usize = 64; -/// A directory only this test spawns from, so the `DT_NEEDED` fallback caches -/// under a spelling nothing else can produce; `check_dlopen_dedup` in -/// `tests/toyos.rs` holds the verdict. The binary beside it `DT_NEEDED`s a -/// library that is in `/system/lib` and not there. -const FROM: &str = "/tmp/dlopen-dedup"; -const NEEDS_A_LIB: &str = "/system/bin/test_rs_std_tls"; -const BY_PATH: &[u8] = b"/system/lib/libtls_lib.so"; - fn main() { let handles = handle_identity(); object_identity(handles[0], handles[LOADS - 1]); one_name_under_contention(); - one_library_under_two_spellings(); println!("all dlopen dedup checks passed"); } -/// A library reached through the `/system/lib` fallback and then by its own path is one -/// module, not two. **The verdict is the kernel's and this arm only stages it:** -/// a guest cannot count physical images, so it puts the fallback's spelling -/// somewhere unique — a `DT_NEEDED`-carrying binary in [`FROM`], which holds no -/// library — and a loader caching under the directory it searched logs that path. -fn one_library_under_two_spellings() { - let copy = format!("{FROM}/needs-a-lib"); - std::fs::create_dir_all(FROM).unwrap_or_else(|e| panic!("make {FROM}: {e}")); - let bytes = std::fs::read(NEEDS_A_LIB).unwrap_or_else(|e| panic!("read {NEEDS_A_LIB}: {e}")); - std::fs::write(©, &bytes).unwrap_or_else(|e| panic!("write {copy}: {e}")); - - let status = std::process::Command::new(©) - .stdout(std::process::Stdio::null()) - .status() - .unwrap_or_else(|e| panic!("spawn {copy}: {e}")); - assert_eq!( - status.code(), - Some(0), - "the copy in {FROM} did not run, so its DT_NEEDED never took the /system/lib fallback", - ); - - syscall::dl_open(BY_PATH).expect("the same library by its own path"); - println!(" PASS: a library came through the fallback and then by its own path"); -} - /// The mechanism: repeated loads of one name are one handle. fn handle_identity() -> Vec { let handles: Vec = (0..LOADS) diff --git a/tests/toyos-rust-tests/src/bin/std_tls.rs b/tests/toyos-rust-tests/src/bin/std_tls.rs deleted file mode 100644 index 52485cdfd14..00000000000 --- a/tests/toyos-rust-tests/src/bin/std_tls.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::cell::Cell; - -// Functions from tls-lib (loaded as shared library via DT_NEEDED) -#[link(name = "tls_lib")] -extern "C" { - fn tls_increment() -> u64; - fn tls_get_counter() -> u64; - fn tls_get_label() -> u64; - fn tls_set_label(val: u64); -} - -// 64-byte aligned by its own type, so the exe's `PT_TLS` declares at least that -// whatever the linker would pick, and `LOCAL_VALUE`'s address is on 64 exactly -// when the loader placed the exe's module on its declared alignment. -#[repr(align(64))] -struct Aligned(Cell); - -thread_local! { - static LOCAL_VALUE: Aligned = const { Aligned(Cell::new(42)) }; -} - -fn main() { - // Test 0: psABI variant II — the exe's TLS module begins on its declared - // 64-byte p_align behind libtls_lib.so; the constant-16 loader landed it 32 low. - LOCAL_VALUE.with(|v| { - let addr = v as *const Aligned as usize; - assert_eq!(addr % 64, 0, "exe TLS module base off its declared 64-byte alignment: {addr:#x}"); - }); - println!("PASS: exe TLS honours its declared alignment"); - - // Test 1: exe-local thread_local works - LOCAL_VALUE.with(|v| { - assert_eq!(v.0.get(), 42, "exe TLS initial value"); - v.0.set(100); - assert_eq!(v.0.get(), 100, "exe TLS after set"); - }); - println!("PASS: exe thread_local"); - - // Test 2: shared library TLS works - unsafe { - assert_eq!(tls_get_counter(), 0, "lib TLS initial counter"); - assert_eq!(tls_increment(), 1, "lib TLS first increment"); - assert_eq!(tls_increment(), 2, "lib TLS second increment"); - assert_eq!(tls_get_counter(), 2, "lib TLS counter after increments"); - } - println!("PASS: lib thread_local counter"); - - // Test 3: shared library TLS doesn't alias exe TLS - unsafe { - assert_eq!(tls_get_label(), 0xDEAD_BEEF, "lib TLS initial label"); - tls_set_label(0xCAFE_BABE); - assert_eq!(tls_get_label(), 0xCAFE_BABE, "lib TLS label after set"); - } - // Verify exe TLS wasn't corrupted - LOCAL_VALUE.with(|v| { - assert_eq!(v.0.get(), 100, "exe TLS not corrupted by lib TLS ops"); - }); - println!("PASS: TLS isolation between exe and lib"); - - println!("all TLS tests passed"); -} diff --git a/tests/toyos-rust-tests/tls-lib/Cargo.toml b/tests/toyos-rust-tests/tls-lib/Cargo.toml index 4c997ab80bb..5cf1f69e83d 100644 --- a/tests/toyos-rust-tests/tls-lib/Cargo.toml +++ b/tests/toyos-rust-tests/tls-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tls-lib" -description = "A guest test fixture: a cdylib of thread-locals that std_tls links through DT_NEEDED." +description = "A guest test fixture: a cdylib of thread-locals that std_tls_multi_crate and the C corpus load with dlopen." version = "0.1.0" edition = "2021" diff --git a/toyos-elf/src/rela.rs b/toyos-elf/src/rela.rs index dd1fd07f54e..e0d2bc2ed01 100644 --- a/toyos-elf/src/rela.rs +++ b/toyos-elf/src/rela.rs @@ -147,9 +147,6 @@ pub struct Rules { pub extent: Extent, /// Where writes may land, `[lo, hi)` in `r_offset`'s own coordinates. pub window: (u64, u64), - /// `Some` for a chunked writer (the exe), which drops a write crossing a - /// fill page and so has it refused; `None` for a contiguous one. - pub fill: Option, /// The module's own `PT_TLS`, which a TLS relocation with `r_sym == 0` /// offsets into; `None` for a module with none. pub tls: Option, @@ -165,7 +162,7 @@ pub struct Reloc { impl Reloc { /// `r_offset`: `[offset, offset + width)` lies inside the window it was - /// parsed against, and inside one fill page for a chunked writer. + /// parsed against. pub const fn offset(&self) -> u64 { self.offset } @@ -244,17 +241,6 @@ pub fn parse(rela: Rela, rules: &Rules, symbols: SymTab<'_>) -> Result hi { return Err(RelocError::OutsideWindow); } - if let Some(fill) = rules.fill { - let within = rela - .offset - .wrapping_sub(fill.base) - .checked_rem(fill.granule) - .ok_or(RelocError::StraddlesFillPage)?; - if within.checked_add(width).is_none_or(|e| e > fill.granule) { - return Err(RelocError::StraddlesFillPage); - } - } - let sym = || SymIndex::below(rela.sym, symbols.count()).ok_or(RelocError::SymbolPastTable); let tls = || -> Result { if rela.sym != 0 { @@ -330,26 +316,6 @@ impl RelaCounts { kinds.iter().map(|&k| self.count_of(k)).max().unwrap_or(0) } - /// What an executable's loader reserves for each group it keeps, at `width` - /// bytes an entry, or why it keeps none: a TLS descriptor, which only a - /// resolver this loader does not have can fill, or a group that would - /// not fit `max_bytes`. The reservation is had only through this refusal. - pub fn for_executable(&self, width: usize, max_bytes: usize) -> Result { - if self.tlsdesc != 0 { - return Err(ExeRefusal::TlsDescriptor); - } - let kept = [RelocKind::Relative, RelocKind::GlobDat, RelocKind::Tpoff64, RelocKind::Tpoff32]; - if self.max_of(&kept).checked_mul(width).is_none_or(|b| b > max_bytes) { - return Err(ExeRefusal::TooLarge); - } - Ok(ExeReservation { - relative: self.relative, - bind: self.bind, - tpoff64: self.tpoff64, - tpoff32: self.tpoff32, - }) - } - pub fn count_of(&self, kind: RelocKind) -> usize { match kind { RelocKind::Relative => self.relative, @@ -364,49 +330,6 @@ impl RelaCounts { } } -/// The lattice a chunked writer applies relocations in: a write must land -/// wholly within one page, since the page-at-a-time applier never revisits a -/// tail handed to the next page. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FillLattice { - /// Based at the image's `vaddr_min`; each write lies within one `granule`. - pub base: u64, - pub granule: u64, -} - -/// The demand-fault page an executable's relocations are filled in. -pub const FILL_GRANULE: u64 = 4096; - -/// How many entries of each group an executable's loader keeps: made only by -/// [`RelaCounts::for_executable`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub struct ExeReservation { - pub relative: usize, - /// `GLOB_DAT` and `JUMP_SLOT`. - pub bind: usize, - pub tpoff64: usize, - pub tpoff32: usize, -} - -/// Why an executable's relocations are refused before any is kept. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ExeRefusal { - /// [`RelocError::TlsDescriptor`]'s reason. - TlsDescriptor, - /// A group would not fit one allocation. - TooLarge, -} - -impl ExeRefusal { - pub const fn as_str(self) -> &'static str { - match self { - ExeRefusal::TlsDescriptor => RelocError::TlsDescriptor.as_str(), - ExeRefusal::TooLarge => "ELF: a relocation group does not fit one allocation", - } - } -} - /// Why a relocation cannot be applied. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RelocError { @@ -416,8 +339,6 @@ pub enum RelocError { OutsideWindow, /// `r_sym` names an entry past the end of `.dynsym`. SymbolPastTable, - /// The write would cross a fill page, so a chunked writer would drop it. - StraddlesFillPage, /// A TLS descriptor, which only a resolver this loader does not have can /// fill. TlsDescriptor, @@ -438,7 +359,6 @@ impl RelocError { RelocError::OffsetOverflows => "ELF: relocation r_offset + width overflows", RelocError::OutsideWindow => "ELF: relocation r_offset outside the writable image", RelocError::SymbolPastTable => "ELF: relocation r_sym past .dynsym", - RelocError::StraddlesFillPage => "ELF: relocation crosses a fill-page boundary", RelocError::TlsDescriptor => "ELF: R_AARCH64_TLSDESC has no resolver in this loader", RelocError::RelativeOutsideImage => "ELF: RELATIVE addend is no address inside the image", RelocError::TlsOutsideSegment => "ELF: TLS relocation names an offset outside its PT_TLS", diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index dd608e25fc7..234ad999bcc 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -23,7 +23,7 @@ mod common; use common::*; use toyos_elf::dynamic::{Dynamic, InitArray}; -use toyos_elf::rela::{self, FillLattice, Op, ReadTables, RelaTable, Rules, TlsRef}; +use toyos_elf::rela::{self, Op, ReadTables, RelaTable, Rules, TlsRef}; use toyos_elf::sym::{self, SymTab}; use toyos_elf::tls::{self, Static, TlsOffset, Variant}; use toyos_elf::{ImageRange, Layout, Machine, TlsSegment}; @@ -117,21 +117,10 @@ impl Rng { } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Mode { - /// Demand-filled at `USER_VM_BASE`, written a page at a time: the window - /// is the whole image and a write may not cross a fill page. - Exe, - /// Loaded whole at vaddr 0: the window is the writable segments, and no - /// table the loader reads may sit on a page of it. - Lib, -} - /// One module, with the numbers it was built from. struct Case { bytes: Vec, machine: Machine, - mode: Mode, } fn reloc_number(machine: Machine, which: usize) -> u32 { @@ -149,11 +138,8 @@ fn generate(rng: &mut Rng) -> Case { // rate per number would refuse nearly every image over its first one. rng.hostile = rng.pick(&[0, 1, 2, 5, 15]); let machine = if rng.chance(50) { Machine::X86_64 } else { Machine::Aarch64 }; - let mode = if rng.chance(50) { Mode::Exe } else { Mode::Lib }; - let vmin = match mode { - Mode::Exe => rng.pick(&[0u64, 0x1000, 0x40_0000]), - Mode::Lib => 0, - }; + // Where the kernel loads a library: at vaddr 0. + let vmin = 0u64; let bss = rng.pick(&[0u64, 0x800, 0x3000]); let span = SIZE as u64 + bss; @@ -240,7 +226,7 @@ fn generate(rng: &mut Rng) -> Case { } } - Case { bytes, machine, mode } + Case { bytes, machine } } fn put(bytes: &mut [u8], at: u64, data: &[u8]) { @@ -347,27 +333,16 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), None => return Err(()), }; let rela_bytes = at(&case.bytes, rela_range); - let window = match case.mode { - Mode::Exe => (extent.min(), extent.max()), - Mode::Lib => { - let window = layout.writable_window().ok_or(())?; - let span_of = |r: ImageRange| (r.start().get(), r.end().get()); - let tables = ReadTables { - dynsym: span_of(sym_range), - dynstr: span_of(str_range), - rela: span_of(rela_range), - jmprel: (0, 0), - }; - rela::tables_outside_window(&tables, window, 4096).map_err(|_| ())?; - window - } - }; - let rules = Rules { - extent, - window, - fill: (case.mode == Mode::Exe).then_some(FillLattice { base: extent.min(), granule: 4096 }), - tls, + let window = layout.writable_window().ok_or(())?; + let span_of = |r: ImageRange| (r.start().get(), r.end().get()); + let tables = ReadTables { + dynsym: span_of(sym_range), + dynstr: span_of(str_range), + rela: span_of(rela_range), + jmprel: (0, 0), }; + rela::tables_outside_window(&tables, window, 4096).map_err(|_| ())?; + let rules = Rules { extent, window, tls }; let mut relocs = Vec::new(); for raw in RelaTable::new(rela_bytes, case.machine).iter() { if let Some(r) = rela::parse(raw, &rules, symbols).map_err(|_| ())? { @@ -474,10 +449,9 @@ fn every_derived_address_lies_inside_the_image_or_the_image_is_refused() { Ok(Ok(())) => reached.add(&this), Ok(Err(())) => {} Err(_) => panic!( - "seed {seed:#x} iteration {i} ({:?}, {:?}, placed {placement:?}) \ + "seed {seed:#x} iteration {i} ({:?}, placed {placement:?}) \ panicked; its image is {} bytes", case.machine, - case.mode, case.bytes.len(), ), } diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index b0c7967f073..34bbd271969 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -15,24 +15,22 @@ mod common; use common::*; use toyos_elf::dynamic::{self, Dynamic}; use toyos_elf::gnu_hash::{self, GnuHash}; -use toyos_elf::rela::{self, FillLattice, Op, Rela, RelaCounts, RelaTable, RelocError, RelocKind, Rules}; +use toyos_elf::rela::{self, Op, Rela, RelaCounts, RelaTable, RelocError, RelocKind, Rules}; use toyos_elf::section::{SectionTable, Unapplied, SHT_DYNSYM, SHT_REL, SHT_RELA, SHT_RELR, SHT_SYMTAB}; use toyos_elf::sym::SymTab; use toyos_elf::Machine; /// Every entry through [`rela::parse`], in an image spanning all of memory with -/// a TLS segment no offset leaves: the window, fill page and symbol count are -/// the only bounds left to decide. +/// a TLS segment no offset leaves: the window and the symbol count are the +/// only bounds left to decide. fn validate( mut entries: impl Iterator, window: (u64, u64), sym_count: usize, - fill: Option, ) -> Result<(), RelocError> { let rules = Rules { extent: extent(0, u64::MAX), window, - fill, tls: Some(tls_segment(u64::MAX)), }; let syms = vec![0; sym_count * toyos_elf::sym::ENTRY_SIZE]; @@ -120,25 +118,25 @@ fn validation_refuses_a_write_outside_the_window_by_name() { let overflowing = [rela(u64::MAX - 3, 0, 8, 0)].concat(); assert_eq!( - validate(RelaTable::new(&overflowing, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&overflowing, Machine::X86_64).iter(), window, 4), Err(RelocError::OffsetOverflows), ); let below = [rela(0xFF8, 0, 8, 0)].concat(); assert_eq!( - validate(RelaTable::new(&below, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&below, Machine::X86_64).iter(), window, 4), Err(RelocError::OutsideWindow), ); // One byte of an eight-byte write past the end. let straddling = [rela(0x1FF9, 0, 8, 0)].concat(); assert_eq!( - validate(RelaTable::new(&straddling, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&straddling, Machine::X86_64).iter(), window, 4), Err(RelocError::OutsideWindow), ); let fits = [rela(0x1FF8, 0, 8, 0)].concat(); - assert_eq!(validate(RelaTable::new(&fits, Machine::X86_64).iter(), window, 4, None), Ok(())); + assert_eq!(validate(RelaTable::new(&fits, Machine::X86_64).iter(), window, 4), Ok(())); } /// A table the loader reads while it writes must not lie inside the range it @@ -207,44 +205,6 @@ fn a_read_table_on_a_page_of_the_write_window_is_refused() { ); } -/// psABI oracle: every entry is applied or the object rejected. An 8-byte write -/// in a page's last 7 bytes is refused for a chunked writer, accepted for `None`. -#[test] -fn a_relocation_crossing_a_fill_page_is_refused_only_for_a_chunked_writer() { - let window = (0u64, 0x1_0000u64); - let lattice = FillLattice { base: 0, granule: 4096 }; - - for off in 0xFF9u64..=0xFFF { - let straddles = [rela(off, 0, 8, 0)].concat(); - assert_eq!( - validate(RelaTable::new(&straddles, Machine::X86_64).iter(), window, 4, Some(lattice)), - Err(RelocError::StraddlesFillPage), - "offset {off:#x} straddles the page but was accepted", - ); - assert_eq!( - validate(RelaTable::new(&straddles, Machine::X86_64).iter(), window, 4, None), - Ok(()), - "offset {off:#x} refused for a contiguous writer", - ); - } - - // A write ending at the boundary fits; a 4-byte TPOFF32 fits in the last 4. - assert_eq!( - validate(RelaTable::new(&[rela(0xFF8, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), - Ok(()), - ); - assert_eq!( - validate(RelaTable::new(&[rela(0xFFC, 0, 23, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), - Ok(()), - ); - - let shifted = FillLattice { base: 3, granule: 4096 }; - assert_eq!( - validate(RelaTable::new(&[rela(0x1000, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(shifted)), - Err(RelocError::StraddlesFillPage), - ); -} - /// A `DTPOFF64` with `r_sym == 0` writes its addend verbatim, so validation has /// to cover every type the loader writes and not just the ones it applies at /// load time. @@ -254,14 +214,14 @@ fn every_written_type_is_validated_and_no_other_is() { for raw in [6u32, 7, 8, 16, 17, 18, 23] { let bytes = [rela(0x1000, 0, raw, 0)].concat(); assert_eq!( - validate(RelaTable::new(&bytes, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&bytes, Machine::X86_64).iter(), window, 4), Err(RelocError::OutsideWindow), "type {raw} was not validated", ); } // A type nobody patches may name any offset at all. let ignored = [rela(u64::MAX, 0, 42, 0)].concat(); - assert_eq!(validate(RelaTable::new(&ignored, Machine::X86_64).iter(), window, 0, None), Ok(())); + assert_eq!(validate(RelaTable::new(&ignored, Machine::X86_64).iter(), window, 0), Ok(())); } /// A TLS descriptor is filled by a resolver this loader does not have, so an @@ -273,37 +233,15 @@ fn an_aarch64_tls_descriptor_is_refused_by_name() { let desc = [rela(0x10, 1, 1031, 0)].concat(); assert_eq!(RelocKind::from_raw(Machine::Aarch64, 1031), RelocKind::TlsDesc); assert_eq!( - validate(RelaTable::new(&desc, Machine::Aarch64).iter(), window, 4, None), + validate(RelaTable::new(&desc, Machine::Aarch64).iter(), window, 4), Err(RelocError::TlsDescriptor), ); assert!(RelocError::TlsDescriptor.as_str().contains("R_AARCH64_TLSDESC")); - assert_eq!(validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4, None), Ok(())); + assert_eq!(validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4), Ok(())); let counts = RelaCounts::of(RelaTable::new(&desc, Machine::Aarch64).iter()); assert_eq!(counts.count_of(RelocKind::TlsDesc), 1); } -/// An executable's loader keeps no group until the counts allow it: one TLS -/// descriptor refuses the image outright, before anything is reserved, and a -/// group too large for one allocation refuses it too. -#[test] -fn an_executable_s_reservation_is_had_only_through_its_refusals() { - let mixed = [rela(0, 0, 8, 0), rela(8, 1, 6, 0), rela(16, 2, 18, 0), rela(24, 3, 7, 0)].concat(); - let desc = [rela(0, 0, 1027, 0), rela(0x10, 1, 1031, 0)].concat(); - - let aarch64 = RelaCounts::of(RelaTable::new(&desc, Machine::Aarch64).iter()); - assert_eq!(aarch64.for_executable(24, usize::MAX), Err(rela::ExeRefusal::TlsDescriptor)); - assert!(rela::ExeRefusal::TlsDescriptor.as_str().contains("R_AARCH64_TLSDESC")); - - let kept = RelaCounts::of(RelaTable::new(&mixed, Machine::X86_64).iter()) - .for_executable(24, 48) - .expect("two entries of 24 bytes fit 48"); - assert_eq!((kept.relative, kept.bind, kept.tpoff64, kept.tpoff32), (1, 2, 1, 0)); - - let x86 = RelaCounts::of(RelaTable::new(&mixed, Machine::X86_64).iter()); - assert_eq!(x86.for_executable(24, 47), Err(rela::ExeRefusal::TooLarge)); - assert_eq!(x86.for_executable(usize::MAX, usize::MAX), Err(rela::ExeRefusal::TooLarge)); -} - #[test] fn a_symbol_index_past_the_table_is_refused_except_for_relative() { let window = (0u64, 0x100u64); @@ -313,22 +251,22 @@ fn a_symbol_index_past_the_table_is_refused_except_for_relative() { for raw in [6u32, 7, 16, 17, 18, 23] { let bind = [rela(0x10, 4, raw, 0)].concat(); assert_eq!( - validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 4), Err(RelocError::SymbolPastTable), "type {raw}", ); - assert_eq!(validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 5, None), Ok(())); + assert_eq!(validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 5), Ok(())); } let relative = [rela(0x10, u32::MAX, 8, 0)].concat(); - assert_eq!(validate(RelaTable::new(&relative, Machine::X86_64).iter(), window, 0, None), Ok(())); + assert_eq!(validate(RelaTable::new(&relative, Machine::X86_64).iter(), window, 0), Ok(())); } /// Four whole entries and eight bytes of a fifth: the partial entry is no /// symbol a relocation may name. #[test] fn a_symbol_index_is_bounded_by_the_whole_entries_of_its_table() { - let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), fill: None, tls: None }; + let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), tls: None }; let syms = [0u8; 4 * 24 + 8]; for raw in [6u32, 7, 16, 17, 18, 23] { let parse = |r_sym| { @@ -746,7 +684,6 @@ fn each_machine_reads_its_own_relocation_numbers() { let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), - fill: None, tls: None, }; let parsed = rela::parse(entry, &rules, SymTab::empty()).unwrap().unwrap(); diff --git a/toyos/src/lib.rs b/toyos/src/lib.rs index 5b7e3e30012..e5a4eb9feb5 100644 --- a/toyos/src/lib.rs +++ b/toyos/src/lib.rs @@ -27,6 +27,7 @@ pub mod log; pub mod net; pub mod port; pub mod process; +pub mod relocate; pub mod surface; pub mod shm; pub mod syscap; diff --git a/toyos/src/relocate/mod.rs b/toyos/src/relocate/mod.rs new file mode 100644 index 00000000000..fe01ca7cc3e --- /dev/null +++ b/toyos/src/relocate/mod.rs @@ -0,0 +1,363 @@ +//! A program applies its own relocations, first thing in its `_start`. +//! +//! The kernel maps an executable's `PT_LOAD`s and jumps to it; it reads none +//! of its dynamic section. std's and libc's `_start` call [`relocate_self`] +//! with the address of the image's own ELF header (`__ehdr_start`, found +//! PC-relatively) before anything else runs. +//! +//! **Nothing here reads a relocated word, because nothing is relocated yet**: +//! no static holding a pointer, no `&dyn`, no formatting, and no panic, whose +//! message and `Location` are pointers. The lints below hold the panic half; +//! the rest is the shape of the code, a core over byte slices and integers +//! answering integers, and one `unsafe` wrapper that hands it the image's own +//! memory. +//! +//! A static PIE carries `RELATIVE` relocations and nothing else, so that is +//! all this applies. Every other form is refused by name: a library this +//! process would have to load (`DT_NEEDED`), a symbol it would have to bind +//! (`DT_JMPREL` with entries, any other type), a table form it does not read +//! (`DT_REL`, `DT_RELR`), and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). +//! A write lands only inside a writable `PT_LOAD` and never in the `PT_TLS` +//! template, which the kernel copies from the file into every thread's block +//! and which a write here would never reach. + +#![deny( + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used, + clippy::arithmetic_side_effects +)] + +#[cfg(test)] +mod tests; + +use toyos_abi::RawHandle; + +/// Why a program's relocations are refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// The header is no little-endian ELF64 of a machine this knows, or its + /// program headers are not 56 bytes each. + Header, + /// No non-writable `PT_LOAD` at file offset 0 holds the header and the + /// program header table in its file bytes. + HeaderSegment, + /// `PT_DYNAMIC` lies in no `PT_LOAD`'s file bytes. + Dynamic, + /// `DT_NEEDED`: a library this process would have to load. + Needed, + /// `DT_JMPREL` with entries: slots this process would have to bind. + JmpRel, + /// `DT_REL`: a table form this does not read. + Rel, + /// `DT_RELR`: a table form this does not read. + Relr, + /// `DT_TEXTREL` or `DF_TEXTREL`: a write into text. + TextRel, + /// `DT_RELA` without its size, a size that is no whole number of entries, + /// or an entry size other than 24. + RelaShape, + /// The `DT_RELA` table is outside every `PT_LOAD`'s file bytes, or a + /// writable segment overlaps it. + RelaPlacement, + /// A relocation of a type other than this machine's `RELATIVE`. + Kind(u32), + /// A write outside every writable `PT_LOAD`. + OutsideWritable, + /// A write into the `PT_TLS` template. + InTlsTemplate, +} + +/// What a program whose relocations are refused writes to slot 2. +const REFUSED_LINE: &[u8] = b"this program's relocations are refused: it cannot start\n"; + +/// The status a program whose relocations are refused exits with. +pub const REFUSED_STATUS: i32 = 127; + +/// Apply the running image's own relocations, or end the process with +/// [`REFUSED_STATUS`]. +/// +/// # Safety +/// Called once, first thing in `_start`, before anything reads a relocated +/// word, with `header` the address `__ehdr_start` names: the running image's +/// own ELF header, which the `PT_LOAD` at file offset 0 maps. +pub unsafe extern "C" fn relocate_self(header: *const u8) { + // SAFETY: the caller's contract. + if unsafe { apply_to_self(header) }.is_err() { + // A literal in place: there is no table of names, whose entries would be relocated words. + let _ = toyos_abi::syscall::write_nonblock(RawHandle(2), REFUSED_LINE); + toyos_abi::syscall::exit(REFUSED_STATUS); + } +} + +/// # Safety +/// [`relocate_self`]'s. +unsafe fn apply_to_self(header: *const u8) -> Result<(), Refusal> { + // SAFETY: the caller's contract: the header is mapped, and every ELF64 header is this long. + let head = unsafe { core::slice::from_raw_parts(header, HEADER_SIZE) }; + let shape = Header::parse(head)?; + // The image's other bytes are reached by address: `__ehdr_start` gives the + // header's provenance alone. + let at = |vaddr: u64, bias: u64| bias.wrapping_add(vaddr) as usize; + // SAFETY: the kernel starts no image whose program header table no + // `PT_LOAD` maps, and lld writes it in the header's own segment, right + // behind it. An image that put it elsewhere faults on this read in its own + // address space, or `Image::new` refuses it before any other use. + let phdrs = unsafe { + core::slice::from_raw_parts( + core::ptr::with_exposed_provenance::(header.addr().wrapping_add(shape.phoff)), + shape.phlen, + ) + }; + let image = Image::new(shape, phdrs)?; + let bias = (header.addr() as u64).wrapping_sub(image.header_vaddr); + let Some(dynamic) = image.dynamic()? else { return Ok(()) }; + let rela = { + // SAFETY: inside a `PT_LOAD`'s file bytes, which the kernel mapped at + // `bias + vaddr`; read whole before the first write below. + let dynamic = unsafe { + core::slice::from_raw_parts(core::ptr::with_exposed_provenance::(at(dynamic.vaddr, bias)), dynamic.len) + }; + image.rela(dynamic)? + }; + let Some(rela) = rela else { return Ok(()) }; + // SAFETY: inside a `PT_LOAD`'s file bytes that no writable segment + // overlaps, so no write below reaches it. + let rela = unsafe { + core::slice::from_raw_parts(core::ptr::with_exposed_provenance::(at(rela.vaddr, bias)), rela.len) + }; + image.apply(rela, bias, |vaddr, value| { + let slot = core::ptr::with_exposed_provenance_mut::(at(vaddr, bias)); + // SAFETY: `apply` names only a vaddr whose 8 bytes lie in a writable + // `PT_LOAD`, which the kernel mapped writable at `bias + vaddr`, and + // outside every table this reads. + unsafe { slot.write_unaligned(value) } + }) +} + +const HEADER_SIZE: usize = 64; +const PHDR_SIZE: usize = 56; +const DYN_SIZE: usize = 16; +const RELA_SIZE: usize = 24; + +const EM_X86_64: u16 = 62; +const EM_AARCH64: u16 = 183; +const R_X86_64_RELATIVE: u32 = 8; +const R_AARCH64_RELATIVE: u32 = 1027; + +const PT_LOAD: u32 = 1; +const PT_DYNAMIC: u32 = 2; +const PT_TLS: u32 = 7; +const PF_W: u32 = 2; + +const DT_NULL: u64 = 0; +const DT_NEEDED: u64 = 1; +const DT_PLTRELSZ: u64 = 2; +const DT_RELA: u64 = 7; +const DT_RELASZ: u64 = 8; +const DT_RELAENT: u64 = 9; +const DT_REL: u64 = 17; +const DT_TEXTREL: u64 = 22; +const DT_FLAGS: u64 = 30; +const DT_RELR: u64 = 36; +const DF_TEXTREL: u64 = 4; + +/// A run of the image's bytes, at its link-time address. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Span { + vaddr: u64, + len: usize, +} + +/// What the ELF header says of the program header table and the machine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Header { + phoff: usize, + phlen: usize, + relative: u32, +} + +impl Header { + fn parse(head: &[u8]) -> Result { + let ident = head.get(..6).ok_or(Refusal::Header)?; + if ident != [0x7f, b'E', b'L', b'F', 2, 1] { + return Err(Refusal::Header); + } + let relative = match u16_at(head, 18).ok_or(Refusal::Header)? { + EM_X86_64 => R_X86_64_RELATIVE, + EM_AARCH64 => R_AARCH64_RELATIVE, + _ => return Err(Refusal::Header), + }; + if usize::from(u16_at(head, 54).ok_or(Refusal::Header)?) != PHDR_SIZE { + return Err(Refusal::Header); + } + let phoff = usize::try_from(u64_at(head, 32).ok_or(Refusal::Header)?).map_err(|_| Refusal::Header)?; + let phnum = usize::from(u16_at(head, 56).ok_or(Refusal::Header)?); + let phlen = phnum.checked_mul(PHDR_SIZE).ok_or(Refusal::Header)?; + Ok(Header { phoff, phlen, relative }) + } +} + +/// One program header, the fields this reads. +#[derive(Clone, Copy)] +struct Phdr { + kind: u32, + flags: u32, + offset: u64, + vaddr: u64, + filesz: u64, + memsz: u64, +} + +impl Phdr { + fn parse(entry: &[u8]) -> Option { + Some(Phdr { + kind: u32_at(entry, 0)?, + flags: u32_at(entry, 4)?, + offset: u64_at(entry, 8)?, + vaddr: u64_at(entry, 16)?, + filesz: u64_at(entry, 32)?, + memsz: u64_at(entry, 40)?, + }) + } + + fn writable_load(&self) -> bool { + self.kind == PT_LOAD && self.flags & PF_W != 0 + } + + /// `[vaddr, vaddr + len)` lies in this segment's first `size` bytes. + fn holds(&self, vaddr: u64, len: u64, size: u64) -> bool { + let (Some(end), Some(own_end)) = (vaddr.checked_add(len), self.vaddr.checked_add(size)) else { + return false; + }; + self.vaddr <= vaddr && end <= own_end + } + + /// `[vaddr, vaddr + len)` meets this segment's first `size` bytes. + fn meets(&self, vaddr: u64, len: u64, size: u64) -> bool { + vaddr < self.vaddr.saturating_add(size) && self.vaddr < vaddr.saturating_add(len) + } +} + +/// The running image as its program headers describe it. +struct Image<'a> { + phdrs: &'a [u8], + relative: u32, + /// Where the header's own segment puts file offset 0. + header_vaddr: u64, +} + +impl<'a> Image<'a> { + fn new(header: Header, phdrs: &'a [u8]) -> Result, Refusal> { + let mut image = Image { phdrs, relative: header.relative, header_vaddr: 0 }; + let table_end = u64::try_from(header.phoff.checked_add(header.phlen).ok_or(Refusal::HeaderSegment)?) + .map_err(|_| Refusal::HeaderSegment)?; + let first = image.loads().find(|p| p.offset == 0).ok_or(Refusal::HeaderSegment)?; + if first.filesz < table_end.max(HEADER_SIZE as u64) { + return Err(Refusal::HeaderSegment); + } + if image.meets_writable(first.vaddr, table_end) { + return Err(Refusal::HeaderSegment); + } + image.header_vaddr = first.vaddr; + Ok(image) + } + + fn headers(&self) -> impl Iterator + 'a { + self.phdrs.chunks_exact(PHDR_SIZE).filter_map(Phdr::parse) + } + + fn loads(&self) -> impl Iterator + 'a { + self.headers().filter(|p| p.kind == PT_LOAD) + } + + /// Some `PT_LOAD`'s file bytes hold `[vaddr, vaddr + len)`. + fn in_file_bytes(&self, vaddr: u64, len: u64) -> bool { + self.loads().any(|p| p.holds(vaddr, len, p.filesz)) + } + + fn meets_writable(&self, vaddr: u64, len: u64) -> bool { + self.headers().any(|p| p.writable_load() && p.meets(vaddr, len, p.memsz)) + } + + /// `PT_DYNAMIC`'s bytes, or `None` for an image with nothing to apply. + fn dynamic(&self) -> Result, Refusal> { + let Some(dynamic) = self.headers().find(|p| p.kind == PT_DYNAMIC) else { return Ok(None) }; + if !self.in_file_bytes(dynamic.vaddr, dynamic.filesz) { + return Err(Refusal::Dynamic); + } + let len = usize::try_from(dynamic.filesz).map_err(|_| Refusal::Dynamic)?; + Ok(Some(Span { vaddr: dynamic.vaddr, len })) + } + + /// The `DT_RELA` table `dynamic` names, or `None` when it names none. + fn rela(&self, dynamic: &[u8]) -> Result, Refusal> { + let (mut rela, mut size, mut plt) = (None, None, 0); + for entry in dynamic.chunks_exact(DYN_SIZE) { + let (Some(tag), Some(value)) = (u64_at(entry, 0), u64_at(entry, 8)) else { break }; + match tag { + DT_NULL => break, + DT_NEEDED => return Err(Refusal::Needed), + DT_PLTRELSZ => plt = value, + DT_RELA => rela = Some(value), + DT_RELASZ => size = Some(value), + DT_RELAENT if value != RELA_SIZE as u64 => return Err(Refusal::RelaShape), + DT_REL => return Err(Refusal::Rel), + DT_RELR => return Err(Refusal::Relr), + DT_TEXTREL => return Err(Refusal::TextRel), + DT_FLAGS if value & DF_TEXTREL != 0 => return Err(Refusal::TextRel), + _ => {} + } + } + if plt != 0 { + return Err(Refusal::JmpRel); + } + let (vaddr, size) = match (rela, size) { + (None, None) => return Ok(None), + (Some(vaddr), Some(size)) if size.is_multiple_of(RELA_SIZE as u64) => (vaddr, size), + _ => return Err(Refusal::RelaShape), + }; + if !self.in_file_bytes(vaddr, size) || self.meets_writable(vaddr, size) { + return Err(Refusal::RelaPlacement); + } + let len = usize::try_from(size).map_err(|_| Refusal::RelaPlacement)?; + Ok(Some(Span { vaddr, len })) + } + + /// Write `bias + addend` at every entry of `rela` through `write`, which + /// is handed the entry's link-time address; the first refusal stops it. + fn apply(&self, rela: &[u8], bias: u64, mut write: impl FnMut(u64, u64)) -> Result<(), Refusal> { + for entry in rela.chunks_exact(RELA_SIZE) { + let (Some(offset), Some(info), Some(addend)) = (u64_at(entry, 0), u64_at(entry, 8), u64_at(entry, 16)) + else { + return Err(Refusal::RelaShape); + }; + // `r_info`'s low half is the type: the mask keeps it. + let kind = (info & 0xffff_ffff) as u32; + if kind != self.relative { + return Err(Refusal::Kind(kind)); + } + if !self.loads().any(|p| p.writable_load() && p.holds(offset, 8, p.memsz)) { + return Err(Refusal::OutsideWritable); + } + if self.headers().any(|p| p.kind == PT_TLS && p.meets(offset, 8, p.filesz)) { + return Err(Refusal::InTlsTemplate); + } + write(offset, bias.wrapping_add(addend)); + } + Ok(()) + } +} + +fn u16_at(bytes: &[u8], at: usize) -> Option { + Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?)) +} + +fn u32_at(bytes: &[u8], at: usize) -> Option { + Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?)) +} + +fn u64_at(bytes: &[u8], at: usize) -> Option { + Some(u64::from_le_bytes(bytes.get(at..at.checked_add(8)?)?.try_into().ok()?)) +} diff --git a/toyos/src/relocate/tests.rs b/toyos/src/relocate/tests.rs new file mode 100644 index 00000000000..3f530fb0323 --- /dev/null +++ b/toyos/src/relocate/tests.rs @@ -0,0 +1,274 @@ +//! The relocator against lld, and every refusal. +//! +//! The oracle is lld itself. `toyos/tests/relocate/fixture.rs` is linked three +//! times per architecture with the sysroot's `rust-lld`, from its object +//! `rustc --edition 2021 --crate-type lib --emit obj -C panic=abort --target +//! -unknown-toyos fixture.rs` (`RUSTC_BOOTSTRAP=1`, and `--cfg tls` for +//! the third): +//! +//! - `.elf`: `-flavor gnu -pie --no-dynamic-linker -e _start`; +//! - `-applied.elf`: the same with `--image-base=0x10000000 +//! --apply-dynamic-relocs`, so lld writes every value itself at the same +//! file offsets; +//! - `-tls.elf`: the first line's, of the `--cfg tls` object. +//! +//! The relocator run on the first at bias [`BASE`] writes, at each relocated +//! word, what lld wrote there in the second. + +#![allow( + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + clippy::expect_used, + clippy::arithmetic_side_effects +)] + +use super::*; + +/// The image base the `-applied` fixtures were linked at. +const BASE: u64 = 0x1000_0000; + +const X86_64: &[u8] = include_bytes!("../../tests/relocate/x86_64.elf"); +const X86_64_APPLIED: &[u8] = include_bytes!("../../tests/relocate/x86_64-applied.elf"); +const X86_64_TLS: &[u8] = include_bytes!("../../tests/relocate/x86_64-tls.elf"); +const AARCH64: &[u8] = include_bytes!("../../tests/relocate/aarch64.elf"); +const AARCH64_APPLIED: &[u8] = include_bytes!("../../tests/relocate/aarch64-applied.elf"); +const AARCH64_TLS: &[u8] = include_bytes!("../../tests/relocate/aarch64-tls.elf"); + +/// The image `file` loads as, from vaddr 0, which every fixture starts at. +fn memory(file: &[u8]) -> Vec { + let image = image_of(file); + let mut memory = Vec::new(); + for p in image.loads() { + let end = (p.vaddr + p.memsz) as usize; + if memory.len() < end { + memory.resize(end, 0); + } + let (at, len) = (p.vaddr as usize, p.filesz as usize); + memory[at..at + len].copy_from_slice(&file[p.offset as usize..p.offset as usize + len]); + } + memory +} + +fn image_of(file: &[u8]) -> Image<'_> { + let header = Header::parse(&file[..HEADER_SIZE]).expect("lld's header"); + Image::new(header, &file[header.phoff..header.phoff + header.phlen]).expect("lld's program headers") +} + +/// Every write the relocator makes of `file` at `bias`, as the wrapper +/// reaches the tables: through the loaded image, by vaddr. +fn writes(file: &[u8], bias: u64) -> Result, Refusal> { + let image = image_of(file); + let memory = memory(file); + let slice = |span: Span| &memory[span.vaddr as usize..span.vaddr as usize + span.len]; + let dynamic = image.dynamic()?.expect("a PT_DYNAMIC"); + let rela = image.rela(slice(dynamic))?.expect("a DT_RELA"); + let mut out = Vec::new(); + image.apply(slice(rela), bias, |vaddr, value| out.push((vaddr, value)))?; + Ok(out) +} + +/// The file offset `vaddr` is read from. +fn file_offset(file: &[u8], vaddr: u64) -> usize { + let p = image_of(file).loads().find(|p| p.holds(vaddr, 8, p.filesz)).expect("a file-backed vaddr"); + (p.offset + (vaddr - p.vaddr)) as usize +} + +fn word(file: &[u8], at: usize) -> u64 { + u64::from_le_bytes(file[at..at + 8].try_into().unwrap()) +} + +fn agrees_with_lld(plain: &[u8], applied: &[u8], count: usize) { + let writes = writes(plain, BASE).expect("lld's own output applies"); + assert_eq!(writes.len(), count, "one write for each of the fixture's RELATIVE entries"); + for (vaddr, value) in writes { + let at = file_offset(plain, vaddr); + assert_eq!(value, word(applied, at), "the word at {vaddr:#x} is not what lld wrote there"); + assert_ne!(value, word(plain, at), "lld wrote {vaddr:#x} into the file too, so it tests nothing"); + } +} + +#[test] +fn x86_64_writes_what_lld_writes() { + // `readelf -r x86_64.elf`: nine `R_X86_64_RELATIVE`. + agrees_with_lld(X86_64, X86_64_APPLIED, 9); +} + +#[test] +fn aarch64_writes_what_lld_writes() { + // `readelf -r aarch64.elf`: nine `R_AARCH64_RELATIVE`. + agrees_with_lld(AARCH64, AARCH64_APPLIED, 9); +} + +#[test] +fn a_pointer_in_the_tls_template_is_refused() { + // `readelf -lr x86_64-tls.elf`: `TLS 0x000478 0x2478 … 0x10` and a + // `R_X86_64_RELATIVE` at `0x2478`; `aarch64-tls.elf` the same at `0x204e0`. + assert_eq!(writes(X86_64_TLS, BASE), Err(Refusal::InTlsTemplate)); + assert_eq!(writes(AARCH64_TLS, BASE), Err(Refusal::InTlsTemplate)); +} + +/// `tags`, `DT_NULL`-terminated, as a dynamic section's bytes. +fn dynamic(tags: &[(u64, u64)]) -> Vec { + let mut out = Vec::new(); + for &(tag, value) in tags.iter().chain(&[(DT_NULL, 0)]) { + out.extend_from_slice(&tag.to_le_bytes()); + out.extend_from_slice(&value.to_le_bytes()); + } + out +} + +/// One `Elf64_Rela`. +fn rela(offset: u64, kind: u64, addend: u64) -> Vec { + [offset, kind, addend].iter().flat_map(|w| w.to_le_bytes()).collect() +} + +/// The x86-64 fixture's own table, with `tags` beside it. +fn x86_tables(tags: &[(u64, u64)]) -> Result, Refusal> { + // `readelf -d x86_64.elf`: `RELA 0x210`, `RELASZ 216`. + let own = [(DT_RELA, 0x210), (DT_RELASZ, 216), (DT_RELAENT, 24)]; + image_of(X86_64).rela(&dynamic(&[&own[..], tags].concat())) +} + +#[test] +fn every_dynamic_form_it_does_not_apply_is_refused_by_name() { + assert_eq!(x86_tables(&[]), Ok(Some(Span { vaddr: 0x210, len: 216 }))); + assert_eq!(x86_tables(&[(DT_NEEDED, 1)]), Err(Refusal::Needed)); + assert_eq!(x86_tables(&[(23, 0x210), (DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); + // A `DT_JMPREL` with no entries binds nothing. + assert!(x86_tables(&[(23, 0x210), (DT_PLTRELSZ, 0)]).is_ok()); + assert_eq!(x86_tables(&[(DT_REL, 0x210)]), Err(Refusal::Rel)); + assert_eq!(x86_tables(&[(DT_RELR, 0x210)]), Err(Refusal::Relr)); + assert_eq!(x86_tables(&[(DT_TEXTREL, 0)]), Err(Refusal::TextRel)); + assert_eq!(x86_tables(&[(DT_FLAGS, DF_TEXTREL)]), Err(Refusal::TextRel)); + // `DF_BIND_NOW` alone is no write into text. + assert!(x86_tables(&[(DT_FLAGS, 8)]).is_ok()); +} + +#[test] +fn a_rela_table_of_the_wrong_shape_or_place_is_refused() { + let image = image_of(X86_64); + assert_eq!(image.rela(&dynamic(&[])), Ok(None)); + assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x210)])), Err(Refusal::RelaShape)); + assert_eq!(image.rela(&dynamic(&[(DT_RELASZ, 24)])), Err(Refusal::RelaShape)); + assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x210), (DT_RELASZ, 25)])), Err(Refusal::RelaShape)); + assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x210), (DT_RELASZ, 24), (DT_RELAENT, 16)])), Err(Refusal::RelaShape)); + // In the writable segment, `[0x2428, 0x2598)` in its file bytes. + assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x2440), (DT_RELASZ, 24)])), Err(Refusal::RelaPlacement)); + // Past the read-only segment's file bytes, `[0, 0x3cc)`, into the gap before text. + assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x3c0), (DT_RELASZ, 24)])), Err(Refusal::RelaPlacement)); + assert_eq!(image.rela(&dynamic(&[(DT_RELA, u64::MAX - 8), (DT_RELASZ, 24)])), Err(Refusal::RelaPlacement)); +} + +fn applied(file: &[u8], entries: &[Vec]) -> Result, Refusal> { + let mut out = Vec::new(); + image_of(file).apply(&entries.concat(), BASE, |vaddr, value| out.push((vaddr, value)))?; + Ok(out) +} + +#[test] +fn a_write_is_bias_plus_addend_inside_a_writable_segment() { + // The writable segment is `[0x2428, 0x3000)` in memory, `.bss` included. + assert_eq!(applied(X86_64, &[rela(0x2428, 8, 0x13d0)]), Ok(vec![(0x2428, BASE + 0x13d0)])); + assert_eq!(applied(X86_64, &[rela(0x2ff8, 8, 0)]), Ok(vec![(0x2ff8, BASE)])); + // An addend is two's complement, as `r_addend` is signed. + assert_eq!(applied(X86_64, &[rela(0x2428, 8, (-16i64) as u64)]), Ok(vec![(0x2428, BASE - 16)])); +} + +#[test] +fn a_write_outside_a_writable_segment_is_refused() { + // Into text, into the read-only headers, and across the writable segment's end. + for offset in [0x13d0, 0x40, 0x2ffc, 0x2424, u64::MAX - 4] { + assert_eq!(applied(X86_64, &[rela(offset, 8, 0)]), Err(Refusal::OutsideWritable), "{offset:#x}"); + } +} + +#[test] +fn a_write_meeting_the_tls_template_is_refused() { + // `x86_64-tls.elf`'s template is `[0x2478, 0x2488)`; an 8-byte write + // starting 4 bytes before its end meets it. + for offset in [0x2478, 0x2480, 0x2484] { + assert_eq!(applied(X86_64_TLS, &[rela(offset, 8, 0)]), Err(Refusal::InTlsTemplate), "{offset:#x}"); + } + assert!(applied(X86_64_TLS, &[rela(0x2488, 8, 0)]).is_ok()); +} + +#[test] +fn every_type_but_the_machines_relative_is_refused() { + // `R_X86_64_JUMP_SLOT`, `GLOB_DAT`, `TPOFF64`, `NONE`, and AArch64's `RELATIVE`. + for kind in [7, 6, 18, 0, 1027] { + assert_eq!(applied(X86_64, &[rela(0x2428, kind, 0)]), Err(Refusal::Kind(kind as u32))); + } + // `R_AARCH64_JUMP_SLOT` and x86-64's `RELATIVE` in an AArch64 image. + for kind in [1026, 8] { + assert_eq!(applied(AARCH64, &[rela(0x20460, kind, 0)]), Err(Refusal::Kind(kind as u32))); + } + // The first refusal stops it: nothing after it is written. + let mut out = Vec::new(); + let refused = image_of(X86_64).apply(&[rela(0x2428, 7, 0), rela(0x2430, 8, 0)].concat(), BASE, |v, w| out.push((v, w))); + assert_eq!((refused, out), (Err(Refusal::Kind(7)), vec![])); +} + +/// The x86-64 fixture's header with `edit` applied to its bytes. +fn header_of(edit: impl FnOnce(&mut [u8])) -> Result { + let mut head = X86_64[..HEADER_SIZE].to_vec(); + edit(&mut head); + Header::parse(&head) +} + +#[test] +fn a_header_this_does_not_know_is_refused() { + assert_eq!(header_of(|_| {}), Ok(Header { phoff: 64, phlen: 7 * 56, relative: R_X86_64_RELATIVE })); + assert_eq!(header_of(|h| h[0] = 0), Err(Refusal::Header)); + // ELFCLASS32, and big-endian. + assert_eq!(header_of(|h| h[4] = 1), Err(Refusal::Header)); + assert_eq!(header_of(|h| h[5] = 2), Err(Refusal::Header)); + // EM_386. + assert_eq!(header_of(|h| h[18] = 3), Err(Refusal::Header)); + // 32-byte program headers. + assert_eq!(header_of(|h| h[54] = 32), Err(Refusal::Header)); + assert_eq!(Header::parse(&X86_64[..32]), Err(Refusal::Header)); + let aarch64 = Header::parse(&AARCH64[..HEADER_SIZE]).map(|h| h.relative); + assert_eq!(aarch64, Ok(R_AARCH64_RELATIVE)); +} + +/// The x86-64 fixture's program headers with `edit` applied to each. +fn image_with(edit: impl Fn(usize, &mut [u8])) -> Result { + let header = Header::parse(&X86_64[..HEADER_SIZE]).unwrap(); + let mut phdrs = X86_64[header.phoff..header.phoff + header.phlen].to_vec(); + for (i, entry) in phdrs.chunks_exact_mut(PHDR_SIZE).enumerate() { + edit(i, entry); + } + Image::new(header, &phdrs).map(|image| image.header_vaddr) +} + +/// `readelf -l x86_64.elf`: `PHDR`, then the read-only `LOAD` at offset 0 +/// whose file bytes are `0x3cc`. +const HEADER_LOAD: usize = 1; + +#[test] +fn a_header_no_read_only_segment_holds_is_refused() { + assert_eq!(image_with(|_, _| {}), Ok(0)); + // Its segment starts one byte into the file. + assert_eq!(image_with(|i, p| if i == HEADER_LOAD { p[8] = 1 }), Err(Refusal::HeaderSegment)); + // Its file bytes end inside the program header table. + assert_eq!(image_with(|i, p| if i == HEADER_LOAD { p[32..40].copy_from_slice(&0x100u64.to_le_bytes()) }), Err(Refusal::HeaderSegment)); + // It is writable. + assert_eq!(image_with(|i, p| if i == HEADER_LOAD { p[4] |= PF_W as u8 }), Err(Refusal::HeaderSegment)); +} + +#[test] +fn a_dynamic_section_in_no_file_bytes_is_refused() { + // `DYNAMIC 0x0004c8 0x24c8 … 0xd0` is the fixture's fifth program header; + // moved past the writable segment's file bytes, `[0x2428, 0x2598)`, into its `.bss`. + let image_dynamic = |vaddr: u64| { + let header = Header::parse(&X86_64[..HEADER_SIZE]).unwrap(); + let mut phdrs = X86_64[header.phoff..header.phoff + header.phlen].to_vec(); + let entry = &mut phdrs[4 * PHDR_SIZE..5 * PHDR_SIZE]; + assert_eq!(u32::from_le_bytes(entry[..4].try_into().unwrap()), PT_DYNAMIC); + entry[16..24].copy_from_slice(&vaddr.to_le_bytes()); + Image::new(header, &phdrs).unwrap().dynamic() + }; + assert_eq!(image_dynamic(0x24c8), Ok(Some(Span { vaddr: 0x24c8, len: 0xd0 }))); + assert_eq!(image_dynamic(0x2600), Err(Refusal::Dynamic)); +} diff --git a/toyos/tests/relocate/aarch64-applied.elf b/toyos/tests/relocate/aarch64-applied.elf new file mode 100755 index 0000000000000000000000000000000000000000..9c7e01bef131f711bea5ffcf4519f90655c680dd GIT binary patch literal 3800 zcmbtX&1+m$6u+67HjRcjY6QPKVi1GoWj-^Lh?1B|V>C@-CQ7?{nU6OgP3FTlZ}MRa z7OEf!QvU!Y3l|D5-04QF;KF4VBH1Y22nCCvs6@2JbM8C8%=nTTrI);W?)lyGJLkT8 z&pUVW+SKeRUssnx^Q%u3YqNo_AYEY|t!U+(&@3P0e6QGbdqYFo@!YAcpK7Mh(+hoVs`QRfigO{hXC|3^EAilJRJ`XeDCtTroTn%Oz9Ukw`w0Dq17O zTr`)<7vr&b*a{CSyVZ2^p*72HR_e7-#26hl!b1gXt-Y9oBYv-d`f1NQ6VkwmcD{(g zqk2{JgNshRKKO(%S1xU;|h5U`;_3wXW&l> zj=To`j2`qT)KB0Of+Jsnrv*pe0-qNAs%M?gYrGd``|`9Nb*o*;H%!d%!T2cqaKFjK z0?E5n`&@KgB#b{A#&Isd-xeI_f;#(|;5Zl9|13Dp1^6JXhyCMR;3rJ@ovM8qLv2|DZLUtnH{%FU?IZ&CJhb)ZE1Bsf@}jOijEnwU|+6 z`Ya(clQ`mAk(4%yYc>D==@KNtc>V~`rANK;}ePqRVD)_ixNa_ZjtF_kw{Sg|lFYs~yk<@L+6$jRj_kV)s zIR^Nsmn3z=J?Lr&hiYm!}y^fet4~&txO7I z;ZBgo;J@L)!H0Z>=5cnNre|bBw`B4Tn&e!?`f>i7gkkNS{9Q48s}rO#;sU-zgTwfj M7%Q6vtmanwGYd6bdSzHKIaP(2Zj|iBnqSG)|j>n}j$m<-*n<>)35#TkCZ` zXeD|832`cjV?jOi01}7X;6@M}IOc#57Z4XDjzt0lLZ!Sn`~I#wO&Wn1dH2oy=Djy> zXJ+55-x$jr>+I+tlrH*|q;;h`WGU^qyaO6UDN52dzVD{{sE2VkUn%Xj<*K@d+@^Yh z#crc=SUVxfL>1n*uo3uSjT5r24k6p^B-R!6yVBihG$Ai(oKvmm48c~`5|fSWdW{z% zNIai-m+{U@uttXCo!t`3wGa~|?52MTigz2&j#I;?m)TCn`@f3##r25g6ucc{9=mjY zlU-r&s?IYw*6C}DPw6_YS^S9l`O)Gj<<~7fs(i1`GsX?nf70S_D<8KwY5R=DBif$J z<-NID)e9DDLD}~_tbfPr)6{YCiH=8i?xX!Y1$+7Wj%RX@OhJoJ)R@G9IP{&Z}X$uJRt0vi4H`RDavUrpYm;s`f}<(8qJ8 zwI6nf`?tTt`1WdlxB5+JTbs8;rcyoc`G+EzOga*aIsHyD@=Q4h>WxEveY2II+{`~>Cg7f%`MCytoyZdULkN;A(9_T#G)l{AfCvVqJ#1LP%<~zU+7PkyungF zmd_VTiFhLFMGuhQYy^eKg6B6XwQ8i_86I|`2aDdqtzhn~$#ZqnFE{O;hymO9+`yBE z8c!)l{s}43)5?pM{sHC4C(wURIr0g3pYkh~pP1p>3HcHF1E$hL$e-XzuJjOg11Qn+ zs*gO0o)?rOPlAsqN1g;vo62@V{slj-9QhFZMdiqc;3t)*G*5z0naaIrwq(}Sw$ixu zf}WCP>fFKS=j|KuG~@gEEx|gPyvnSp9P0@FzH+Ri$(zi+RE~9o{vXP*j^KS9kBp0T zgr7m?yXn?Cf~S~2)V5yW$5bEl9Odz(WRzpxB8KwKDK1wV%k$*SR+~;a*C>C9c(qxgbZ5S9VK&Zr&p&@RkC;hgj~S&3eXbZoX15=6=ns=ZeKjb(ZAao%gD2 z#A61zJj*hV64pXVrOHy!^gU-_I5|-8Vo@(YFx+2=#ts%{n@*JWFWvDg(*DhU?)1dM zXk+O3;_-slIG>+cc87GFIG?g~mu^0RwYz-!TsH65$BTuGJLI~q$shegBHX+;TdBIu z>SCo@H0o#8A8jHy8gt{5RnJrgvUM+aKCxKy)u`caXqIr}GmAAgOSre0HG-TU(6Lh! zqcbNaC$f|oIXaf1iIJ0IS;|h0jl48Aoh3K@Dw7kVSoQxbc^i?O0j;+N6_q#_B+rX3 z&Kq#aFu(H|12dcpkn&w1MQo+OK5P*^z!f=jV2^VI67K=*ao#{a%(jvWV2^VP5_YU& zirs#asrZ6D&Q-|kq1her@jb^vV$akeD|*C_c8vz*zw!rOI|nj>JaDCst$BjIfQNh3U^5CvW8NSzwtk4 zEyQMxqzZe)bygYdqdnnDM0G;etRqljZ7=e%B`n$wrLFdFSseC|OWcpOZ&&?H?SIxz qiC4bm5I^?MC1!}7tF3ZX<9@G2!V1O(eupdT*~j$5?KR85YSKT7z=MDQ literal 0 HcmV?d00001 diff --git a/toyos/tests/relocate/aarch64.elf b/toyos/tests/relocate/aarch64.elf new file mode 100755 index 0000000000000000000000000000000000000000..a7f10fe29cff8c8e82774f647f592a8cb1f878f0 GIT binary patch literal 3800 zcmbuC%WoT16o+qOw`m{>27<)9Mns4RG8w;OCkW&ary<}b!49ZhP5c-?n)tzZoCmE$ zrGkWngvuWP%7O(*EZD&YQ6v^Dvp^^tP&Y`GsE|N_f*_Q0#^>iaQ>Ov-Dl_Na@1Dn< z`#9HmVrQbX@mP!P6bI#U ztYrte-J%K--RE*=wp&BeE*z3bvvq+P3;_Ik}OS*GcHu1`L?=k=88 zx$tPuLjxz(FtPjT*`XO5;{L>-FEQ|l?}Og=y@`R7zQmyV;d{cY7pLdzBlOr;iJnhM z&xzOJkA1vJ&xc3&V7*W1xqO7)!=&>qJr}yfBJ7mC^Iv|v=TYzZ)y34_`L8~@;xWDh zi+5qPKX@93p!0w2By3k9$GhFL(O70YkPE~L=6E3+%w}_ia44*s`iQcc4Lj#wGp$CsR`mz8i3v>~ z&6{iOv2>~Mu)XS+8}2T@15Qw0Klovf3rD_z-4nuxBp-l}2uJ<^e_A;52l$w9+;`wX zr_xXPTNXw-B91(+kmsNu6OKFw{)}+sIq>J4O20z>1fLR)dUXgus6>jFPI@w?Ujx`0m#$9(GKmrGnY=Ev_)dTII1YGbpav|_cXl`@Tzk~eeP zQkrG1W>|@8wS30TD9tPx1uIiAm1dh8HhCb*YNX(}%T&ra$Ihx5^-Mlrt`-#^wu)IL zZ3<>*vLs97zEx{URVZ)RP0Q3G6R}9n4C-b!G7-q>!O>i?sp)EXSww~o42128li=%=~N==x|vxCuh%RwY8Z!_g^keCdX3D&#sRa2ow00n>XrHF zrMZRqw3?qfJ(E`H#hIy>W>RToB+e3E<$6ur_jt%>EUX=@fTTBrb_E^`m)Xxm|+me^J#a_n` z1@YrvzY=j~y7WW9{w)BJu!nq&IM%LH^^8pDmUP}loxE2ueysl{F^s*_epeLV>4a2A UT;NMo>1KaPepsbk2Ck<17o-7%k^lez literal 0 HcmV?d00001 diff --git a/toyos/tests/relocate/fixture.rs b/toyos/tests/relocate/fixture.rs new file mode 100644 index 00000000000..42c5ca1f142 --- /dev/null +++ b/toyos/tests/relocate/fixture.rs @@ -0,0 +1,59 @@ +//! The relocator's oracle fixture: every word lld relocates in a static PIE +//! of it, a table of pointers, a table of vtables and a table of functions, +//! and with `--cfg tls` a thread-local holding a pointer, which lld relocates +//! inside the `PT_TLS` template. `toyos/src/relocate/tests.rs` says how each +//! file beside this one is made. + +#![no_std] +#![cfg_attr(tls, feature(thread_local))] + +pub trait Speak: Sync { + fn word(&self) -> u32; +} + +pub struct One; +pub struct Two; + +impl Speak for One { + fn word(&self) -> u32 { + 1 + } +} + +impl Speak for Two { + fn word(&self) -> u32 { + 2 + } +} + +fn one() -> u32 { + 1 +} + +fn two() -> u32 { + 2 +} + +#[no_mangle] +pub static SPEAKERS: [&dyn Speak; 2] = [&One, &Two]; + +#[no_mangle] +pub static NAMES: [&str; 3] = ["one", "two", "three"]; + +#[no_mangle] +pub static FUNCTIONS: [fn() -> u32; 2] = [one, two]; + +#[cfg(tls)] +#[thread_local] +#[no_mangle] +pub static mut LABEL: &str = "label"; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +} + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/toyos/tests/relocate/x86_64-applied.elf b/toyos/tests/relocate/x86_64-applied.elf new file mode 100755 index 0000000000000000000000000000000000000000..111fb0944e76aeae84c209be58372eae9600bf25 GIT binary patch literal 3312 zcmb_eO>Ep$5PqAMrVXVmv`7><7z9!fisSuBc2n__kS0w}`N-lVs1l6ioj2diH}Cn) zd*l6NVtTSCl~Rx+>UG7IaYm&X8|5F8d>VzoV+Cdg(Wo#Qr{kUlY3-_|Zx;s)fF zbn2rym0C@wo;}%n{9D?P=LqL#gX*u3!q}*Uva z1-@`sNq>C^m*-ul=i@{CoO^?wr2}#ktmX35m$x^#ub%jW&UJyyr>D-xsIRcaMj^K8V+MOZe!0e~K;PduqBKEc)R& zV|x0$kh+D_=nshh zL2#TG;P(Z`_Y8TsFi)rxs$bSgS4vBt;OJ|X;Az2cNW3AqGRtd0d#$0&YS1xjPP?Y$ zm+>f6X6P?D97LmL*@+xw`Zc=}It^c$k-r*I20}TE0(3g3QTH@2Y}zfST&@RI#l>#; z0Y%efk>irCQTlikYgMVQMxD?%henD+o}aaRcW5NzS=j-v+A%F<&&@24w+k0mE_i-> z(Vbtj3&O}zclDj(>t5)RA@ U_%j;d-GQPwtzb}9-<^U<`Jq{Jx%3E1x1&mWDwYpvIb zgOJDt6c9)iryM!zm;4dq)&4Lu|{ z!_4a?=fnxEM#4zu9iuBI{+QGWy(S5vyLnt|TX^l4!$9b5sdML~B#`+|kja!@JPE@z zv@W6alj8YCxmecc59&Lisi47Ix$75?nlDQ^UwfqQg~v|}-J*p$N6Wz19vzh6Xw5u1@>&1v6UP@QKzy%} zzTLxrmVEEiwM+h>X7GjECjDg(F3-D5*T+5loO_S1+9A0K4&?gc=Xdvb!$0``N0z|m z!`)@yQ}-SQPWb;te3(6amoYv}%XW5LQTtfOb8x^V93@fzKiPlo@4J#O*oJ2#$+=Ik zEb`ir;C%fWnhNcc*PP{nUaM`?#~Pk{Qx|-L z0%CS1os5_CL@I5U;*%*mleH!jg+#WbCrftHwhN_nDjnDHSCrRk`i0nr_L^1Ki6xAw zDI-2s)EkeA`TK~UYf$~RAN9P*j{cLKehPn#22atdqrYJN-Vhx91^9=8Ps@vo{rXC9 z^b5p)Cph{A@CSl7B6+xZo>s#O{f}O;K!yoF(>V>Q^W7Mcc=W%x#Ges-NcvDraAg#? zoaR%3|%oxo>kY%@O9g#dkFKLqbV-PYSpWS zfbO|w!zvc5PFe9;sB4EvG^cObq$@N_I105YRoi~c)5gS9cA}t@acxgbB?|H6SfSi9 z;>uiE+L&!-E^l5g=;pe;x@BgBk)l3&l$(02xGV46%-ddLu~^8N8Pha_la3TwHnmuW0Gqt+u5-n{Wpf!EV^VP!j zrP#-m09fHf7Em=g6~P^^~OLn`Udv_ zj>Y)~&i&{Q>Or8PZ$R_6gN3;);D?q{Pt$;V68Pvd&^YhlqaQ&(M_lgZ;G?fWgGV8O zMD3Rd(3u!#^U0K0{>G0qQQr56OO&_4&9aw$x0yC p%Qd2YoS$vMNga?q`4RZ~020ev=GbG89&;4E1lmIm9&+rZAOxfyEEc8ly_xrubxdkOAI!Y}`@Q#j|L-^R z@zm^eZ%>bc98s?;wu~neWLzA#k4b~nxGJfBdS}#0b(HMB7)$))i6U=x|_#i9h6Q7UrYj_TQbhwhb4jZ*GnezwuQGX zJm?(>hsOLCZben5JZOK?y}Cd=?(hE+&Wq=nR8rvGFxCU}z`9y8Z{Rm&-AgI_Ctfg; znH0V!_)-f0S@2I%xRUdCHHBvc|2BntVt<#yuZSJ^I$8@NFVs37pQKSeA1{^}4e z_q$2YM~C=1_ZB@X2jnI=mWwlA+}h%eeeZ`GEV0dN+-2{ny?e0}{s)Lpvj6if*YGb# z+UC1@kZ_mxleGWe?BDnI1IZUJ`%|gp{bFb2yx|=03VvKPuQAJA$p;*Fa$5$1GJ$x^ z^MvFdNYcc2kDI^VOU8Fo_s%b8iTWDI&y)M}p|9b%4&0{6$Y1eB{C_%aEpbURnsH+f?Kmj^KP-^jAkp@QcaK6 z+??xHYK45k($>o=?6jkbv8uy%Bk+x^IW}fmLsh+sp&X=n*nR4k{j~2CJ9-Dyw%j}T z4Qf12Hv;tp>-UD>s3*YR7yP!=8SK~Bf}=hl{(HeuAAsK#9N#nK;f;Ar9an=gPP$UD z3K)Tk_QKriWV?8F?QBK2m)*q;yC{r2Rmc0esd9?jd*^c54O=tS z%B)?qZ96_X*&-|3)%AvNcl@=6UyZ|;4(@F(dNXI|=Y1VFM#?SiEEm>-P)Ke2P+Gyx zFRledE7*UdwIe5t)bz!<$;ET?b7eI*@%B_%mCsL2yfw8@R`!`ogq)kimi-sa$D7ZV zTyuqb0F8RU_nTvJ-hgvGxkI%DeAEMI{syoxmj(R9Qi`tw*BbCqKcI1r!RK+XK%b&E z*J$ujub{zW4olj8o-ob@KI$p-E|W;W!}u}(SHuTC{uiKeZ{s9gd0^*L{NE%I^p6M> zjs5|@CVbR$)Fb>)E+J4f))O!vBAy$*bEqHq&%261(dqur6V82uk2;C}%N^lizre>h zE|H%x@Hbyd2%GW&^6=r2JlF>@l}8Q6EC8ax|15=r5B&l8v3K2}hAgNdfsc}t$B6M` j|2G8h=5l*nKG0vL9OxJDr_{jQfw8_W{1;M$l&Ss%X~JIa literal 0 HcmV?d00001 diff --git a/userland/libc/src/arch/aarch64.rs b/userland/libc/src/arch/aarch64.rs index 151ea40adfd..2d7c0c27ce2 100644 --- a/userland/libc/src/arch/aarch64.rs +++ b/userland/libc/src/arch/aarch64.rs @@ -6,11 +6,15 @@ /// the stack pointer 16-byte aligned: /// [sp] = argc /// [sp+8] = argv[0], argv[1], ..., NULL +/// The image relocates itself first, from its own header's address. #[cfg(not(feature = "std-runtime"))] #[unsafe(no_mangle)] #[unsafe(naked)] unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( + "adrp x0, __ehdr_start", + "add x0, x0, :lo12:__ehdr_start", + "bl {relocate}", "ldr x0, [sp]", // argc "add x1, sp, #8", // argv // The outermost frame record: a backtrace ends here. @@ -18,6 +22,7 @@ unsafe extern "C" fn _start() -> ! { "mov x30, xzr", "bl {start_c}", "brk #0x1", + relocate = sym toyos::relocate::relocate_self, start_c = sym crate::runtime::start_c, ); } diff --git a/userland/libc/src/arch/x86_64.rs b/userland/libc/src/arch/x86_64.rs index f20cc8af4a2..e676c8dd482 100644 --- a/userland/libc/src/arch/x86_64.rs +++ b/userland/libc/src/arch/x86_64.rs @@ -3,15 +3,19 @@ /// Entry point for C programs. Stack layout at entry (set up by kernel): /// [RSP] = argc /// [RSP+8] = argv[0], argv[1], ..., NULL +/// The image relocates itself first, from its own header's address. #[cfg(not(feature = "std-runtime"))] #[unsafe(no_mangle)] #[unsafe(naked)] unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( + "lea rdi, [rip + __ehdr_start]", + "call {relocate}", "mov rdi, [rsp]", // argc "lea rsi, [rsp + 8]", // argv "call {start_c}", "ud2", + relocate = sym toyos::relocate::relocate_self, start_c = sym crate::runtime::start_c, ); } From 1d0455d81be7bcb867b01d571b352f11c29eb280 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:31:03 +0200 Subject: [PATCH 03/15] spawn_cost: how long a spawn of shell takes to its exit, a metal row for an A/B /system/bin/shell -c '' returns as soon as it has started, so spawn to exit is the kernel's spawn, the program's own start (its relocation, which stage 1 moved there from the kernel's spawn and fault path) and its exit. The job prints the minimum and the median over 50 spawns after one that warms the file's pages; the row judges that it ran and said so, and the host reads the numbers, as syscall_cost's. Under QEMU a duration is no measurement, so the shared boot skips it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/toyos-rust-tests/src/bin/spawn_cost.rs | 32 ++++++++++++++++++++ tests/toyos.rs | 17 +++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/toyos-rust-tests/src/bin/spawn_cost.rs diff --git a/tests/toyos-rust-tests/src/bin/spawn_cost.rs b/tests/toyos-rust-tests/src/bin/spawn_cost.rs new file mode 100644 index 00000000000..8af013e11af --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/spawn_cost.rs @@ -0,0 +1,32 @@ +//! What starting a program costs: `/system/bin/shell -c ''` spawned and waited +//! for. It returns as soon as it has started, so what the clock reads is the +//! kernel's spawn, the program's own start, its relocation among it, and its +//! exit. +//! +//! **No threshold, and the host reads the numbers instead**, as +//! `syscall_cost`'s: a duration measured under TCG is no duration, and one +//! measured on metal is for a same-session A/B against another build of this +//! tree. The minimum and the median over repetitions, after a first spawn that +//! reads the file's pages off the device. + +use std::process::Command; + +use toyos_abi::clock::nanos_since_boot as clock_nanos; + +const SHELL: &str = "/system/bin/shell"; +const REPS: usize = 50; + +fn spawn_ns() -> u64 { + let start = clock_nanos(); + let status = Command::new(SHELL).args(["-c", ""]).status().unwrap_or_else(|e| panic!("spawn {SHELL}: {e}")); + let end = clock_nanos(); + assert!(status.success(), "{SHELL} -c '' exited {status:?}"); + end - start +} + +fn main() { + spawn_ns(); + let mut ns: Vec = (0..REPS).map(|_| spawn_ns()).collect(); + ns.sort_unstable(); + println!("spawn_cost: {SHELL} min {} us median {} us over {REPS}", ns[0] / 1000, ns[REPS / 2] / 1000); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index d795f879202..e14157ed3d8 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -80,6 +80,9 @@ const RUST_SKIP: &[&str] = &[ // Its product is a cycle count per syscall and the clock rate beside it, // which a guest's host sets: the `syscall_cost` metal row runs it. "syscall_cost", + // Its product is how long a spawn of `shell` takes to its exit, which a + // guest's host sets: the `spawn_cost` metal row runs it. + "spawn_cost", // Its verdict is a duration: whether a shootdown waits for every other CPU // is read off the clock around the syscall. The `tlb_shootdown_waits` // metal row runs it on the T14. @@ -576,6 +579,20 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // How long a spawn of `shell` takes to its exit, printed by the job and + // read off the stick for an A/B against another build: that it ran, and + // what it said, are the verdict. + "spawn_cost", + metal::Metal { + arms: TESTCASES, + judge: |b| { + b[0].job_passed("test_rs_spawn_cost")?; + b[0].log().must_say("spawn_cost: /system/bin/shell min ")?; + Ok(()) + }, + }, + ), ( // `SYS_DEBUG` holds each other CPU's shootdown acknowledgement back, // so the kernel that carries it; the verdict is the guest's own exit, From 8412441963abf10c82202779bb3c8dcdc288a07c Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:36:18 +0200 Subject: [PATCH 04/15] The relocator's six rust-lld fixtures are declared, ours Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- src/licence.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/licence.rs b/src/licence.rs index 33433164732..b6cdb8ef377 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -535,6 +535,42 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "ours: a ToyOS binary this build produced (toyos-symbols/tests/real.rs)", Terms::Spdx("MIT OR Apache-2.0"), ), + ( + "toyos/tests/relocate/aarch64-applied.elf", + "a2d2c6bf84693da880f1389d9d3c59ceba5dee8cc4cb6dba94aa6fcc1c7eff77", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "toyos/tests/relocate/aarch64-tls.elf", + "035ac304d710439db036c944ba9e468a749bea15b6c6943debbcb759e6aba9a2", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "toyos/tests/relocate/aarch64.elf", + "629fd3a391821ea9113b3028eec23f50f7e204442b0be16f8cf7593955bc5dd6", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "toyos/tests/relocate/x86_64-applied.elf", + "d9fbd54a6d22e7a5c235075951b3a162d59b818c48c0a898e826d9a123c5eefb", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "toyos/tests/relocate/x86_64-tls.elf", + "9fe7bf2c16810153d0d19af0799f3612ef6305a0e09f25c6aa93fe4f226b1abe", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "toyos/tests/relocate/x86_64.elf", + "61ddae0382bafa0c4f76c23ac9608e7a53281d57a8439fe05d4f43390d2eee46", + "ours: rust-lld's link of toyos/tests/relocate/fixture.rs (toyos/src/relocate/tests.rs)", + Terms::Spdx("MIT OR Apache-2.0"), + ), ( "toyos-microcode/intel-ucode/06-8c-01", "efe83e312b90f7fe4b8f75260087edf03e048d2f4f80caef2ef631c842714bb3", From 0829cc01c4014b6b33bf4726bd7dcaf9a24f172d Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 13:11:35 +0200 Subject: [PATCH 05/15] toyos-elf loses what only the executable loader called; the relocator's refusal stays its own Dynamic::strtab_table, Dynamic::needed with DT_NEEDED, SectionTable::rela_dyn and Layout::file_bytes_from had no caller left but their tests once the kernel stopped reading executables' tables. The two tests of the dynamic table's walk that read it through needed now read it through parse. The relocator's Refusal and exit status are private: nothing outside the module names them. The loader's header names the build-id note it still reads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- kernel/src/loader/mod.rs | 8 ++++---- toyos-elf/src/dynamic.rs | 15 --------------- toyos-elf/src/layout.rs | 17 ----------------- toyos-elf/src/section.rs | 23 ----------------------- toyos-elf/tests/crafted.rs | 17 ----------------- toyos-elf/tests/tables.rs | 35 ++++++----------------------------- toyos/src/relocate/mod.rs | 4 ++-- 7 files changed, 12 insertions(+), 107 deletions(-) diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 4264e1eb0c1..53c13e1fc7e 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -1,9 +1,9 @@ //! Building a process out of an ELF file. //! -//! The kernel reads an executable's program headers and its `PT_TLS` -//! template, demand-pages its `PT_LOAD`s and jumps to its entry; it reads no -//! dynamic section, applies no relocation and loads no library at spawn. The -//! program relocates itself (`toyos::relocate`). +//! The kernel reads an executable's program headers, its `PT_TLS` template +//! and its build-id note, demand-pages its `PT_LOAD`s and jumps to its entry; +//! it reads no dynamic section, applies no relocation and loads no library at +//! spawn. The program relocates itself (`toyos::relocate`). //! //! Every number the file names is untrusted: a refusal is //! `SyscallError::{InvalidArgument, ResourceExhausted}`, never a panic. diff --git a/toyos-elf/src/dynamic.rs b/toyos-elf/src/dynamic.rs index 361572afbe7..091449f0f56 100644 --- a/toyos-elf/src/dynamic.rs +++ b/toyos-elf/src/dynamic.rs @@ -21,7 +21,6 @@ pub struct Table { } pub const DT_NULL: i64 = 0; -pub const DT_NEEDED: i64 = 1; pub const DT_PLTRELSZ: i64 = 2; pub const DT_STRTAB: i64 = 5; pub const DT_SYMTAB: i64 = 6; @@ -81,11 +80,6 @@ impl Dynamic { out } - /// The `DT_STRTAB`/`DT_STRSZ` pair, when the file names both. - pub fn strtab_table(&self) -> Option { - Table::from_tags(self.strtab, self.strsz) - } - /// `.dynsym`'s entry count where no section header says it: what the /// `.gnu.hash` table describes, `gnu_hash` being its bytes from /// `DT_GNU_HASH` on, or else the gap from `DT_SYMTAB` to `DT_STRTAB`, @@ -101,15 +95,6 @@ impl Dynamic { Some(table) => GnuHash::parse(table).and_then(|h| h.sym_count()).ok_or(gap), } } - - /// `DT_NEEDED` offsets into the string table, in the order they appear. - /// - /// An iterator rather than a collection: the count is one per entry of a - /// table whose length the file chose, so anything that materialises it is - /// an allocation sized by untrusted input. - pub fn needed(data: &[u8]) -> impl Iterator + '_ { - Entries::new(data).filter_map(|(tag, val)| (tag == DT_NEEDED).then_some(val)) - } } /// Bytes in one `.init_array` entry, an ELF64 address. diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index 18068cb0db0..d3f850aea06 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -570,23 +570,6 @@ impl Layout { self.vaddr_to_file_offset(self.extent.min.checked_add(at.get())?) } - /// How many bytes of file back `vaddr` before the segment holding it runs - /// out. - /// - /// `.gnu.hash` declares no length anywhere — its extent is the section it - /// lives in and no `DT_*` tag names one — so the honest bound is the - /// containing segment's own file image. `None` when no segment's file - /// image covers `vaddr`. - pub fn file_bytes_from(&self, vaddr: u64) -> Option { - for seg in self.segments() { - let Some(within) = vaddr.checked_sub(self.seg_vaddr(seg)) else { continue }; - if within < seg.filesz { - return seg.filesz.checked_sub(within); - } - } - None - } - /// A segment's `p_vaddr`, back out of its image offset: inside the extent, /// so the sum cannot overflow. fn seg_vaddr(&self, seg: &Segment) -> u64 { diff --git a/toyos-elf/src/section.rs b/toyos-elf/src/section.rs index 2b9f2456d5e..7f5ebab035b 100644 --- a/toyos-elf/src/section.rs +++ b/toyos-elf/src/section.rs @@ -122,27 +122,4 @@ impl<'a> SectionTable<'a> { } Ok(self.iter().filter(|sh| sh.kind == SHT_RELA)) } - - /// The `.rela.dyn` section, for a file with no `PT_DYNAMIC`. - /// - /// Identified by shape rather than by name: `.shstrtab` would have to be - /// located first to read section names, and `e_shstrndx` is not in this - /// table. `first_entry` reads the section's first `Elf64_Rela` off the - /// file, and only a table whose first entry is a `R_X86_64_RELATIVE` - /// qualifies — a `SHT_RELA` of any other shape belongs to something else. - pub fn rela_dyn( - self, - first_entry: &mut dyn FnMut(u64) -> Option, - ) -> Option<(u64, u64)> { - for sh in self.iter() { - if sh.kind != SHT_RELA || sh.entry_size != crate::rela::ENTRY_SIZE as u64 || sh.size == 0 - { - continue; - } - if first_entry(sh.offset).is_some_and(|r| r.kind() == crate::rela::RelocKind::Relative) { - return Some((sh.offset, sh.size)); - } - } - None - } } diff --git a/toyos-elf/tests/crafted.rs b/toyos-elf/tests/crafted.rs index 4d44324f914..bbf429dcd27 100644 --- a/toyos-elf/tests/crafted.rs +++ b/toyos-elf/tests/crafted.rs @@ -330,23 +330,6 @@ fn an_extrapolated_file_offset_that_overflows_has_no_answer() { assert_eq!(layout.vaddr_to_file_offset(0x2000), None); } -/// `.gnu.hash` declares its length nowhere, so its bound is the file image of -/// the segment holding it. -#[test] -fn the_file_bytes_behind_a_vaddr_are_the_containing_segments() { - let layout = accepted( - Elf::new(0x4000) - .ph(Phdr::load(0, 0, 0x1000, 0x2000, PF_R | PF_X)) - .ph(Phdr::load(0x2000, 0x2000, 0x800, 0x2000, PF_R | PF_W)) - .build(), - ); - assert_eq!(layout.file_bytes_from(0), Some(0x1000)); - assert_eq!(layout.file_bytes_from(0xF00), Some(0x100)); - // Inside the segment's memory image but past its file image: no bytes. - assert_eq!(layout.file_bytes_from(0x1800), None); - assert_eq!(layout.file_bytes_from(0x2400), Some(0x400)); -} - /// `(image offset, bytes, count)` of the table a layout places, for comparing. fn table_of(layout: &Layout) -> Option<(u64, u64, u16)> { layout.program_headers().map(|t| (t.image().start().get(), t.image().len(), t.count())) diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index 34bbd271969..db34919df26 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -76,20 +76,19 @@ fn a_table_needs_both_of_its_tags_and_a_non_zero_size() { #[test] fn a_dynamic_table_with_no_null_terminator_stops_at_the_buffer() { - let tags: Vec<_> = (0..64).map(|i| (dynamic::DT_NEEDED, i as u64)).collect(); - let data = dynamic_unterminated(&tags); - assert_eq!(Dynamic::needed(&data).count(), 64); + let data = dynamic_unterminated(&[(dynamic::DT_RELA, 0x1000), (dynamic::DT_RELASZ, 48)]); + assert_eq!(Dynamic::parse(&data).rela, Some(dynamic::Table { vaddr: 0x1000, size: 48 })); // And a trailing partial entry is not an entry. let short = &data[..data.len() - 4]; - assert_eq!(Dynamic::needed(short).count(), 63); + assert_eq!(Dynamic::parse(short).rela, None); } #[test] fn everything_after_dt_null_is_ignored() { - let mut data = dynamic(&[(dynamic::DT_NEEDED, 1)]); - data.extend_from_slice(&dynamic_unterminated(&[(dynamic::DT_NEEDED, 2)])); - assert_eq!(Dynamic::needed(&data).collect::>(), vec![1]); + let mut data = dynamic(&[(dynamic::DT_RELA, 0x1000)]); + data.extend_from_slice(&dynamic_unterminated(&[(dynamic::DT_RELASZ, 48)])); + assert_eq!(Dynamic::parse(&data).rela, None); } // ── Relocation tables ─────────────────────────────────────────────────── @@ -496,28 +495,6 @@ fn a_symbol_section_whose_link_names_nothing_is_no_symbol_section() { assert!(table.symbols(SHT_DYNSYM).is_some()); } -/// `.rela.dyn` is identified by shape — a `SHT_RELA` of 24-byte entries whose -/// first is `R_X86_64_RELATIVE` — because reading section names needs -/// `.shstrtab`, which needs `e_shstrndx`, which is not in this table. -#[test] -fn rela_dyn_is_found_by_shape_and_only_by_shape() { - let bytes = [ - shdr(0, 0, 0, 0, 0), - shdr(SHT_RELA, 0x100, 24, 0, 16), // wrong entry size - shdr(SHT_RELA, 0x200, 0, 0, 24), // empty - shdr(SHT_RELA, 0x300, 48, 0, 24), // first entry is not RELATIVE - shdr(SHT_RELA, 0x400, 72, 0, 24), // this one - ] - .concat(); - let table = SectionTable::new(&bytes); - let mut reader = |off: u64| match off { - 0x300 => RelaTable::new(&rela(0, 1, 6, 0), Machine::X86_64).get(0), - 0x400 => RelaTable::new(&rela(0, 0, 8, 0), Machine::X86_64).get(0), - _ => None, - }; - assert_eq!(table.rela_dyn(&mut reader), Some((0x400, 72))); -} - /// A loader that applies `SHT_RELA` alone reaches those sections only through /// the refusal of every other relocation form: an lld kernel linked with `-z /// pack-relative-relocs` carries its relative relocations in `SHT_RELR` and diff --git a/toyos/src/relocate/mod.rs b/toyos/src/relocate/mod.rs index fe01ca7cc3e..1b24188a9a5 100644 --- a/toyos/src/relocate/mod.rs +++ b/toyos/src/relocate/mod.rs @@ -36,7 +36,7 @@ use toyos_abi::RawHandle; /// Why a program's relocations are refused. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Refusal { +enum Refusal { /// The header is no little-endian ELF64 of a machine this knows, or its /// program headers are not 56 bytes each. Header, @@ -73,7 +73,7 @@ pub enum Refusal { const REFUSED_LINE: &[u8] = b"this program's relocations are refused: it cannot start\n"; /// The status a program whose relocations are refused exits with. -pub const REFUSED_STATUS: i32 = 127; +const REFUSED_STATUS: i32 = 127; /// Apply the running image's own relocations, or end the process with /// [`REFUSED_STATUS`]. From 5b9bf9066c8364294f526e6f42bfe5e25e1ffe1c Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 13:24:11 +0200 Subject: [PATCH 06/15] File: a suite that builds its own sysroot can wedge on its own locks Seen once on this branch, on the run that first made the head's sysroot: workers holding the build lock shared waited on the sysroot key's lock while the worker that held it waited for the build lock exclusive. The same head ran 50 of 50 green once its sysroot was made. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...-its-sysroot-can-wedge-on-its-own-locks.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md diff --git a/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md new file mode 100644 index 00000000000..1c728a5b536 --- /dev/null +++ b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md @@ -0,0 +1,33 @@ +--- +status: open +kind: tooling +opened: 2026-10-10 +--- + +# A suite that builds its own sysroot can wedge on its own locks + +`src/buildlock.rs` orders a sysroot key's lock before the worktree build lock, +and takes the key's lock with the worktree lock put down. One run of the guest +suite (`cargo test --test toyos-build`, 12 wide) on a head whose sysroot no +earlier build had made held both the other way round, inside one harness +process, and stopped: + +- at 11:11:53 every worker waited on `building sysroot 697f7bee2b30245e`, and + one built it; +- at 11:14:26 the waiters acquired it in turn, one began `clean crate targets + against changed external deps` and waited for the build lock exclusive, + `held by other builds in this tree`, while workers that held it shared for a + test binary waited on `using sysroot 697f7bee2b30245e — held, but the holder + left no readable note`; +- 450 s later six tests (`https_fetch`, `iommu_virtio_platform`, + `machine_shutdown`, `nested_nmi_is_loud`, `netstack_streams`, + `netstack_streams_e1000e`) had not started, `lsof` showed the key's lock file + open in the harness alone, and the run was stopped. The same head, its + sysroot made, ran 50 of 50 green in 49 s. + +`cargo run -- --ci host` ran beside it in the same worktree; whether it took +part is not established. + +**Exit condition**: no path holds the worktree build lock while it waits on a +key's lock, and a suite started on a head whose sysroot is not yet made +finishes. From 82f74a8a421127343fbff49a50919ace0e7252e4 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:29:23 +0200 Subject: [PATCH 07/15] spawn_cost runs on the testcases boot its row reads The row judged test_rs_spawn_cost's exit, and TESTCASES never listed the job, so the T14 readback said it never ran in both arms (#836 review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/toyos.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/toyos.rs b/tests/toyos.rs index 771d6133137..75260b4478e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1037,6 +1037,7 @@ const TESTCASES: &[metal::Arm] = &[metal::once( "test_rs_audio_tone", "test_rs_hda_client_stall", "test_rs_syscall_cost", + "test_rs_spawn_cost", "test_rs_null_sink_client_exits", claims::RECLAIM, ], From b602df09f9c58fc0ef1820c16d8eaa8f8fa4b072 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:29:23 +0200 Subject: [PATCH 08/15] The relocator reads its bias off the header's link address, and refuses every tag that carries relocations Image::bias is the core's, so the -applied fixtures, linked at 0x10000000, hold the subtraction of the header's link address: at its own link address every word is lld's, 0x20000000 higher every word is lld's plus that. DT_JMPREL with no DT_PLTRELSZ, DT_RELSZ/DT_RELENT, DT_RELRSZ/DT_RELRENT, Android's packed REL/RELA and RELR tags, and AArch64's signed RELR tags (in an AArch64 image only, where those processor-specific numbers mean that) fell through and started the image with those relocations unapplied; each is refused by name now (#836 review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- toyos/src/relocate/mod.rs | 58 ++++++++++++++++++++++++++++++------- toyos/src/relocate/tests.rs | 51 +++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/toyos/src/relocate/mod.rs b/toyos/src/relocate/mod.rs index 1b24188a9a5..1888678330c 100644 --- a/toyos/src/relocate/mod.rs +++ b/toyos/src/relocate/mod.rs @@ -16,7 +16,8 @@ //! all this applies. Every other form is refused by name: a library this //! process would have to load (`DT_NEEDED`), a symbol it would have to bind //! (`DT_JMPREL` with entries, any other type), a table form it does not read -//! (`DT_REL`, `DT_RELR`), and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). +//! (`DT_REL`, every `RELR`, Android's packed tables, and every one's size), +//! and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). //! A write lands only inside a writable `PT_LOAD` and never in the `PT_TLS` //! template, which the kernel copies from the file into every thread's block //! and which a write here would never reach. @@ -47,12 +48,17 @@ enum Refusal { Dynamic, /// `DT_NEEDED`: a library this process would have to load. Needed, - /// `DT_JMPREL` with entries: slots this process would have to bind. + /// `DT_JMPREL` with entries, or with no `DT_PLTRELSZ` to say it has none: + /// slots this process would have to bind. JmpRel, - /// `DT_REL`: a table form this does not read. + /// `DT_REL` or its size or entry size: a table form this does not read. Rel, - /// `DT_RELR`: a table form this does not read. + /// `DT_RELR`, Android's or AArch64's signed one, or any of their sizes: a + /// table form this does not read. Relr, + /// Android's packed `DT_ANDROID_REL` or `DT_ANDROID_RELA`, or either's + /// size: a table form this does not read. + AndroidPacked, /// `DT_TEXTREL` or `DF_TEXTREL`: a write into text. TextRel, /// `DT_RELA` without its size, a size that is no whole number of entries, @@ -111,7 +117,7 @@ unsafe fn apply_to_self(header: *const u8) -> Result<(), Refusal> { ) }; let image = Image::new(shape, phdrs)?; - let bias = (header.addr() as u64).wrapping_sub(image.header_vaddr); + let bias = image.bias(header.addr() as u64); let Some(dynamic) = image.dynamic()? else { return Ok(()) }; let rela = { // SAFETY: inside a `PT_LOAD`'s file bytes, which the kernel mapped at @@ -158,9 +164,25 @@ const DT_RELA: u64 = 7; const DT_RELASZ: u64 = 8; const DT_RELAENT: u64 = 9; const DT_REL: u64 = 17; +const DT_RELSZ: u64 = 18; +const DT_RELENT: u64 = 19; const DT_TEXTREL: u64 = 22; +const DT_JMPREL: u64 = 23; const DT_FLAGS: u64 = 30; +const DT_RELRSZ: u64 = 35; const DT_RELR: u64 = 36; +const DT_RELRENT: u64 = 37; +const DT_ANDROID_REL: u64 = 0x6000_000f; +const DT_ANDROID_RELSZ: u64 = 0x6000_0010; +const DT_ANDROID_RELA: u64 = 0x6000_0011; +const DT_ANDROID_RELASZ: u64 = 0x6000_0012; +const DT_ANDROID_RELR: u64 = 0x6fff_e000; +const DT_ANDROID_RELRSZ: u64 = 0x6fff_e001; +const DT_ANDROID_RELRENT: u64 = 0x6fff_e003; +/// Processor-specific: these numbers mean this only in an AArch64 image. +const DT_AARCH64_AUTH_RELRSZ: u64 = 0x7000_0011; +const DT_AARCH64_AUTH_RELR: u64 = 0x7000_0012; +const DT_AARCH64_AUTH_RELRENT: u64 = 0x7000_0013; const DF_TEXTREL: u64 = 4; /// A run of the image's bytes, at its link-time address. @@ -264,6 +286,11 @@ impl<'a> Image<'a> { Ok(image) } + /// What every link-time address is moved by, for the header found at `header`. + fn bias(&self, header: u64) -> u64 { + header.wrapping_sub(self.header_vaddr) + } + fn headers(&self) -> impl Iterator + 'a { self.phdrs.chunks_exact(PHDR_SIZE).filter_map(Phdr::parse) } @@ -293,24 +320,35 @@ impl<'a> Image<'a> { /// The `DT_RELA` table `dynamic` names, or `None` when it names none. fn rela(&self, dynamic: &[u8]) -> Result, Refusal> { - let (mut rela, mut size, mut plt) = (None, None, 0); + let (mut rela, mut size, mut jmprel, mut plt) = (None, None, false, None); + let aarch64 = self.relative == R_AARCH64_RELATIVE; for entry in dynamic.chunks_exact(DYN_SIZE) { let (Some(tag), Some(value)) = (u64_at(entry, 0), u64_at(entry, 8)) else { break }; match tag { DT_NULL => break, DT_NEEDED => return Err(Refusal::Needed), - DT_PLTRELSZ => plt = value, + DT_PLTRELSZ => plt = Some(value), + DT_JMPREL => jmprel = true, DT_RELA => rela = Some(value), DT_RELASZ => size = Some(value), DT_RELAENT if value != RELA_SIZE as u64 => return Err(Refusal::RelaShape), - DT_REL => return Err(Refusal::Rel), - DT_RELR => return Err(Refusal::Relr), + DT_REL | DT_RELSZ | DT_RELENT => return Err(Refusal::Rel), + DT_RELR | DT_RELRSZ | DT_RELRENT | DT_ANDROID_RELR | DT_ANDROID_RELRSZ | DT_ANDROID_RELRENT => { + return Err(Refusal::Relr); + } + DT_AARCH64_AUTH_RELRSZ | DT_AARCH64_AUTH_RELR | DT_AARCH64_AUTH_RELRENT if aarch64 => { + return Err(Refusal::Relr); + } + DT_ANDROID_REL | DT_ANDROID_RELSZ | DT_ANDROID_RELA | DT_ANDROID_RELASZ => { + return Err(Refusal::AndroidPacked); + } DT_TEXTREL => return Err(Refusal::TextRel), DT_FLAGS if value & DF_TEXTREL != 0 => return Err(Refusal::TextRel), _ => {} } } - if plt != 0 { + // A `DT_JMPREL` whose size is missing is no table of known length. + if plt.is_some_and(|plt| plt != 0) || (jmprel && plt.is_none()) { return Err(Refusal::JmpRel); } let (vaddr, size) = match (rela, size) { diff --git a/toyos/src/relocate/tests.rs b/toyos/src/relocate/tests.rs index 3f530fb0323..b5a1c0da830 100644 --- a/toyos/src/relocate/tests.rs +++ b/toyos/src/relocate/tests.rs @@ -35,16 +35,16 @@ const AARCH64: &[u8] = include_bytes!("../../tests/relocate/aarch64.elf"); const AARCH64_APPLIED: &[u8] = include_bytes!("../../tests/relocate/aarch64-applied.elf"); const AARCH64_TLS: &[u8] = include_bytes!("../../tests/relocate/aarch64-tls.elf"); -/// The image `file` loads as, from vaddr 0, which every fixture starts at. +/// The image `file` loads as, from its header's link address. fn memory(file: &[u8]) -> Vec { let image = image_of(file); let mut memory = Vec::new(); for p in image.loads() { - let end = (p.vaddr + p.memsz) as usize; + let end = (p.vaddr - image.header_vaddr + p.memsz) as usize; if memory.len() < end { memory.resize(end, 0); } - let (at, len) = (p.vaddr as usize, p.filesz as usize); + let (at, len) = ((p.vaddr - image.header_vaddr) as usize, p.filesz as usize); memory[at..at + len].copy_from_slice(&file[p.offset as usize..p.offset as usize + len]); } memory @@ -60,7 +60,8 @@ fn image_of(file: &[u8]) -> Image<'_> { fn writes(file: &[u8], bias: u64) -> Result, Refusal> { let image = image_of(file); let memory = memory(file); - let slice = |span: Span| &memory[span.vaddr as usize..span.vaddr as usize + span.len]; + let at = |span: Span| (span.vaddr - image.header_vaddr) as usize; + let slice = |span: Span| &memory[at(span)..at(span) + span.len]; let dynamic = image.dynamic()?.expect("a PT_DYNAMIC"); let rela = image.rela(slice(dynamic))?.expect("a DT_RELA"); let mut out = Vec::new(); @@ -100,6 +101,24 @@ fn aarch64_writes_what_lld_writes() { agrees_with_lld(AARCH64, AARCH64_APPLIED, 9); } +/// The `-applied` fixtures' header is linked at [`BASE`]: the bias is where it +/// is found less that, so at its own link address nothing moves. +#[test] +fn the_bias_is_where_the_header_is_found_less_its_link_address() { + for applied in [X86_64_APPLIED, AARCH64_APPLIED] { + let image = image_of(applied); + assert_eq!(image.header_vaddr, BASE); + for found in [BASE, BASE + 0x2000_0000] { + let writes = writes(applied, image.bias(found)).expect("lld's own output applies"); + assert_eq!(writes.len(), 9); + for (vaddr, value) in writes { + let lld = word(applied, file_offset(applied, vaddr)); + assert_eq!(value, lld + (found - BASE), "the word at {vaddr:#x}, header found at {found:#x}"); + } + } + } +} + #[test] fn a_pointer_in_the_tls_template_is_refused() { // `readelf -lr x86_64-tls.elf`: `TLS 0x000478 0x2478 … 0x10` and a @@ -134,11 +153,27 @@ fn x86_tables(tags: &[(u64, u64)]) -> Result, Refusal> { fn every_dynamic_form_it_does_not_apply_is_refused_by_name() { assert_eq!(x86_tables(&[]), Ok(Some(Span { vaddr: 0x210, len: 216 }))); assert_eq!(x86_tables(&[(DT_NEEDED, 1)]), Err(Refusal::Needed)); - assert_eq!(x86_tables(&[(23, 0x210), (DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); + assert_eq!(x86_tables(&[(DT_JMPREL, 0x210), (DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); + assert_eq!(x86_tables(&[(DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); + // A `DT_JMPREL` whose size is missing is no table of known length. + assert_eq!(x86_tables(&[(DT_JMPREL, 0x210)]), Err(Refusal::JmpRel)); // A `DT_JMPREL` with no entries binds nothing. - assert!(x86_tables(&[(23, 0x210), (DT_PLTRELSZ, 0)]).is_ok()); - assert_eq!(x86_tables(&[(DT_REL, 0x210)]), Err(Refusal::Rel)); - assert_eq!(x86_tables(&[(DT_RELR, 0x210)]), Err(Refusal::Relr)); + assert!(x86_tables(&[(DT_JMPREL, 0x210), (DT_PLTRELSZ, 0)]).is_ok()); + for tag in [DT_REL, DT_RELSZ, DT_RELENT] { + assert_eq!(x86_tables(&[(tag, 0x210)]), Err(Refusal::Rel), "{tag:#x}"); + } + for tag in [DT_RELR, DT_RELRSZ, DT_RELRENT, DT_ANDROID_RELR, DT_ANDROID_RELRSZ, DT_ANDROID_RELRENT] { + assert_eq!(x86_tables(&[(tag, 0x210)]), Err(Refusal::Relr), "{tag:#x}"); + } + for tag in [DT_ANDROID_REL, DT_ANDROID_RELSZ, DT_ANDROID_RELA, DT_ANDROID_RELASZ] { + assert_eq!(x86_tables(&[(tag, 0x210)]), Err(Refusal::AndroidPacked), "{tag:#x}"); + } + // AArch64's signed `RELR`: a processor-specific number, which says so only + // in an AArch64 image. + for tag in [DT_AARCH64_AUTH_RELRSZ, DT_AARCH64_AUTH_RELR, DT_AARCH64_AUTH_RELRENT] { + assert_eq!(image_of(AARCH64).rela(&dynamic(&[(tag, 0)])), Err(Refusal::Relr), "{tag:#x}"); + assert_eq!(image_of(X86_64).rela(&dynamic(&[(tag, 0)])), Ok(None), "{tag:#x}"); + } assert_eq!(x86_tables(&[(DT_TEXTREL, 0)]), Err(Refusal::TextRel)); assert_eq!(x86_tables(&[(DT_FLAGS, DF_TEXTREL)]), Err(Refusal::TextRel)); // `DF_BIND_NOW` alone is no write into text. From e9f61b6ff9d57179b6ce9fe9b7bcc8df24bb2242 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:29:23 +0200 Subject: [PATCH 09/15] abuse_elf_loader: a std image whose own _start refuses its relocations exits 127 and says why Two copies of the test's own file, its DT_DEBUG made DT_NEEDED in one and DT_RELR in the other, spawned with slot 2 piped: exit 127 and the refusal's line. Spawned with no slot, the line names a handle the copy does not hold and the kernel ends it with its handle-fault exit, 139. A copy that reaches main panics, so a refusal that fell through cannot rerun the test. A shared-boot member, so the T14 is where it runs: a host test cannot run relocate_self's wrapper on a live image (#836 review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- .../src/bin/abuse_elf_loader.rs | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index 71f9018180d..f9252017d5e 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -10,9 +10,12 @@ //! //! An executable's dynamic section is the program's own //! (`toyos::relocate`): the spawn cases crafted in one are answered either way, -//! the kernel lives, and a child the kernel starts dies ([`dies`]). +//! the kernel lives, and a child the kernel starts dies ([`dies`]). A real std +//! image whose dynamic section names a form its own `_start` refuses exits +//! [`REFUSED_STATUS`] with [`REFUSED_LINE`] ([`refuses_itself`]). use std::fs; +use std::process::Command; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; @@ -231,7 +234,11 @@ fn write_file_in(dir: &str, name: &str, bytes: &[u8]) -> String { } fn spawn_path(path: &str) -> Result { - let argv = format!("{path}\0"); + spawn_argv(path, &format!("{path}\0")) +} + +/// `path` spawned with `argv`, NUL-separated, and no slot of this process's. +fn spawn_argv(path: &str, argv: &str) -> Result { unsafe { syscall::spawn(&SpawnArgs { path_ptr: path.as_ptr() as u64, @@ -369,6 +376,10 @@ fn base_exe(size: usize) -> Elf { } fn main() { + // A copy [`refuses_itself`] spawns that reached this far was not refused. + if std::env::args().nth(1).as_deref() == Some(REFUSED_COPY) { + panic!("a copy of this program whose relocations its _start must refuse reached main"); + } fs::create_dir_all(DIR).expect("create /tmp/abuse_loader_exe"); fs::create_dir_all(BIG_DIR).expect("create /tmp/abuse_loader"); @@ -614,6 +625,11 @@ fn main() { spawn_refused("phdrs_unmapped", &unmapped); dlopen_refused("phdrs_unmapped.so", &unmapped); + // 22. This program's own image, a library to load in one copy and a `RELR` + // table in another: its `_start` refuses both before it writes a word. + refuses_itself("needs_a_library", DT_NEEDED as u64); + refuses_itself("carries_relr", DT_RELR); + // The kernel heap is intact: allocate and touch enough to walk it, then // prove the real loader still works. let mut blocks: Vec> = Vec::new(); @@ -1025,3 +1041,55 @@ fn tls_refs_so(name: &[u8; 4], addend: i64) -> Vec { .shdr(0x1C00, SHT_DYNSYM, 0x1200, 96, 24) .build() } + +/// What `toyos::relocate` writes to slot 2 when it refuses its own program. +const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; +/// The status it exits with then. +const REFUSED_STATUS: i32 = 127; +/// `process::HANDLE_FAULT_EXIT_CODE`. +const HANDLE_FAULT: i32 = 139; +/// The argument a refused copy is handed, which its `main` panics on. +const REFUSED_COPY: &str = "--refused-copy"; + +const DT_DEBUG: u64 = 21; +const DT_RELR: u64 = 36; + +/// This program's own file with its `DT_DEBUG` entry's tag made `tag`: a std +/// image as lld linked it, whose dynamic section now names `tag` ahead of its +/// `DT_RELA`. +fn own_image_tagged(tag: u64) -> Vec { + let exe = std::env::current_exe().expect("current_exe"); + let mut bytes = fs::read(&exe).unwrap_or_else(|e| panic!("read {exe:?}: {e}")); + let word = |bytes: &[u8], at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + let phoff = word(&bytes, 32) as usize; + let phnum = usize::from(u16::from_le_bytes([bytes[56], bytes[57]])); + let dynamic = (0..phnum) + .map(|i| phoff + i * PH_SIZE) + .find(|&at| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) == PT_DYNAMIC) + .expect("lld's PT_DYNAMIC"); + let (offset, size) = (word(&bytes, dynamic + 8) as usize, word(&bytes, dynamic + 32) as usize); + let entry = (offset..offset + size).step_by(16).find(|&at| word(&bytes, at) == DT_DEBUG).expect("lld's DT_DEBUG"); + bytes[entry..entry + 8].copy_from_slice(&tag.to_le_bytes()); + bytes +} + +/// A copy of this program tagged `tag` exits [`REFUSED_STATUS`] and says +/// [`REFUSED_LINE`] on the slot 2 it is handed. Handed no slot 2, it names a +/// handle it does not hold, and the kernel ends it. +fn refuses_itself(name: &str, tag: u64) { + let path = write_file(name, &own_image_tagged(tag)); + let out = Command::new(&path) + .arg(REFUSED_COPY) + .output() + .unwrap_or_else(|e| panic!("{name}: spawn: {e}")); + let said = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(REFUSED_STATUS), "{name}: exited {:?}, said {said:?}", out.status); + assert_eq!(said, REFUSED_LINE, "{name}"); + + let child = spawn_argv(&path, &format!("{path}\0{REFUSED_COPY}\0")) + .unwrap_or_else(|e| panic!("{name} without slot 2: spawn gave {e:?}")); + // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. + let child = unsafe { toyos::process::Process::from_raw(child) }; + let code = child.wait().unwrap_or_else(|e| panic!("{name} without slot 2: wait: {e:?}")); + assert_eq!(code, HANDLE_FAULT, "{name} without slot 2"); +} From 2a65befd8de5f3b57620733e935a98031877fbd6 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:29:23 +0200 Subject: [PATCH 10/15] The static TLS block is the executable's alone: place_module and Static's first alignment go Since executables relocate themselves, build_tls_layout places one module at cursor 0, and every Static::new passed its one alignment twice. place_module and first_align are deleted with the multi-module tests that held only them; TlsModule::base_offset and tpoff's offset, 0 at every caller too, run through dlopen's path and are recorded on the track against the stage that brings the userland loader (#836 review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- kernel/src/loader/tls.rs | 9 ++-- toyos-elf/src/tls.rs | 49 ++++++++-------------- toyos-elf/tests/fuzz.rs | 3 +- toyos-elf/tests/tls.rs | 89 ++++++++++++---------------------------- 4 files changed, 50 insertions(+), 100 deletions(-) diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index 942323fe99d..dce5d39c979 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -38,7 +38,7 @@ impl TlsBlock { /// the layout. pub fn build(modules: &[TlsModule], tls: Static) -> Option { if modules.is_empty() { - let alone = Static::new(VARIANT, 0, tls.max_align(), tls.max_align())?; + let alone = Static::new(VARIANT, 0, tls.align())?; return build_combined(&[TlsModule { template: None, memsz: 0, base_offset: 0, module_id: 1, is_static: true }], alone); } build_combined(modules, tls) @@ -170,7 +170,7 @@ pub fn build_tls_layout( exe_tls_template: Option<&OwnedAlloc>, ) -> Option<(alloc::vec::Vec, Static)> { let Some(tls) = layout.tls().and_then(TlsSegment::occupied) else { - return Some((alloc::vec::Vec::new(), Static::new(VARIANT, 0, 1, 1)?)); + return Some((alloc::vec::Vec::new(), Static::new(VARIANT, 0, 1)?)); }; let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); // Variant II's executable ends at the thread pointer at its extent, not its `memsz`: @@ -179,8 +179,7 @@ pub fn build_tls_layout( Variant::II => toyos_elf::tls::exe_extent(memsz, align)?, Variant::I => memsz, }; - let (base_offset, end) = toyos_elf::tls::place_module(0, placed, align)?; let template = exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)); - let module = TlsModule { template, memsz, base_offset, module_id: 1, is_static: true }; - Some((alloc::vec![module], Static::new(VARIANT, end, align.max(1), align)?)) + let module = TlsModule { template, memsz, base_offset: 0, module_id: 1, is_static: true }; + Some((alloc::vec![module], Static::new(VARIANT, placed, align)?)) } diff --git a/toyos-elf/src/tls.rs b/toyos-elf/src/tls.rs index 46e049edac7..3377319c55f 100644 --- a/toyos-elf/src/tls.rs +++ b/toyos-elf/src/tls.rs @@ -14,8 +14,8 @@ //! rounded, ends at the thread pointer, `tls_start + total_memsz`. Variant I: //! the linker computes `TPOFF = align_up(16, p_align) + sym_offset` from the //! executable's own `PT_TLS`, so the executable's block is the first one, -//! `gap` above the thread pointer. Either way `tls_start` carries the largest -//! alignment any module asked for. Every input is a sum of numbers a file +//! `gap` above the thread pointer. Either way `tls_start` carries the +//! executable's alignment. Every input is a sum of numbers a file //! declared, which is why every step here is checked and the whole thing is a //! pure function: `dtv_bytes <= tls_start` is the property, and it used to be //! an assertion in the kernel reached from a crafted `PT_TLS`. @@ -43,19 +43,17 @@ impl Variant { } } -/// A thread's static TLS, as much of it as its layout depends on. Its -/// alignments are powers of two, or it does not exist ([`Static::new`]). +/// A thread's static TLS, the executable's module alone, as much of it as its +/// layout depends on. Its alignment is a power of two, or it does not exist +/// ([`Static::new`]). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Static { variant: Variant, - /// Every static module's bytes, placed by [`place_module`]. + /// The module's bytes: in variant II its [`exe_extent`]. total_memsz: usize, - /// The largest `p_align` any static module declared, "no constraint" as 8. - max_align: usize, - /// The `p_align` of the module at offset 0, "no constraint" as 8: variant - /// I's executable, whose linker fixed its distance from the thread pointer - /// from it. - first_align: usize, + /// Its `p_align`, "no constraint" as 8: variant I's linker fixed the + /// executable's distance from the thread pointer from it. + align: usize, } /// A planned TLS allocation, in offsets from the base of one block. @@ -73,21 +71,21 @@ pub struct TlsBlock { impl Static { /// `None` for an alignment that is not a power of two: a mask that is not /// a mask can place the data anywhere. Zero and one mean "no constraint". - pub fn new(variant: Variant, total_memsz: usize, max_align: usize, first_align: usize) -> Option { - Some(Static { variant, total_memsz, max_align: effective(max_align)?, first_align: effective(first_align)? }) + pub fn new(variant: Variant, total_memsz: usize, align: usize) -> Option { + Some(Static { variant, total_memsz, align: effective(align)? }) } pub fn total_memsz(self) -> usize { self.total_memsz } - pub fn max_align(self) -> usize { - self.max_align + pub fn align(self) -> usize { + self.align } /// No module at all: what a thread of a program without TLS is given. pub const fn empty(variant: Variant) -> Static { - Static { variant, total_memsz: 0, max_align: 8, first_align: 8 } + Static { variant, total_memsz: 0, align: 8 } } /// Lay out one thread's TLS block, or `None` for a layout no allocation @@ -99,7 +97,7 @@ impl Static { /// All three are kernel constants; everything else came out of a file. pub fn plan(self, tcb_size: usize, dtv_bytes: usize, granule: usize) -> Option { debug_assert!(granule.is_power_of_two()); - let align = self.max_align; + let align = self.align; match self.variant { Variant::II => { let block_size = self.total_memsz.checked_add(tcb_size)?; @@ -141,10 +139,10 @@ impl Static { } } - /// Variant I's distance from the thread pointer to the first module's - /// data: `align_up(16, p_align)` of that module, the linker's own. + /// Variant I's distance from the thread pointer to the module's data: + /// `align_up(16, p_align)`, the linker's own. fn gap(self) -> usize { - self.first_align.max(16) + self.align.max(16) } } @@ -169,17 +167,6 @@ impl TlsOffset { } } -/// One module's placement in a combined block: `cursor` rounded up to the -/// module's own `p_align` (psABI, not a shared constant), floored at the 16 -/// `cmpxchg16b` needs. `align` is a power of two ≤ [`crate::MAX_TLS_ALIGN`] -/// by [`crate::Layout::parse`]; `tls_start` carries the max, so a base on the -/// module's own align lands the module on it. -pub fn place_module(cursor: usize, memsz: usize, align: usize) -> Option<(usize, usize)> { - let align = align.max(16); - let base = if cursor > 0 { align_up(cursor, align)? } else { 0 }; - Some((base, base.checked_add(memsz)?)) -} - /// The bytes variant II's executable module takes below the thread pointer: its /// `p_memsz` rounded up to its `p_align`, the psABI's `tlsoffset1 = /// round(tlssize1, align1)`, which a linker bakes into every local-exec access diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index 234ad999bcc..50d1d435f3d 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -361,8 +361,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), Variant::II => tls::exe_extent(memsz, align).ok_or(())?, Variant::I => memsz, }; - let (base, total) = tls::place_module(0, placed, align).ok_or(())?; - (Static::new(variant, total, align, align).ok_or(())?, base, t.memsz()) + (Static::new(variant, placed, align).ok_or(())?, 0, t.memsz()) } None => (Static::empty(variant), 0, 0), }; diff --git a/toyos-elf/tests/tls.rs b/toyos-elf/tests/tls.rs index 566f76f2f64..98a7d71b847 100644 --- a/toyos-elf/tests/tls.rs +++ b/toyos-elf/tests/tls.rs @@ -50,7 +50,7 @@ fn the_dtv_is_never_overwritten_by_tls_data() { let aligns = [0usize, 1, 2, 8, 16, 64, 4096, 65536, GRANULE]; for &memsz in &sizes { for &align in &aligns { - let plan = Static::new(Variant::II, memsz, align, align) + let plan = Static::new(Variant::II, memsz, align) .and_then(|s| s.plan(TCB, DTV, GRANULE)) .unwrap_or_else(|| panic!("no plan for memsz {memsz} align {align}")); let effective = if align > 1 { align } else { 8 }; @@ -73,7 +73,7 @@ fn the_dtv_is_never_overwritten_by_tls_data() { #[test] fn a_size_no_allocation_can_hold_has_no_plan() { for variant in [Variant::I, Variant::II] { - let plan = |memsz, align| Static::new(variant, memsz, align, align).unwrap().plan(TCB, DTV, GRANULE); + let plan = |memsz, align| Static::new(variant, memsz, align).unwrap().plan(TCB, DTV, GRANULE); assert_eq!(plan(usize::MAX, 8), None, "{variant:?}"); assert_eq!(plan(usize::MAX - TCB, 8), None, "{variant:?}"); assert_eq!(plan(usize::MAX - GRANULE, GRANULE), None, "{variant:?}"); @@ -86,44 +86,17 @@ fn a_size_no_allocation_can_hold_has_no_plan() { fn an_alignment_that_is_not_a_power_of_two_has_no_plan() { for align in [3usize, 5, 6, 100, usize::MAX] { for variant in [Variant::I, Variant::II] { - assert_eq!(Static::new(variant, 64, align, 8), None, "align {align}"); - assert_eq!(Static::new(variant, 64, 8, align), None, "first align {align}"); + assert_eq!(Static::new(variant, 64, align), None, "align {align}"); } } } -#[test] -fn modules_are_placed_in_order_with_the_first_at_zero() { - // The first module lands at 0 whatever its align; a later one rounds up to - // its own align, floored at 16. - let (base, cursor) = tls::place_module(0, 0x48, 8).unwrap(); - assert_eq!((base, cursor), (0, 0x48)); - let (base, cursor) = tls::place_module(cursor, 0x10, 16).unwrap(); - assert_eq!((base, cursor), (0x50, 0x60)); - let (base, cursor) = tls::place_module(cursor, 0, 8).unwrap(); - assert_eq!((base, cursor), (0x60, 0x60)); - assert_eq!(tls::place_module(0x60, usize::MAX, 16), None); -} - -/// psABI variant II oracle (`std_tls`): a 160-byte lib ahead of a 64-aligned exe -/// places it at `align_up(160, 64) == 192`; the old constant 16 gave 160. -#[test] -fn a_module_lands_on_its_own_declared_alignment() { - assert_eq!(tls::place_module(160, 152, 64), Some((192, 344))); - assert_eq!(tls::place_module(160, 152, 16), Some((160, 312))); - for &align in &[0usize, 1, 2, 8, 16, 32, 64, 4096, 65536] { - let (base, _) = tls::place_module(0xA5, 8, align).unwrap(); - assert_eq!(base % align.max(16), 0, "align {align}: base {base}"); - assert!(base >= 0xA5, "align {align}: base {base} overlaps the cursor"); - } -} - /// `TPOFF64`/`TPOFF32` is `S + A - tp`: the addend is in every branch's answer. #[test] fn tpoff_carries_the_addend() { let total = 0x200usize; - let two = Static::new(Variant::II, total, 64, 64).unwrap(); - let one = Static::new(Variant::I, total, 64, 64).unwrap(); + let two = Static::new(Variant::II, total, 64).unwrap(); + let one = Static::new(Variant::I, total, 64).unwrap(); for &module_addr in &[0u64, 8, 0x40, 0x1F0] { assert_eq!(two.tpoff(0, datum(module_addr)), Some(module_addr as i64 - total as i64)); assert_eq!(one.tpoff(0, datum(module_addr)), Some(module_addr as i64 + 64)); @@ -145,7 +118,7 @@ fn tpoff_carries_the_addend() { /// overflow panic; each is now no offset at all, or no thread-pointer offset. #[test] fn a_tpoff_no_segment_or_word_holds_is_refused() { - let s = Static::new(Variant::II, 16, 8, 8).unwrap(); + let s = Static::new(Variant::II, 16, 8).unwrap(); assert_eq!(tls_offset(0, i64::MIN, 16), None); assert_eq!(tls_offset(16, i64::MAX, 16), None); // A segment as large as a file can declare: the offset exists, the @@ -153,7 +126,7 @@ fn a_tpoff_no_segment_or_word_holds_is_refused() { let huge = tls_offset(16, i64::MAX, u64::MAX).unwrap(); assert_eq!(s.tpoff(0, huge), None); assert_eq!(s.tpoff(usize::MAX, datum(1)), None); - let wide = Static::new(Variant::II, usize::MAX, 8, 8).unwrap(); + let wide = Static::new(Variant::II, usize::MAX, 8).unwrap(); assert_eq!(wide.tpoff(0, datum(0)), None); // The inclusive end is a datum: one past the segment's last byte. assert_eq!(tls_offset(8, 8, 16).map(TlsOffset::get), Some(16)); @@ -162,28 +135,25 @@ fn a_tpoff_no_segment_or_word_holds_is_refused() { /// Variant I, as lld resolves an AArch64 executable's own local-exec access /// at link time: `TPOFF = align_up(16, p_align) + offset in its PT_TLS` — -/// lld's `getTlsTpOffset`, and the AArch64 ELF ABI's 16-byte TCB. The -/// executable is the module at offset 0, so its data has to start exactly -/// that far above the thread pointer, whatever a later module asks for. +/// lld's `getTlsTpOffset`, and the AArch64 ELF ABI's 16-byte TCB: the +/// executable's data has to start exactly that far above the thread pointer. #[test] -fn variant_i_puts_the_first_module_where_its_linker_put_it() { +fn variant_i_puts_the_module_where_its_linker_put_it() { let sizes = [0usize, 1, 8, 0x48, 4096, DTV + 1, GRANULE - 1, GRANULE + 1]; let aligns = [0usize, 1, 2, 8, 16, 64, 4096, GRANULE]; for &memsz in &sizes { - for &first in &aligns { - for &max in aligns.iter().filter(|&&a| a.max(8) >= first.max(8)) { - let s = Static::new(Variant::I, memsz, max, first).unwrap(); - let plan = s.plan(TCB, DTV, GRANULE).unwrap(); - let gap = 16usize.max(first); - let at = format!("memsz {memsz} first {first} max {max}: {plan:?}"); - assert_eq!(plan.tls_start - plan.tp_offset, gap, "{at}"); - assert_eq!(s.tpoff(0, datum(0)), Some(gap as i64), "{at}"); - assert!(plan.tp_offset >= DTV, "{at}: the TCB overlaps the DTV"); - assert_eq!(plan.tp_offset % 16, 0, "{at}"); - assert_eq!(plan.tls_start % max.max(16), 0, "{at}"); - assert!(plan.tls_start + memsz <= plan.alloc_size, "{at}"); - assert_eq!(plan.alloc_size % GRANULE, 0, "{at}"); - } + for &align in &aligns { + let s = Static::new(Variant::I, memsz, align).unwrap(); + let plan = s.plan(TCB, DTV, GRANULE).unwrap(); + let gap = 16usize.max(align); + let at = format!("memsz {memsz} align {align}: {plan:?}"); + assert_eq!(plan.tls_start - plan.tp_offset, gap, "{at}"); + assert_eq!(s.tpoff(0, datum(0)), Some(gap as i64), "{at}"); + assert!(plan.tp_offset >= DTV, "{at}: the TCB overlaps the DTV"); + assert_eq!(plan.tp_offset % 16, 0, "{at}"); + assert_eq!(plan.tls_start % align.max(16), 0, "{at}"); + assert!(plan.tls_start + memsz <= plan.alloc_size, "{at}"); + assert_eq!(plan.alloc_size % GRANULE, 0, "{at}"); } } } @@ -193,7 +163,7 @@ fn variant_i_puts_the_first_module_where_its_linker_put_it() { /// `.tdata`, `#0x80` for the one at 0x40. #[test] fn variant_i_agrees_with_what_lld_linked() { - let s = Static::new(Variant::I, 0xb0, 64, 64).unwrap(); + let s = Static::new(Variant::I, 0xb0, 64).unwrap(); assert_eq!(s.tpoff(0, datum(0)), Some(0x40)); assert_eq!(s.tpoff(0x40, datum(0)), Some(0x80)); } @@ -217,13 +187,8 @@ fn the_executables_extent_is_its_size_rounded_to_its_alignment() { assert_eq!(tls::exe_extent(0x71, 0), Some(0x71)); assert_eq!(tls::exe_extent(usize::MAX, 0x40), None); - // Placed after a library, the extent is what ends at the combined size, so - // `tp - base` is the offset the linker subtracted. - let (lib_base, cursor) = tls::place_module(0, 0x78, 8).unwrap(); - assert_eq!(lib_base, 0); - let extent = tls::exe_extent(0xa8, 0x40).unwrap(); - let (exe_base, total) = tls::place_module(cursor, extent, 0x40).unwrap(); - let s = Static::new(Variant::II, total, 0x40, 8).unwrap(); - assert_eq!(s.tpoff(exe_base, datum(0)), Some(-0xc0)); - assert_eq!(exe_base % 0x40, 0); + // The extent is what ends at the thread pointer, so `tp - base` is the + // offset the linker subtracted. + let s = Static::new(Variant::II, tls::exe_extent(0xa8, 0x40).unwrap(), 0x40).unwrap(); + assert_eq!(s.tpoff(0, datum(0)), Some(-0xc0)); } From fd12ed149fcb523a5672686db4598200dc1bc2ac Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:29:24 +0200 Subject: [PATCH 11/15] Issues stop citing what executables relocating themselves deleted load_needed_libs, RelocationIndex::apply_to_page and resolve_lib_bind_relocs are gone; echo's second candidate, a page handed out half-relocated, has no mechanism left. The track records the refusal's two exits and the TLS offsets left at 0 (#836 review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...d-library-never-binds-to-the-executables-exports.md | 7 ++----- ...ot-wedges-after-a-jobs-exit-and-nothing-said-why.md | 3 ++- issues/clippy-stage-two-is-lints-one-at-a-time.md | 2 +- issues/echo-faulted-after-the-fault-arms.md | 10 ++++++---- issues/the-kernel-still-parses-what-userland-writes.md | 8 ++++++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md index 1e6eff8d8ba..3f56a76d034 100644 --- a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md +++ b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md @@ -8,13 +8,10 @@ opened: 2026-09-29 `resolve_dlopen_relocs` (`kernel/src/elf/reloc.rs`) resolves a `dlopen`ed module's `GLOB_DAT`/`JUMP_SLOT` slots against the other loaded libraries only. -Its startup sibling, `resolve_lib_bind_relocs`, tries the executable's -dynamic exports first (`kernel/src/loader/mod.rs` builds `exe_sym_map` for -it). glibc's `dlopen` searches the global scope, which starts with the +glibc's `dlopen` searches the global scope, which starts with the executable, so on ToyOS a plugin that calls back into a symbol its host exports is left with an unresolved slot and faults when it uses it. **Exit**: `resolve_dlopen_relocs` binds a slot against the executable's dynamic -exports before the other loaded libraries, as `resolve_lib_bind_relocs` does, -and a guest case `dlopen`s a library that calls a function its executable +exports before the other loaded libraries, and a guest case `dlopen`s a library that calls a function its executable exports, and the call lands. diff --git a/issues/a-t14-boot-wedges-after-a-jobs-exit-and-nothing-said-why.md b/issues/a-t14-boot-wedges-after-a-jobs-exit-and-nothing-said-why.md index 81c6188d1bc..d9ea2f67532 100644 --- a/issues/a-t14-boot-wedges-after-a-jobs-exit-and-nothing-said-why.md +++ b/issues/a-t14-boot-wedges-after-a-jobs-exit-and-nothing-said-why.md @@ -67,7 +67,8 @@ ran. **The VFS lock is not in the window.** Run 20 also shows `vfs::lock()` held across a 32 s stick write with other CPUs at 200M spins, and a spawn does take -that lock. But it takes it at `loader/mod.rs:370` and in `load_needed_libs`, both +that lock. But it took it at `loader/mod.rs:370` and in `load_needed_libs` (deleted +since: an executable relocates itself, and nothing loads a `DT_NEEDED`), both *before* the `ELF: … relocations indexed` and `spawn: TLS … modules` records that every hung boot wrote; everything after them reads the already-open backing and takes no VFS lock. So it is not this. diff --git a/issues/clippy-stage-two-is-lints-one-at-a-time.md b/issues/clippy-stage-two-is-lints-one-at-a-time.md index 7cd302c5719..8f98bc1d4b9 100644 --- a/issues/clippy-stage-two-is-lints-one-at-a-time.md +++ b/issues/clippy-stage-two-is-lints-one-at-a-time.md @@ -147,7 +147,7 @@ before adopting: | `arch/` | 107 | **adopted 2026-08-22 — the last area, and the highest-risk one.** 107 findings (112 `unsafe` blocks in all); **30 removed, 77 documented, 3 filed.** Per file below. | | root files (`user_ptr.rs`, `process.rs`, `preempt.rs`, `inbox.rs`, `main.rs`, `symbols.rs`, `hw.rs`, `file_backing.rs`, `sync.rs`, `scheduler.rs`, `pipe.rs`, `page_cache.rs`, `bcachefs_adapter.rs`) | 76 | **adopted** — 13 removed, 63 documented, 4 filed. Per file and per finding below. | | `mm/` | 35 | **adopted** — every site now carries a `SAFETY:` comment; two (`object::shm::Pages`, `mm::paging::AddressSpace`'s `Send`/`Sync` impls) turned out to be vestigial and were deleted, and `src/sourcegate.rs`'s `AUTO_TRAIT_IMPLS` now names every hand-written one the kernel keeps | -| `elf/` | 23 | **adopted** — every site now carries a `SAFETY:` comment; writing the justification found two functions (`elf::read_backing_into`, `elf::index::RelocationIndex::apply_to_page`) that write through a raw pointer without being `unsafe fn`, filed rather than fixed. Both take a `mm::KernelSlice` now (2026-08-22), so the extent is the allocation's and the file is closed. | +| `elf/` | 23 | **adopted** — every site now carries a `SAFETY:` comment; writing the justification found two functions (`elf::read_backing_into`, `elf::index::RelocationIndex::apply_to_page`) that write through a raw pointer without being `unsafe fn`, filed rather than fixed. Both took a `mm::KernelSlice` (2026-08-22), so the extent is the allocation's and the file is closed; the second is deleted since, with the fault-time relocation index. | | `sched/` | 8 | **adopted 2026-08-22**, under the reduction ruling. 8 findings (26 `unsafe` blocks in all); **4 removed, 5 documented, 1 filed** — the five sites of the same shape outside this area, since taken by the root-file and `arch/` sweeps, so that file is closed too. | | `loader/` | 8 | **adopted** — every site now carries a `SAFETY:` comment (one finding shared with `elf/`, above) | | `iommu/` | 8 | **adopted 2026-08-22**, under the reduction ruling. 8 findings (8 `unsafe` blocks in all); **5 removed, 3 documented, 0 filed** — the area's whole remainder is two window constructions and a `clflush`. | diff --git a/issues/echo-faulted-after-the-fault-arms.md b/issues/echo-faulted-after-the-fault-arms.md index fbda35f47fe..878171ffe11 100644 --- a/issues/echo-faulted-after-the-fault-arms.md +++ b/issues/echo-faulted-after-the-fault-arms.md @@ -38,10 +38,12 @@ explained either way: it is a userland fault the kernel handled correctly. **What to read if it happens again.** `err=0x4` is a user-mode read of a not-present page, so it is `/system/bin/echo`'s own dereference of 1 and not the -loader's. Two candidates and one run cannot separate them: a `Command::output()` -path whose child inherits something the fault arms disturbed, and demand paging -handing the binary a page it had not finished relocating (`relocs=2600` is on the -trace). Capture the guest with QMP rather than letting the harness kill it — +loader's. One run named two candidates: a `Command::output()` path whose child +inherits something the fault arms disturbed, and demand paging handing the binary +a page it had not finished relocating (`relocs=2600` is on the trace). The second +is gone from the tree: an executable applies every relocation in its own `_start` +before it reads a word (`toyos::relocate`), the fault path relocates nothing, and +the trace carries no `relocs=`. Capture the guest with QMP rather than letting the harness kill it — `BootOptions { qmp: true }` leaves the socket, and `info registers -a` says whether any other CPU is still running. diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index a3aac811880..a3df88ccba9 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -29,10 +29,14 @@ executable's dynamic section, symbol tables or relocations, and loads no library at spawn. Left in Ring 0: `dlopen` and its four sibling syscalls, the TLS block, and the program-header parse. **The interval it opens, owed by the stage that brings the userland loader:** an executable with a `DT_NEEDED` -does not start (it is refused at its own start, exit 127), so `std_tls`, the +does not start (its own `_start` refuses it: exit 127 and a line on slot 2, +or, holding no slot 2, the kernel's handle-fault exit), so `std_tls`, the guest test of a startup library's TLS beside the executable's, is deleted at the commit that lands stage 1 and comes back with that stage. Nothing shipped -links a library at load time. +links a library at load time. The kernel's static TLS block holds the +executable's module alone, at offset 0, so `TlsModule::base_offset` and +`Static::tpoff`'s offset are 0 at every caller; that stage places modules in +userland and deletes them with the kernel's TLS build. The evidence that it is worth doing: nine dated commits over eleven days fixed userland-reachable kernel defects in this code, six of them machine-wide panics; From aabf349f3a3f2fc89f77ce94a26b25740ded645b Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 15:35:45 +0200 Subject: [PATCH 12/15] spawn_cost spawns toybox, which the testcases ROOT ships /system/bin/shell is on no tests/testcases ROOT, so the T14 readings of fd12ed149 panicked the job with `spawn /system/bin/shell: entity not found` in every arm and measured nothing. toybox is a [programs] row of that config and, of the std programs on that ROOT, carries the most RELATIVE entries (x86-64, readelf -d RELACOUNT: toybox 1518, fileserver 1422, supervisor 1347, acpiserver 1050). `toybox echo` with stdout to null prints a line to nowhere and exits 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/toyos-rust-tests/src/bin/spawn_cost.rs | 25 +++++++++++++------- tests/toyos.rs | 6 ++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/spawn_cost.rs b/tests/toyos-rust-tests/src/bin/spawn_cost.rs index 8af013e11af..27015569b0b 100644 --- a/tests/toyos-rust-tests/src/bin/spawn_cost.rs +++ b/tests/toyos-rust-tests/src/bin/spawn_cost.rs @@ -1,7 +1,9 @@ -//! What starting a program costs: `/system/bin/shell -c ''` spawned and waited -//! for. It returns as soon as it has started, so what the clock reads is the -//! kernel's spawn, the program's own start, its relocation among it, and its -//! exit. +//! What starting a program costs: `/system/bin/toybox echo` spawned and waited +//! for, its line to nowhere. It returns as soon as it has started, so what the +//! clock reads is the kernel's spawn, the program's own start, its relocation +//! among it, and its exit. `toybox` because it is on the ROOT of the boot this +//! runs on, `tests/testcases`, and carries the most `RELATIVE` entries of what +//! that ROOT does. //! //! **No threshold, and the host reads the numbers instead**, as //! `syscall_cost`'s: a duration measured under TCG is no duration, and one @@ -9,18 +11,23 @@ //! tree. The minimum and the median over repetitions, after a first spawn that //! reads the file's pages off the device. -use std::process::Command; +use std::process::{Command, Stdio}; use toyos_abi::clock::nanos_since_boot as clock_nanos; -const SHELL: &str = "/system/bin/shell"; +const PROGRAM: &str = "/system/bin/toybox"; const REPS: usize = 50; fn spawn_ns() -> u64 { let start = clock_nanos(); - let status = Command::new(SHELL).args(["-c", ""]).status().unwrap_or_else(|e| panic!("spawn {SHELL}: {e}")); + let status = Command::new(PROGRAM) + .arg("echo") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .status() + .unwrap_or_else(|e| panic!("spawn {PROGRAM}: {e}")); let end = clock_nanos(); - assert!(status.success(), "{SHELL} -c '' exited {status:?}"); + assert!(status.success(), "{PROGRAM} echo exited {status:?}"); end - start } @@ -28,5 +35,5 @@ fn main() { spawn_ns(); let mut ns: Vec = (0..REPS).map(|_| spawn_ns()).collect(); ns.sort_unstable(); - println!("spawn_cost: {SHELL} min {} us median {} us over {REPS}", ns[0] / 1000, ns[REPS / 2] / 1000); + println!("spawn_cost: {PROGRAM} min {} us median {} us over {REPS}", ns[0] / 1000, ns[REPS / 2] / 1000); } diff --git a/tests/toyos.rs b/tests/toyos.rs index e6f55e8d0b7..ea5a05ed299 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -80,7 +80,7 @@ const RUST_SKIP: &[&str] = &[ // Its product is a cycle count per syscall and the clock rate beside it, // which a guest's host sets: the `syscall_cost` metal row runs it. "syscall_cost", - // Its product is how long a spawn of `shell` takes to its exit, which a + // Its product is how long a spawn of `toybox` takes to its exit, which a // guest's host sets: the `spawn_cost` metal row runs it. "spawn_cost", // Its verdict is a duration: whether a shootdown waits for every other CPU @@ -613,7 +613,7 @@ const METAL: &[(&str, metal::Metal)] = &[ }, ), ( - // How long a spawn of `shell` takes to its exit, printed by the job and + // How long a spawn of `toybox` takes to its exit, printed by the job and // read off the stick for an A/B against another build: that it ran, and // what it said, are the verdict. "spawn_cost", @@ -621,7 +621,7 @@ const METAL: &[(&str, metal::Metal)] = &[ arms: TESTCASES, judge: |b| { b[0].job_passed("test_rs_spawn_cost")?; - b[0].log().must_say("spawn_cost: /system/bin/shell min ")?; + b[0].log().must_say("spawn_cost: /system/bin/toybox min ")?; Ok(()) }, }, From 5483c3acab6b7fa12195d8fa1dd2375077d29205 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:31:48 +0200 Subject: [PATCH 13/15] A file offset is only a segment's file bytes: the PT_TLS template's whole range must lie in one `vaddr_to_file_offset` extrapolated from the nearest segment below an address past every segment's file bytes, for `DT_*` tags that this branch no longer reads. Its one remaining caller was the kernel's `PT_TLS` template read, where the extrapolation turned a template starting in `.bss` into a read of another segment's bytes or of bytes past the end of the file. `Layout::file_offset_of` now takes the template's `ImageRange` and answers only when one segment's file bytes hold all of it; `vaddr_to_file_offset`, the extrapolation and the crafted tests that pinned it are deleted. The new `a_tls_template_starting_in_bss_has_no_file_offset` is red (exit 101) with the old fallback restored. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- kernel/src/loader/mod.rs | 4 ++-- toyos-elf/src/layout.rs | 47 +++++++------------------------------- toyos-elf/tests/crafted.rs | 39 ++++++++++++++++++------------- toyos-elf/tests/real.rs | 7 +++--- 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 53c13e1fc7e..782cfa81a97 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -268,8 +268,8 @@ pub fn spawn( let exe_tls_template = match layout.tls().and_then(TlsSegment::occupied) { Some(tls) => { - let Some(tls_file_off) = layout.file_offset_of(tls.template().start()) else { - log!("spawn: {}: PT_TLS is in or near no PT_LOAD segment", path); + let Some(tls_file_off) = layout.file_offset_of(tls.template()) else { + log!("spawn: {}: PT_TLS's template is in no PT_LOAD's file bytes", path); return Err(SyscallError::InvalidArgument.into()); }; // Read directly into the `memsz`-sized buffer: `OwnedAlloc` zeroes diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index d3f850aea06..dc5cb0bb968 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -535,45 +535,14 @@ impl Layout { None } - /// The file offset a virtual address maps to. - /// - /// Falls back to extrapolating from the nearest segment at or below - /// `vaddr`, which is what `.rela.dyn` and friends need when the linker - /// places them past a segment's file-backed extent. - /// - /// `None` when there is no segment at or below `vaddr` to extrapolate - /// from, or when the extrapolation overflows. Every `vaddr` asked here is a - /// `DT_*` tag, so the answer to "this address is in no segment" is that the - /// binary is malformed, not that the kernel dies. - pub fn vaddr_to_file_offset(&self, vaddr: u64) -> Option { - let into = |seg: &Segment| vaddr.checked_sub(self.seg_vaddr(seg)); - for seg in self.segments() { - if let Some(within) = into(seg).filter(|&w| w < seg.filesz) { - return seg.file_offset.checked_add(within); - } - } - let mut best: Option<(&Segment, u64)> = None; - for seg in self.segments() { - if let Some(within) = into(seg) { - if best.is_none_or(|(_, w)| within < w) { - best = Some((seg, within)); - } - } - } - let (seg, within) = best?; - seg.file_offset.checked_add(within) - } - - /// The file offset of an image offset this layout handed out, as - /// [`vaddr_to_file_offset`](Self::vaddr_to_file_offset) maps it. - pub fn file_offset_of(&self, at: ImageOffset) -> Option { - self.vaddr_to_file_offset(self.extent.min.checked_add(at.get())?) - } - - /// A segment's `p_vaddr`, back out of its image offset: inside the extent, - /// so the sum cannot overflow. - fn seg_vaddr(&self, seg: &Segment) -> u64 { - self.extent.min.wrapping_add(seg.image.start) + /// Where the file holds `range`, or `None` when no one segment's file + /// bytes hold all of it: past `p_filesz` a segment is zeroes, not the file. + pub fn file_offset_of(&self, range: ImageRange) -> Option { + let seg = self.segments().iter().find(|seg| { + seg.image.start <= range.start && range.end().get() <= seg.image.start.wrapping_add(seg.filesz) + })?; + // `p_offset + p_filesz` fits a `u64`, and `range` lies inside both. + Some(seg.file_offset.wrapping_add(range.start.wrapping_sub(seg.image.start))) } } diff --git a/toyos-elf/tests/crafted.rs b/toyos-elf/tests/crafted.rs index bbf429dcd27..d683f303717 100644 --- a/toyos-elf/tests/crafted.rs +++ b/toyos-elf/tests/crafted.rs @@ -15,7 +15,7 @@ mod common; use common::*; use toyos_elf::header::PROGRAM_HEADER_SIZE; -use toyos_elf::{Error, Layout, Machine, StackedImage, MAX_LOAD_SEGMENTS}; +use toyos_elf::{Error, ImageRange, Layout, Machine, StackedImage, MAX_LOAD_SEGMENTS}; fn refused(bytes: Vec) -> Error { match Layout::parse(&bytes, Machine::X86_64) { @@ -302,32 +302,39 @@ fn segments_that_share_a_page_are_an_overlap() { assert_eq!(huge.overlapping_load_pages(4096), None); } +/// `[vaddr, vaddr + len)` in `layout`'s image, which the caller knows it is. +fn range(layout: &Layout, vaddr: u64, len: u64) -> ImageRange { + layout.extent().range(vaddr, len).expect("inside the image") +} + #[test] -fn a_vaddr_below_every_segment_has_no_file_offset() { +fn only_a_segments_file_bytes_have_a_file_offset() { let layout = accepted( Elf::new(0x4000) - .ph(Phdr::load(0x1000, 0x1000, 0x3000, 0x3000, PF_R | PF_W)) - .entry(0x1000) + .ph(Phdr::load(0, 0, 0x1000, 0x1000, PF_R | PF_X)) + .ph(Phdr::load(0x2000, 0x2000, 0x800, 0x3000, PF_R | PF_W)) .build(), ); - assert_eq!(layout.vaddr_to_file_offset(0), None); - assert_eq!(layout.vaddr_to_file_offset(0xFFF), None); - assert_eq!(layout.vaddr_to_file_offset(0x1000), Some(0x1000)); - assert_eq!(layout.vaddr_to_file_offset(0x2000), Some(0x2000)); - // Past the file image of the only segment, extrapolated — what `.rela.dyn` - // needs when the linker places it outside any `PT_LOAD`. - assert_eq!(layout.vaddr_to_file_offset(0x9000), Some(0x9000)); + assert_eq!(layout.file_offset_of(range(&layout, 0x2000, 0x800)), Some(0x2000)); + assert_eq!(layout.file_offset_of(range(&layout, 0x2800, 0)), Some(0x2800)); + // Between the segments, in the second's `.bss`, and across its file end. + assert_eq!(layout.file_offset_of(range(&layout, 0x1800, 8)), None); + assert_eq!(layout.file_offset_of(range(&layout, 0x2800, 8)), None); + assert_eq!(layout.file_offset_of(range(&layout, 0x27F8, 0x10)), None); } +/// A template in `.bss` names no file bytes: the file offset its distance +/// into the segment gives is another segment's. #[test] -fn an_extrapolated_file_offset_that_overflows_has_no_answer() { +fn a_tls_template_starting_in_bss_has_no_file_offset() { let layout = accepted( - Elf::new(0x1000) - .ph(Phdr::load(u64::MAX - 0x1000, 0, 0x1000, 0x1000, PF_R)) + Elf::new(0x2000) + .ph(Phdr::load(0, 0, 0x1000, 0x2000, PF_R | PF_W)) + .ph(Phdr::load(0x1800, 0x3800, 0x800, 0x800, PF_R)) + .ph(Phdr::tls(0x1800, 0x100, 0x100, 8)) .build(), ); - assert_eq!(layout.vaddr_to_file_offset(0), Some(u64::MAX - 0x1000)); - assert_eq!(layout.vaddr_to_file_offset(0x2000), None); + assert_eq!(layout.file_offset_of(layout.tls().unwrap().template()), None); } /// `(image offset, bytes, count)` of the table a layout places, for comparing. diff --git a/toyos-elf/tests/real.rs b/toyos-elf/tests/real.rs index 8cf5da76f0f..cb7cf97cd73 100644 --- a/toyos-elf/tests/real.rs +++ b/toyos-elf/tests/real.rs @@ -50,9 +50,10 @@ fn a_linked_binary_parses_to_what_readelf_says() { // No two segments contend for a page, and a vaddr resolves to its file // offset — the two derived answers `spawn` refuses a binary over. assert_eq!(layout.overlapping_load_pages(4096), None); - assert_eq!(layout.vaddr_to_file_offset(0xc0620), Some(0xbe620)); - assert_eq!(layout.vaddr_to_file_offset(0xc1800), Some(0xbe800)); - assert_eq!(layout.vaddr_to_file_offset(0x2a8), Some(0x2a8)); + let file_offset = |vaddr| layout.file_offset_of(layout.extent().range(vaddr, 1).unwrap()); + assert_eq!(file_offset(0xc0620), Some(0xbe620)); + assert_eq!(file_offset(0xc1800), Some(0xbe800)); + assert_eq!(file_offset(0x2a8), Some(0x2a8)); } /// The build-id `object` reads out of `file`'s program headers. From b874b2a1f19c08fe3a3920fb8678d8d1c4e30c53 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:51:19 +0200 Subject: [PATCH 14/15] The sysroot wedge recurred on round 4's guest suite; its issue records it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...e-that-builds-its-sysroot-can-wedge-on-its-own-locks.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md index 1c728a5b536..b2a2daf7b10 100644 --- a/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md +++ b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md @@ -28,6 +28,13 @@ process, and stopped: `cargo run -- --ci host` ran beside it in the same worktree; whether it took part is not established. +It recurred on #836's round 4, the same shape on the host the other agents' +suites loaded: the harness built `fa7a35ac0091a05b`'s std by 14:35:56, ran its +other tests, and from 14:43:03 one worker alone waited on `using sysroot +fa7a35ac0091a05b — the holder left no readable note`, 930 s by 14:48:17, with +`lsof` showing the key's lock file open twice in the harness and nowhere else. +`--ci host` again ran beside it and had finished at 14:45:47. + **Exit condition**: no path holds the worktree build lock while it waits on a key's lock, and a suite started on a head whose sysroot is not yet made finishes. From edcc8a230dc18bfcd772e259c38925d926c5c998 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 16:52:54 +0200 Subject: [PATCH 15/15] Two guest-test comments stop citing vaddr_to_file_offset and its extrapolation, which are deleted Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs | 3 +-- tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index f9252017d5e..85e3d02dd02 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -383,8 +383,7 @@ fn main() { fs::create_dir_all(DIR).expect("create /tmp/abuse_loader_exe"); fs::create_dir_all(BIG_DIR).expect("create /tmp/abuse_loader"); - // 1. A DT_* vaddr below every PT_LOAD, which `vaddr_to_file_offset` panicked - // on outright when the kernel read an executable's tables. + // 1. A DT_* vaddr below every PT_LOAD. spawn_dies( "rela_below_image", &Elf::new(0x4000) diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs index f1d5820ee93..7eb8f006d7f 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs @@ -199,8 +199,7 @@ fn main() { "PT_TLS memsz = u64::MAX/2 gave {err:?}", ); - // 3. PT_TLS vaddr far outside every PT_LOAD — the file offset it - // extrapolates to is not in the image. + // 3. PT_TLS vaddr far outside every PT_LOAD. let err = spawn_err("tls_vaddr", &elf(Phdr { kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x8000_0000, filesz: 8, memsz: 16, align: 8,