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-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-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-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..b2a2daf7b10 --- /dev/null +++ b/issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md @@ -0,0 +1,40 @@ +--- +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. + +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. 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/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/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/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/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 4574801b669..a3df88ccba9 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -23,6 +23,21 @@ 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 (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. 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; 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..782cfa81a97 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, 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. //! -//! 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, @@ -516,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 @@ -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..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) @@ -159,52 +159,27 @@ 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)?)); }; - 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 template = exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)); + 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/kernel/src/process.rs b/kernel/src/process.rs index c72a4362d55..78bfe36fbe2 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -458,8 +458,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 } @@ -475,8 +474,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, @@ -511,8 +509,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), @@ -537,7 +533,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, @@ -1063,7 +1058,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, @@ -1428,14 +1422,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. @@ -1475,23 +1468,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( @@ -1534,7 +1510,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, @@ -1574,15 +1549,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/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", diff --git a/tests/checks.rs b/tests/checks.rs index 9905dc25759..be93c1cc746 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..85e3d02dd02 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,23 @@ //! 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`]). 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; /// Where the child starts: `SpawnArgs` names a working directory or the spawn is refused. const CWD: &str = "/"; @@ -226,8 +233,12 @@ fn write_file_in(dir: &str, name: &str, bytes: &[u8]) -> String { path } -fn spawn_path(path: &str) -> Result { - let argv = format!("{path}\0"); +fn spawn_path(path: &str) -> Result { + 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, @@ -249,18 +260,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 +290,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 +314,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 +322,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, @@ -354,13 +376,15 @@ 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"); - // 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. + spawn_dies( "rela_below_image", &Elf::new(0x4000) .ph(Phdr::load(0, 0x1000, 0x3000, 0x3000, PF_R | PF_W)) @@ -449,15 +473,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 +487,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 +518,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 +541,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 +563,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 +572,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 +610,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. @@ -620,6 +624,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(); @@ -748,36 +757,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 +921,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 +943,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 +961,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 +974,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 @@ -1039,3 +1040,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"); +} 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, 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/spawn_cost.rs b/tests/toyos-rust-tests/src/bin/spawn_cost.rs new file mode 100644 index 00000000000..27015569b0b --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/spawn_cost.rs @@ -0,0 +1,39 @@ +//! 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 +//! 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, Stdio}; + +use toyos_abi::clock::nanos_since_boot as clock_nanos; + +const PROGRAM: &str = "/system/bin/toybox"; +const REPS: usize = 50; + +fn spawn_ns() -> u64 { + let start = clock_nanos(); + 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(), "{PROGRAM} echo exited {status:?}"); + end - start +} + +fn main() { + spawn_ns(); + let mut ns: Vec = (0..REPS).map(|_| spawn_ns()).collect(); + ns.sort_unstable(); + println!("spawn_cost: {PROGRAM} min {} us median {} us over {REPS}", ns[0] / 1000, ns[REPS / 2] / 1000); +} 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/tests/toyos.rs b/tests/toyos.rs index 001638fa132..319b330c8d0 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 `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 // is read off the clock around the syscall. The `tlb_shootdown_waits` // metal row runs it on the T14. @@ -617,6 +620,20 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // 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", + metal::Metal { + arms: TESTCASES, + judge: |b| { + b[0].job_passed("test_rs_spawn_cost")?; + b[0].log().must_say("spawn_cost: /system/bin/toybox 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, @@ -1028,6 +1045,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, ], 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..dc5cb0bb968 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -535,62 +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())?) - } - - /// 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 { - 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/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/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/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/crafted.rs b/toyos-elf/tests/crafted.rs index 4d44324f914..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,49 +302,39 @@ fn segments_that_share_a_page_are_an_overlap() { assert_eq!(huge.overlapping_load_pages(4096), None); } -#[test] -fn a_vaddr_below_every_segment_has_no_file_offset() { - let layout = accepted( - Elf::new(0x4000) - .ph(Phdr::load(0x1000, 0x1000, 0x3000, 0x3000, PF_R | PF_W)) - .entry(0x1000) - .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)); +/// `[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 an_extrapolated_file_offset_that_overflows_has_no_answer() { +fn only_a_segments_file_bytes_have_a_file_offset() { let layout = accepted( - Elf::new(0x1000) - .ph(Phdr::load(u64::MAX - 0x1000, 0, 0x1000, 0x1000, PF_R)) + Elf::new(0x4000) + .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), Some(u64::MAX - 0x1000)); - assert_eq!(layout.vaddr_to_file_offset(0x2000), None); + 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); } -/// `.gnu.hash` declares its length nowhere, so its bound is the file image of -/// the segment holding it. +/// A template in `.bss` names no file bytes: the file offset its distance +/// into the segment gives is another segment's. #[test] -fn the_file_bytes_behind_a_vaddr_are_the_containing_segments() { +fn a_tls_template_starting_in_bss_has_no_file_offset() { 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)) + 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.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)); + 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/fuzz.rs b/toyos-elf/tests/fuzz.rs index dd608e25fc7..50d1d435f3d 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(|_| ())? { @@ -386,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), }; @@ -474,10 +448,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/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. diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index b0c7967f073..db34919df26 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]; @@ -78,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 ─────────────────────────────────────────────────── @@ -120,25 +117,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 +204,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 +213,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 +232,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 +250,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| { @@ -558,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 @@ -746,7 +661,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-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)); } diff --git a/toyos/src/lib.rs b/toyos/src/lib.rs index 89d6799526d..6e5c7567723 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..1888678330c --- /dev/null +++ b/toyos/src/relocate/mod.rs @@ -0,0 +1,401 @@ +//! 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`, 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. + +#![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)] +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, or with no `DT_PLTRELSZ` to say it has none: + /// slots this process would have to bind. + JmpRel, + /// `DT_REL` or its size or entry size: a table form this does not read. + Rel, + /// `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, + /// 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. +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 = 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 + // `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_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. +#[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) + } + + /// 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) + } + + 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 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 = 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 | 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), + _ => {} + } + } + // 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) { + (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..b5a1c0da830 --- /dev/null +++ b/toyos/src/relocate/tests.rs @@ -0,0 +1,309 @@ +//! 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 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 - image.header_vaddr + p.memsz) as usize; + if memory.len() < end { + memory.resize(end, 0); + } + 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 +} + +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 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(); + 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); +} + +/// 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 + // `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(&[(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(&[(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. + 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 00000000000..9c7e01bef13 Binary files /dev/null and b/toyos/tests/relocate/aarch64-applied.elf differ diff --git a/toyos/tests/relocate/aarch64-tls.elf b/toyos/tests/relocate/aarch64-tls.elf new file mode 100755 index 00000000000..775c07306b6 Binary files /dev/null and b/toyos/tests/relocate/aarch64-tls.elf differ diff --git a/toyos/tests/relocate/aarch64.elf b/toyos/tests/relocate/aarch64.elf new file mode 100755 index 00000000000..a7f10fe29cf Binary files /dev/null and b/toyos/tests/relocate/aarch64.elf differ 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 00000000000..111fb0944e7 Binary files /dev/null and b/toyos/tests/relocate/x86_64-applied.elf differ diff --git a/toyos/tests/relocate/x86_64-tls.elf b/toyos/tests/relocate/x86_64-tls.elf new file mode 100755 index 00000000000..a6ba5e8a4bc Binary files /dev/null and b/toyos/tests/relocate/x86_64-tls.elf differ diff --git a/toyos/tests/relocate/x86_64.elf b/toyos/tests/relocate/x86_64.elf new file mode 100755 index 00000000000..49a7b42cc87 Binary files /dev/null and b/toyos/tests/relocate/x86_64.elf differ 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, ); }