From e5af35824b4b66a572055c0f62a23dffa0f99b54 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 16:15:36 +0200 Subject: [PATCH 01/10] loader: parse every ELF value into a type that cannot leave its image Every number the kernel's loader takes from an ELF file now reaches it only through toyos-elf types with private fields, each made by a parse that checks it against the image or TLS segment it must name: - `Rela` exposes only its kind; `rela::parse` against `Rules` yields a `Reloc` whose `RELATIVE` value is an `ImageOffset` inside the image, whose symbol is a `SymIndex` below the table, and whose `r_sym == 0` TLS offset is a `TlsOffset` inside PT_TLS (E1, E2). `validate` is gone. - `Sym::address` and `Sym::tls_offset` are the only ways to read `st_value`; `SymTab::bounded` refuses a library any defined symbol of which names nothing in it (E3). - `InitArray::parse` bounds DT_INIT_ARRAY to whole pointers inside the image (E4). - `Static::tpoff` is checked and answers `None`; TPOFF32 refuses a value its 32 bits cannot hold. - `Layout` hands out `Extent`, `ImageOffset` and `ImageRange` instead of public raw vaddrs. The kernel consumes those types: RELATIVE slots, symbol addresses, the init array and every TPOFF/DTPOFF are image start plus a checked offset, and a library's rebase recomputes each RELATIVE slot from its parsed target rather than reading the slot back. `rela::tables_outside_window` rounds the window out to the page, so the tables a library's loader parses again after mapping sit on no page the process can write. C1: a thread's TLS block is built in `Unpublished` frames and rebased inside `Unpublished::publish`, which picks the address and maps it under one address-space lock hold; the DTV walk is bounded by DTV_INITIAL_CAPACITY, never by the block's length word. A seeded host fuzzer over the value path (toyos-elf/tests/fuzz.rs) drives 1,000,000 image loads per run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- bootloader/src/main.rs | 94 +++---- kernel/src/elf/cache.rs | 60 ++--- kernel/src/elf/index.rs | 104 +++++--- kernel/src/elf/mod.rs | 181 ++++++++----- kernel/src/elf/reloc.rs | 243 ++++++++++------- kernel/src/loader/mod.rs | 218 +++++++-------- kernel/src/loader/symbols.rs | 39 ++- kernel/src/loader/tls.rs | 140 +++++----- kernel/src/mm/region.rs | 8 +- kernel/src/process.rs | 91 ++++--- kernel/src/syscall/vm.rs | 50 ++-- src/build.rs | 9 +- toyos-elf/src/dynamic.rs | 35 ++- toyos-elf/src/layout.rs | 475 ++++++++++++++++++++++----------- toyos-elf/src/lib.rs | 31 ++- toyos-elf/src/rela.rs | 300 ++++++++++++++------- toyos-elf/src/section.rs | 2 +- toyos-elf/src/sym.rs | 111 ++++++-- toyos-elf/src/tls.rs | 38 ++- toyos-elf/tests/crafted.rs | 22 +- toyos-elf/tests/fuzz.rs | 498 +++++++++++++++++++++++++++++++++++ toyos-elf/tests/real.rs | 24 +- toyos-elf/tests/tables.rs | 134 +++++++--- toyos-elf/tests/tls.rs | 46 +++- 24 files changed, 2079 insertions(+), 874 deletions(-) create mode 100644 toyos-elf/tests/fuzz.rs diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index bec945c9469..d9fae4f92d5 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -8,7 +8,7 @@ use core::mem; use alloc::vec; use alloc::alloc::Layout; use toyos_elf::section::SectionTable; -use toyos_elf::{RelaTable, RelocKind}; +use toyos_elf::{rela, ImageOffset, Op, RelaTable}; use uefi::{ prelude::*, CStr16, @@ -344,7 +344,7 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { // make the image runnable, so a file with no readable table is refused // rather than started unrelocated. let sections = layout - .section_headers + .section_headers() .and_then(|table| file_range(kernel_elf_bytes, table.file_offset, table.byte_len() as u64)) .map(SectionTable::new) .expect("kernel.elf: no section header table inside the file"); @@ -352,12 +352,16 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { let stack_size: usize = 8 * 1024 * 1024; // 8MB println!("Kernel stack size: {}", stack_size); - // `vaddr_max` is the largest `p_vaddr + p_memsz` over the `PT_LOAD` + // The extent's end is the largest `p_vaddr + p_memsz` over the `PT_LOAD` // segments, and the image is laid out at its own vaddrs — so it is what the // kernel's memory has to cover before the stack is added after it. - let placed = toyos_elf::StackedImage::place(layout.vaddr_max, stack_size as u64) + let extent = layout.extent(); + let placed = toyos_elf::StackedImage::place(extent.max(), stack_size as u64) .expect("kernel.elf: image plus stack does not fit an allocation"); let mem_size = usize::try_from(placed.size).expect("kernel.elf: image plus stack does not fit an allocation"); + // Where an image offset lies in `process_mem`: the image sits at its own + // vaddrs, which begin at the extent's start. + let at = |offset: ImageOffset| (extent.min() + offset.get()) as usize; println!("Kernel memory size: {}", mem_size); @@ -366,9 +370,9 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { for segment in layout.segments() { println!("Loading segment: {:?}", segment); - let src = file_range(kernel_elf_bytes, segment.file_offset, segment.filesz) + let src = file_range(kernel_elf_bytes, segment.file_offset(), segment.filesz()) .expect("kernel.elf: PT_LOAD file extent is past the end of the file"); - let vstart = segment.vaddr as usize; + let vstart = at(segment.image().start()); // In bounds by construction: `mem_size` is at least // `p_vaddr + p_memsz` for this segment and `p_filesz <= p_memsz`. process_mem[vstart..vstart + src.len()].copy_from_slice(src); @@ -377,58 +381,54 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { let rela_sections = sections.rela_sections().unwrap_or_else(|form| panic!("kernel.elf: {form} is not supported")); + // Both fields of a relocation index the image and both come out of the + // file: `r_offset` is the destination of an 8-byte store and `r_addend` the + // address stored. Unchecked, the store is an arbitrary write anywhere in the + // machine, made before ExitBootServices with firmware still live — so every + // entry goes through the parse the kernel's own loader uses. + let rules = rela::Rules { + extent, + window: (0, mem_size as u64), + sym_count: 0, + fill: None, + tls_memsz: None, + }; let mut reloc_count = 0u64; for section in rela_sections { let table = file_range(kernel_elf_bytes, section.offset, section.size) .expect("kernel.elf: SHT_RELA section is past the end of the file"); - for rela in RelaTable::new(table, arch::ELF_MACHINE).iter() { - match rela.kind { - RelocKind::Relative => { - // Both fields index the image and both come out of the - // file: `r_offset` is the destination of an 8-byte store - // and `r_addend` is the address stored. Unchecked, the - // store is an arbitrary write anywhere in the machine, made - // before ExitBootServices with firmware still live. - let offset = rela.offset; - let addend = rela.addend; - assert!( - offset.checked_add(8).is_some_and(|end| end <= mem_size as u64), - "kernel.elf: relocation stores 8 bytes at {offset:#x}, outside the {mem_size:#x}-byte image" - ); - assert!( - (0..=mem_size as i64).contains(&addend), - "kernel.elf: relocation addend {addend:#x} is outside the {mem_size:#x}-byte image" - ); - // SAFETY: `addend` is asserted above to be in `0..=mem_size`, - // so this is at most one byte past the end of `process_mem`'s - // allocation — in bounds for pointer arithmetic, and never - // dereferenced: only the resulting address is used. - let value = PHYS_OFFSET + unsafe { process_mem.as_ptr().add(addend as usize) } as u64; - unsafe { - // SAFETY: `offset + 8 <= mem_size` is asserted above, so - // the 8-byte write lands fully inside `process_mem`'s - // allocation. `write_unaligned`, not `write`: an - // `r_offset` from the file is not guaranteed 8-byte - // aligned by anything checked here, only by the - // linker emitting `R_X86_64_RELATIVE` against aligned - // slots — a fact this reader has no way to verify. - process_mem - .as_mut_ptr() - .add(offset as usize) - .cast::() - .write_unaligned(value); - } - reloc_count += 1; - } - kind => panic!("kernel.elf: unsupported relocation type {kind:?}"), + for raw in RelaTable::new(table, arch::ELF_MACHINE).iter() { + let parsed = rela::parse(raw, &rules).unwrap_or_else(|e| panic!("kernel.elf: {e}")); + let Some((offset, Op::Relative(target))) = parsed.map(|r| (r.offset(), r.op())) else { + panic!("kernel.elf: unsupported relocation type {:?}", raw.kind()); + }; + // SAFETY: `target` is inside the image, so this is at most one byte + // past the end of `process_mem`'s allocation — in bounds for + // pointer arithmetic, and never dereferenced: only the resulting + // address is used. + let value = PHYS_OFFSET + unsafe { process_mem.as_ptr().add(at(target)) } as u64; + unsafe { + // SAFETY: the parse put `offset + 8` inside `[0, mem_size)`, so + // the 8-byte write lands fully inside `process_mem`'s + // allocation. `write_unaligned`, not `write`: an `r_offset` + // from the file is not guaranteed 8-byte aligned by anything + // checked here, only by the linker emitting + // `R_X86_64_RELATIVE` against aligned slots — a fact this + // reader has no way to verify. + process_mem + .as_mut_ptr() + .add(offset as usize) + .cast::() + .write_unaligned(value); } + reloc_count += 1; } } println!("Applied {} relocations", reloc_count); LoadedKernel { memory: process_mem, - entry_offset: layout.entry as usize, + entry_offset: at(layout.entry()), stack_offset: placed.stack as usize, stack_size, } diff --git a/kernel/src/elf/cache.rs b/kernel/src/elf/cache.rs index 4bc7d919fdc..261f23b3c46 100644 --- a/kernel/src/elf/cache.rs +++ b/kernel/src/elf/cache.rs @@ -20,25 +20,28 @@ use crate::process::PageAlloc; use crate::sync::Lock; use crate::vfs::BackingId; use crate::UserAddr; -use toyos_elf::{RelaCounts, RelocKind}; +use toyos_elf::dynamic::InitArray; +use toyos_elf::rela::Rules; +use toyos_elf::{ImageRange, Op, RelaCounts, RelocKind, SymIndex, TlsRef}; -/// A module's non-`RELATIVE` relocations, extracted once at cache time. +/// A module's non-`RELATIVE` relocations, parsed and extracted once at cache +/// time, each as `(r_offset, what it names)`. #[derive(Clone)] pub struct CachedRelocs { - /// `GLOB_DAT` and `JUMP_SLOT`: (offset, symbol). - pub bind: Vec<(u64, u32)>, - pub tpoff64: Vec<(u64, u32, i64)>, - pub tpoff32: Vec<(u64, u32, i64)>, - /// The kernel writes a module id here. - pub dtpmod64: Vec<(u64, u32, i64)>, + /// `GLOB_DAT` and `JUMP_SLOT`. + pub bind: Vec<(u64, SymIndex)>, + pub tpoff64: Vec<(u64, TlsRef)>, + pub tpoff32: Vec<(u64, TlsRef)>, + /// The kernel writes a module id here; `None` names the module itself. + pub dtpmod64: Vec<(u64, Option)>, /// The kernel writes a TLS offset within the module here. - pub dtpoff64: Vec<(u64, u32, i64)>, + pub dtpoff64: Vec<(u64, TlsRef)>, } // Extracts every non-`RELATIVE` entry, or `None` if it would not fit one kernel allocation. fn prescan_relocs(lib: &LoadedLib) -> Option { - let counts = RelaCounts::of(lib.relocations()); - let widest = core::mem::size_of::<(u64, u32, i64)>(); + let counts = RelaCounts::of(lib.raw_relocations()); + let widest = core::mem::size_of::<(u64, TlsRef)>().max(core::mem::size_of::<(u64, Option)>()); // Excludes `Relative`: bounding on it would refuse to cache nearly every library. let kept = [RelocKind::GlobDat, RelocKind::Tpoff64, RelocKind::Tpoff32, RelocKind::DtpMod64, RelocKind::DtpOff64]; @@ -55,13 +58,13 @@ fn prescan_relocs(lib: &LoadedLib) -> Option { dtpoff64: Vec::with_capacity(counts.dtpoff64), }; for r in lib.relocations() { - match r.kind { - RelocKind::GlobDat | RelocKind::JumpSlot => relocs.bind.push((r.offset, r.sym)), - RelocKind::Tpoff64 => relocs.tpoff64.push((r.offset, r.sym, r.addend)), - RelocKind::Tpoff32 => relocs.tpoff32.push((r.offset, r.sym, r.addend)), - RelocKind::DtpMod64 => relocs.dtpmod64.push((r.offset, r.sym, r.addend)), - RelocKind::DtpOff64 => relocs.dtpoff64.push((r.offset, r.sym, r.addend)), - _ => {} + match r.op() { + Op::Bind(sym) => relocs.bind.push((r.offset(), sym)), + Op::Tpoff64(t) => relocs.tpoff64.push((r.offset(), t)), + Op::Tpoff32(t) => relocs.tpoff32.push((r.offset(), t)), + Op::DtpMod64(sym) => relocs.dtpmod64.push((r.offset(), sym)), + Op::DtpOff64(t) => relocs.dtpoff64.push((r.offset(), t)), + Op::Relative(_) => {} } } Some(relocs) @@ -79,10 +82,9 @@ struct Snapshot { rela: Option, jmprel: Option, gnu_hash: Option, - eh_frame_hdr_vaddr: u64, - eh_frame_hdr_size: u64, - init_array_vaddr: u64, - init_array_size: u64, + rules: Rules, + eh_frame_hdr: Option, + init_array: Option, span: u64, rw_lo: u64, rw_hi: u64, @@ -100,10 +102,9 @@ impl Snapshot { rela: lib.rela, jmprel: lib.jmprel, gnu_hash: lib.gnu_hash, - eh_frame_hdr_vaddr: lib.eh_frame_hdr_vaddr, - eh_frame_hdr_size: lib.eh_frame_hdr_size, - init_array_vaddr: lib.init_array_vaddr, - init_array_size: lib.init_array_size, + rules: lib.rules, + eh_frame_hdr: lib.eh_frame_hdr, + init_array: lib.init_array, span: lib.span, rw_lo: lib.rw_lo, rw_hi: lib.rw_hi, @@ -130,10 +131,9 @@ impl Snapshot { jmprel: self.jmprel, gnu_hash: self.gnu_hash, cached_relocs, - eh_frame_hdr_vaddr: self.eh_frame_hdr_vaddr, - eh_frame_hdr_size: self.eh_frame_hdr_size, - init_array_vaddr: self.init_array_vaddr, - init_array_size: self.init_array_size, + rules: self.rules, + eh_frame_hdr: self.eh_frame_hdr, + init_array: self.init_array, span: self.span, rw_lo: self.rw_lo, rw_hi: self.rw_hi, diff --git a/kernel/src/elf/index.rs b/kernel/src/elf/index.rs index 2d807a0c144..89354f5537a 100644 --- a/kernel/src/elf/index.rs +++ b/kernel/src/elf/index.rs @@ -7,66 +7,88 @@ use alloc::vec::Vec; use crate::mm::{KernelSlice, MAX_HEAP_ALLOC}; -use toyos_elf::rela::ExeRefusal; -use toyos_elf::{Rela, RelaCounts, RelaTable, RelocKind}; +use toyos_abi::syscall::SyscallError; +use toyos_elf::rela::{self, ExeRefusal, Rules}; +use toyos_elf::{ImageOffset, Op, RelaCounts, RelaTable, RelocError, SymIndex, TlsRef}; -/// Relocation entries the loader needs, grouped by what it does with them. +/// Relocation entries the loader needs, grouped by what it does with them; +/// each is `(r_offset, what it names)`, parsed. pub struct ParsedRelaEntries { - /// `R_X86_64_RELATIVE`: (offset, addend). - pub relative: Vec<(u64, i64)>, - /// `R_X86_64_GLOB_DAT` and `R_X86_64_JUMP_SLOT`: (offset, symbol, addend). - pub glob_dat: Vec<(u64, u32, i64)>, - pub tpoff64: Vec<(u64, u32, i64)>, - pub tpoff32: Vec<(u64, u32, i64)>, + /// `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)>, } -impl ParsedRelaEntries { - /// Every entry as a `Rela` for `rela::validate`; `GLOB_DAT` stands for the - /// `JUMP_SLOT` grouped with it, since kind carries width and symbol-need. - pub fn as_relas(&self) -> impl Iterator + '_ { - let rel = self.relative.iter().map(|&(offset, addend)| Rela { - offset, sym: 0, kind: RelocKind::Relative, addend, - }); - let bind = self.glob_dat.iter().map(|&(offset, sym, addend)| Rela { - offset, sym, kind: RelocKind::GlobDat, addend, - }); - let t64 = self.tpoff64.iter().map(|&(offset, sym, addend)| Rela { - offset, sym, kind: RelocKind::Tpoff64, addend, - }); - let t32 = self.tpoff32.iter().map(|&(offset, sym, addend)| Rela { - offset, sym, kind: RelocKind::Tpoff32, addend, - }); - rel.chain(bind).chain(t64).chain(t32) +/// 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 + } + } } } -/// Groups both relocation tables, reserved exactly from what -/// `RelaCounts::for_executable` allows, or its refusal. -pub fn parse_rela_entries(rela_data: &[u8], jmprel_data: &[u8]) -> Result { +/// 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, +) -> 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()); - // Ceiling assumes the widest record type, since any one group could be the whole table. - let widest = core::mem::size_of::<(u64, u32, i64)>(); - let reserve = counts.for_executable(widest, MAX_HEAP_ALLOC).inspect_err(|_| log!("ELF: {:?} refused", counts))?; + let reserve = counts + .for_executable(WIDEST, MAX_HEAP_ALLOC) + .inspect_err(|_| log!("ELF: {:?} refused", counts)) + .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 r in entries() { - match r.kind { - RelocKind::Relative => out.relative.push((r.offset, r.addend)), - RelocKind::GlobDat | RelocKind::JumpSlot => { - out.glob_dat.push((r.offset, r.sym, r.addend)) - } - RelocKind::Tpoff64 => out.tpoff64.push((r.offset, r.sym, r.addend)), - RelocKind::Tpoff32 => out.tpoff32.push((r.offset, r.sym, r.addend)), - _ => {} + for raw in entries() { + let Some(r) = rela::parse(raw, rules).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) diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 928aaa7287e..fe7ee949b4d 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -17,17 +17,17 @@ 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_tpoff_relocs, defining_module, rebase_relative_relocs, - resolve_dlopen_relocs, resolve_lib_bind_relocs, + apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, defining_module, + rebase_relative_relocs, resolve_dlopen_relocs, resolve_lib_bind_relocs, tpoff32_value, }; use crate::mm::{align_2m_checked, KernelSlice, MAX_HEAP_ALLOC, PAGE_2M, PAGE_BYTES}; use crate::process::PageAlloc; use crate::UserAddr; -use toyos_elf::dynamic::Dynamic; +use toyos_elf::dynamic::{Dynamic, InitArray}; use toyos_elf::section::{SectionTable, SHT_DYNSYM}; -use toyos_elf::sym::SymTab; -use toyos_elf::{rela, GnuHash, Layout, RelaTable}; +use toyos_elf::sym::{Sym, SymTab}; +use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError}; /// `toyos_elf::MAX_TLS_ALIGN` must equal the kernel's largest page. const _: () = assert!(toyos_elf::MAX_TLS_ALIGN == PAGE_2M); @@ -37,7 +37,7 @@ const _: () = assert!(toyos_elf::MAX_TLS_ALIGN == PAGE_2M); pub fn parse_layout(data: &[u8]) -> Result { let layout = Layout::parse(data, crate::arch::ELF_MACHINE).map_err(|e| e.as_str())?; if layout - .section_headers + .section_headers() .is_some_and(|s| s.byte_len() > MAX_HEAP_ALLOC) { return Err("ELF: section header table larger than one kernel allocation"); @@ -98,12 +98,13 @@ pub struct LoadedLib { jmprel: Option, gnu_hash: Option, cached_relocs: Option, - /// `.eh_frame_hdr`, relative to the module base, from `PT_GNU_EH_FRAME`. - pub eh_frame_hdr_vaddr: u64, - pub eh_frame_hdr_size: u64, - /// `.init_array`, relative to the module base, from `DT_INIT_ARRAY`. - pub init_array_vaddr: u64, - pub init_array_size: u64, + /// What `load_shared_lib` parsed `rela` and `jmprel` against: every later + /// walk of those tables parses them again, against the same rules. + rules: rela::Rules, + /// `PT_GNU_EH_FRAME`, for DWARF unwinding. + pub eh_frame_hdr: Option, + /// `DT_INIT_ARRAY`. + pub init_array: Option, /// Bytes between the image's lowest and highest virtual address. pub span: u64, /// Exact (unrounded) writable range within the image; `(span, span)` if none. @@ -203,11 +204,23 @@ impl LoadedLib { GnuHash::parse(unsafe { self.gnu_hash.as_ref()?.as_slice() }) } - /// Every relocation in this module's `DT_RELA` and `DT_JMPREL` tables. - fn relocations(&self) -> impl Iterator + '_ { + /// Every entry of this module's `DT_RELA` and `DT_JMPREL` tables, as the + /// file wrote it. + fn raw_relocations(&self) -> impl Iterator + '_ { table_entries(&self.rela).chain(table_entries(&self.jmprel)) } + /// Every relocation this module's loader writes, parsed. + /// + /// `load_shared_lib` parsed each of these once, refusing the module on the + /// first it could not, and the bytes have not moved since: they lie on no + /// page of the writable window (`rela::tables_outside_window`), so neither + /// the process nor a relocation writes them. + fn relocations(&self) -> impl Iterator + '_ { + self.raw_relocations() + .filter_map(|raw| rela::parse(raw, &self.rules).unwrap_or_else(|e| reparse_refused(e))) + } + /// One past the last virtual address this module occupies. /// /// Derived, not stored, so it needs no update at every site that rebases @@ -220,48 +233,60 @@ impl LoadedLib { pub fn resolve(&self, name: &str) -> Option { let symbols = self.symbols(); let idx = self.gnu_hash()?.lookup(name, &symbols)?; - Some(self.user_base + symbols.get(idx)?.value) + self.address_of(&symbols.get(idx)?) } - /// A `STT_TLS` symbol's offset within this module's TLS segment. - pub fn resolve_tls(&self, name: &str) -> Option { + /// A defined `STT_TLS` symbol of this name, as this module holds it. + pub fn resolve_tls(&self, name: &str) -> Option { self.symbols().find_tls(name) } + + /// Where a symbol of this module lies once mapped: inside the image, which + /// `load_shared_lib` checked every defined one of against its extent. + fn address_of(&self, sym: &Sym) -> Option { + Some(self.user_base + sym.address(self.rules.extent)?.get()) + } +} + +/// A module's tables parsed differently the second time: the bytes moved under +/// a module whose tables no writer reaches, which is a kernel bug. +#[cold] +#[inline(never)] +fn reparse_refused(e: RelocError) -> ! { + panic!("ELF: a relocation load_shared_lib accepted is refused on a second parse: {e}") } /// Look up a symbol by name, walking `.dynsym` rather than the hash table; /// some symbols are absent from `.gnu.hash`. pub fn dlsym(lib: &LoadedLib, name: &str) -> Option { let symbols = lib.symbols(); - symbols.find(name).map(|(_, sym)| lib.user_base + sym.value) + symbols.find(name).and_then(|(_, sym)| lib.address_of(&sym)) } -/// A loaded module image, addressed by the module's own virtual addresses. -/// Converts a file-supplied vaddr to an in-image offset with a bounds check; -/// refuses rather than panics, since a malformed `.so` is untrusted input. -/// The bounds check must precede the `vaddr - vaddr_min` subtraction: on an -/// out-of-range vaddr that subtraction wraps, and a wrapped offset can pass -/// the slice's own bounds check. +/// A loaded module image, addressed by the module's own virtual addresses: +/// a file-supplied vaddr becomes an in-image range through the extent's +/// check, or is refused, since a malformed `.so` is untrusted input. struct ModuleImage { image: KernelSlice, - vaddr_min: u64, - vaddr_max: u64, + extent: Extent, } impl ModuleImage { fn slice(&self, vaddr: u64, size: u64) -> Result { - let end = vaddr.checked_add(size).ok_or("ELF: dynamic extent overflows")?; - if vaddr < self.vaddr_min || end > self.vaddr_max { - return Err("ELF: dynamic table outside the loaded image"); - } - Ok(self - .image - .subslice((vaddr - self.vaddr_min) as usize, size as usize)) + let range = self + .extent + .range(vaddr, size) + .ok_or("ELF: dynamic table outside the loaded image")?; + Ok(self.at(range)) + } + + fn at(&self, range: ImageRange) -> KernelSlice { + self.image.subslice(range.start().get() as usize, range.len() as usize) } /// From `vaddr` to the end of the image (used where no tag records a size, e.g. `.gnu.hash`). fn slice_to_end(&self, vaddr: u64) -> Result { - self.slice(vaddr, self.vaddr_max.saturating_sub(vaddr)) + self.slice(vaddr, self.extent.max().saturating_sub(vaddr)) } fn optional(&self, vaddr: Option, size: u64) -> Result, &'static str> { @@ -309,11 +334,11 @@ pub fn load_shared_lib( let header_data = crate::loader::read_file_range(backing, 0, header_size); let layout = parse_layout(&header_data)?; - let (vaddr_min, vaddr_max) = (layout.vaddr_min, layout.vaddr_max); + let extent = layout.extent(); // Every bound below is image-relative and every `r_offset` is a vaddr, so a // non-zero `vaddr_min` shifts the two against each other: a write validated // inside the writable window lands `vaddr_min` bytes lower in the image. - if vaddr_min != 0 { + if extent.min() != 0 { return Err("ELF: a shared object must begin at vaddr 0"); } // No writable segment yields an empty window; no relocation can target it. @@ -343,20 +368,20 @@ pub fn load_shared_lib( } let t2 = crate::clock::nanos_since_boot(); + let module = ModuleImage { image, extent }; // In bounds only because `Layout` guarantees `filesz <= memsz`; the checked // subslice turns a weakening of that into an assert, not an overwrite. for seg in layout.segments() { - let dst = image.subslice((seg.vaddr - vaddr_min) as usize, seg.filesz as usize); - read_backing_into(backing, seg.file_offset, dst) + let dst = module.at(seg.image()).subslice(0, seg.filesz() as usize); + read_backing_into(backing, seg.file_offset(), dst) .map_err(|_| "a segment could not be read off the device")?; } let t3 = crate::clock::nanos_since_boot(); - let module = ModuleImage { image, vaddr_min, vaddr_max }; - let dyn_info = match layout.dynamic { - Some((_, vaddr, size)) => { - let region = module.slice(vaddr, size)?; - // SAFETY: `region` came from `ModuleImage::slice`'s bounds check; + let dyn_info = match layout.dynamic() { + Some(dynamic) => { + let region = module.at(dynamic.image); + // SAFETY: `region` came from the layout's own range inside the image; // `image` is still exclusively owned here. Dynamic::parse(unsafe { region.as_slice() }) } @@ -391,6 +416,17 @@ pub fn load_shared_lib( }; let sym_count = dynsym.as_ref().map_or(0, |s| s.size() / toyos_elf::sym::ENTRY_SIZE); let dynstr = module.optional(dyn_info.strtab, dyn_info.strsz.unwrap_or(0))?; + // Every symbol another module or `dlsym` may later ask for is inside this + // one, and every TLS one inside its segment — or the module is refused now. + { + // SAFETY: both came from `ModuleImage::slice`'s bounds check; `image` + // is still exclusively owned here. + let (syms, strs) = unsafe { + (dynsym.as_ref().map_or(&[][..], |s| s.as_slice()), dynstr.as_ref().map_or(&[][..], |s| s.as_slice())) + }; + SymTab::new(syms, strs).bounded(extent, layout.tls_memsz()).map_err(|e| e.as_str())?; + } + let init_array = InitArray::parse(dyn_info.init_array, extent).map_err(|e| e.as_str())?; let rela = match dyn_info.rela { Some(t) => Some(module.slice(t.vaddr, t.size)?), @@ -401,49 +437,51 @@ pub fn load_shared_lib( None => None, }; - // Validate every entry before writing any: an unvalidated `DTPOFF64` with - // `r_sym == 0` writes `r_addend` verbatim, an arbitrary 8-byte write. // The exact writable extent, not `rw_offset`'s 2 MiB-rounded one: the // rounded start is up to 2 MiB below the first writable byte, and the pages // there are mapped `ReadExec` by `page_prot`. let window = (rw_lo, rw_hi); - let extent = |slice: &KernelSlice| { + let span_of = |slice: &KernelSlice| { let at = (slice.base() as usize - image.base() as usize) as u64; (at, at + slice.size() as u64) }; let tables = rela::ReadTables { - dynsym: dynsym.as_ref().map_or((0, 0), extent), - dynstr: dynstr.as_ref().map_or((0, 0), extent), - rela: rela.as_ref().map_or((0, 0), extent), - jmprel: jmprel.as_ref().map_or((0, 0), extent), + dynsym: dynsym.as_ref().map_or((0, 0), span_of), + dynstr: dynstr.as_ref().map_or((0, 0), span_of), + rela: rela.as_ref().map_or((0, 0), span_of), + jmprel: jmprel.as_ref().map_or((0, 0), span_of), }; - rela::tables_outside_window(&tables, window)?; - let entries = table_entries(&rela).chain(table_entries(&jmprel)); - // `None`: a library's image is written contiguously, with no fill-page edge. - rela::validate(entries, window, sym_count, None).map_err(|e| e.as_str())?; - + rela::tables_outside_window(&tables, window, PAGE_BYTES as u64)?; + let rules = rela::Rules { + extent, + window, + sym_count, + // A library's image is written contiguously, with no fill-page edge. + fill: None, + tls_memsz: layout.tls_memsz(), + }; + // Every entry is parsed here, and a refusal drops the image this pass has + // written into: nothing but this function has seen it. let base_phys = image.phys(); let mut reloc_count = 0u64; - for entry in table_entries(&rela).chain(table_entries(&jmprel)) { - if entry.kind == toyos_elf::RelocKind::Relative { - let value = (base_phys as i64 + entry.addend) as u64; - // SAFETY: `module.slice(entry.offset, 8)?` bounds-checks the write - // independently of `rela::validate`; `image` is still exclusively owned. - unsafe { module.slice(entry.offset, 8)?.write::(0, value) }; + for raw in table_entries(&rela).chain(table_entries(&jmprel)) { + let Some(r) = rela::parse(raw, &rules).map_err(|e| e.as_str())? else { continue }; + if let toyos_elf::Op::Relative(target) = r.op() { + // SAFETY: `module.slice(r.offset(), 8)?` bounds-checks the write + // independently of the parse; `image` is still exclusively owned. + unsafe { module.slice(r.offset(), 8)?.write::(0, base_phys + target.get()) }; reloc_count += 1; } } - let (tls_template, tls_memsz, tls_align) = match layout.tls { + let (tls_template, tls_memsz, tls_align) = match layout.tls() { Some(tls) => ( - Some(module.slice(tls.vaddr, tls.filesz)?), - tls.memsz as usize, - tls.align as usize, + Some(module.at(tls.template())), + tls.memsz() as usize, + tls.align() as usize, ), None => (None, 0, 0), }; - let (eh_frame_hdr_vaddr, eh_frame_hdr_size) = layout.eh_frame_hdr.unwrap_or((0, 0)); - let init_array = dyn_info.init_array; let t4 = crate::clock::nanos_since_boot(); log!( @@ -473,10 +511,9 @@ pub fn load_shared_lib( jmprel, gnu_hash, cached_relocs: None, - eh_frame_hdr_vaddr, - eh_frame_hdr_size, - init_array_vaddr: init_array.map_or(0, |t| t.vaddr), - init_array_size: init_array.map_or(0, |t| t.size), + rules, + eh_frame_hdr: layout.eh_frame_hdr(), + init_array, span: layout.span(), rw_lo, rw_hi, @@ -492,7 +529,7 @@ fn dynsym_count_from_sections( backing: &dyn crate::file_backing::FileBacking, layout: &Layout, ) -> Option { - let table = layout.section_headers?; + let table = layout.section_headers()?; let bytes = crate::loader::read_file_range(backing, table.file_offset, table.byte_len()); let dynsym = SectionTable::new(&bytes).find(SHT_DYNSYM)?; let entry_size = dynsym.entry_size.max(toyos_elf::sym::ENTRY_SIZE as u64); diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 743c0533e55..d7b28db7285 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -1,15 +1,20 @@ //! Applying relocations to a loaded module. //! //! Every write goes through [`LoadedLib::write_at`]; every offset given to it -//! was already validated against the module's writable window by -//! `load_shared_lib`, so `write_at`'s asserts are kernel-bug asserts, not -//! refusals. Unresolved symbols are logged and left unresolved, never fatal: -//! a `.so` naming an undefined symbol is untrusted input, not a kernel bug, -//! and the process faults on the slot only if it later uses it. +//! was parsed against the module's writable window by `load_shared_lib`, so +//! `write_at`'s asserts are kernel-bug asserts, not refusals. Every value +//! written is derived from a parsed relocation — never read back out of the +//! image — so what a slot holds before its write decides nothing. +//! +//! Unresolved symbols are logged and left unresolved, never fatal: a `.so` +//! naming an undefined symbol is untrusted input, not a kernel bug, and the +//! process faults on the slot only if it later uses it. A resolved TLS +//! reference whose `S + A` leaves the defining module's segment is refused. use super::{CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; use crate::UserAddr; -use toyos_elf::RelocKind; +use toyos_elf::sym::Sym; +use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef}; impl LoadedLib { /// Write a value at a byte offset within this module's kernel mapping. @@ -45,43 +50,58 @@ impl LoadedLib { } } - fn bind_entries(&self) -> impl Iterator + '_ { + fn bind_entries(&self) -> impl Iterator + '_ { let cached = self.cached_relocs.as_ref().map(|r| r.bind.iter().copied()); let scanned = self.cached_relocs.is_none().then(|| { - self.relocations() - .filter(|r| r.kind.is_bind()) - .map(|r| (r.offset, r.sym)) + self.relocations().filter_map(|r| match r.op() { + Op::Bind(sym) => Some((r.offset(), sym)), + _ => None, + }) }); cached.into_iter().flatten().chain(scanned.into_iter().flatten()) } - fn typed_entries( + fn tls_entries( &self, - kind: RelocKind, - pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, u32, i64)>, - ) -> impl Iterator + '_ { + of: fn(Op) -> Option, + pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, TlsRef)>, + ) -> impl Iterator + '_ { let cached = self.cached_relocs.as_ref().map(|r| pick(r).iter().copied()); let scanned = self.cached_relocs.is_none().then(move || { - self.relocations() - .filter(move |r| r.kind == kind) - .map(|r| (r.offset, r.sym, r.addend)) + self.relocations().filter_map(move |r| Some((r.offset(), of(r.op())?))) }); cached.into_iter().flatten().chain(scanned.into_iter().flatten()) } + + fn dtpmod_entries(&self) -> impl Iterator)> + '_ { + let cached = self.cached_relocs.as_ref().map(|r| r.dtpmod64.iter().copied()); + let scanned = self.cached_relocs.is_none().then(|| { + self.relocations().filter_map(|r| match r.op() { + Op::DtpMod64(sym) => Some((r.offset(), sym)), + _ => None, + }) + }); + cached.into_iter().flatten().chain(scanned.into_iter().flatten()) + } + + /// Every `RELATIVE` slot and the position in the image it points at. + fn relative_entries(&self) -> impl Iterator + '_ { + self.relocations().filter_map(|r| match r.op() { + Op::Relative(target) => Some((r.offset(), target)), + _ => None, + }) + } } -/// Add `delta` to every `R_X86_64_RELATIVE` slot. +/// Point every `R_X86_64_RELATIVE` slot into the image at `lib.user_base`. /// -/// Reads the old value from the shared image, not the private window, because -/// this only runs on a freshly cloned window that's still byte-identical to it. -pub fn rebase_relative_relocs(lib: &LoadedLib, delta: i64) { - for r in lib.relocations() { - if r.kind == RelocKind::Relative { - // SAFETY: this window was just cloned; nothing else writes to it yet. - let old = unsafe { lib.image.read::(r.offset as usize) }; - // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(r.offset, (old as i64 + delta) as u64) }; - } +/// Each value is the image's address plus the slot's parsed target, a position +/// inside the image — never the slot's old contents, which a module mapped into +/// a running process may already have changed. +pub fn rebase_relative_relocs(lib: &LoadedLib) { + for (offset, target) in lib.relative_entries() { + // SAFETY: see write_at's `# Safety`. + unsafe { lib.write_at::(offset, (lib.user_base + target.get()).raw()) }; } } @@ -92,7 +112,7 @@ pub fn resolve_dlopen_relocs(lib: &LoadedLib, other_libs: &[LoadedLib]) { let mut resolved = 0u64; let mut unresolved = 0u64; for (offset, sym) in lib.bind_entries() { - let name = symbols.name(sym as usize); + let name = symbols.name(sym.get()); match other_libs.iter().find_map(|other| other.resolve(name)) { Some(addr) => { // SAFETY: rela::tables_outside_window refused any image whose tables meet the window these writes land in. @@ -119,7 +139,7 @@ pub fn resolve_lib_bind_relocs( ) { let symbols = lib.symbols(); for (offset, sym) in lib.bind_entries() { - let name = symbols.name(sym as usize); + let name = symbols.name(sym.get()); let resolved = exe_sym_map .get(name) .copied() @@ -134,24 +154,42 @@ pub fn resolve_lib_bind_relocs( /// Apply `R_X86_64_TPOFF64` and `R_X86_64_TPOFF32`: the initial-exec TLS /// model, a fixed offset from the thread pointer. +/// +/// Every value is resolved before the first is written, so a refused module is +/// left as it was found. pub fn apply_tpoff_relocs( lib: &LoadedLib, lib_base_offset: usize, tls: toyos_elf::tls::Static, tls_info: &TlsModuleInfo, -) { +) -> Result<(), RelocError> { + let tpoff64 = |op| match op { + Op::Tpoff64(t) => Some(t), + _ => None, + }; + let tpoff32 = |op| match op { + Op::Tpoff32(t) => Some(t), + _ => None, + }; + for (_, r) in lib.tls_entries(tpoff64, |c| &c.tpoff64) { + compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; + } + for (_, r) in lib.tls_entries(tpoff32, |c| &c.tpoff32) { + tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; + } + let mut count64 = 0u64; - for (offset, sym, addend) in lib.typed_entries(RelocKind::Tpoff64, |r| &r.tpoff64) { - let tpoff = compute_tpoff(lib, sym, addend, lib_base_offset, tls, tls_info); + for (offset, r) in lib.tls_entries(tpoff64, |c| &c.tpoff64) { + let tpoff = compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff as u64) }; + unsafe { lib.write_at::(offset, tpoff) }; count64 += 1; } let mut count32 = 0u64; - for (offset, sym, addend) in lib.typed_entries(RelocKind::Tpoff32, |r| &r.tpoff32) { - let tpoff = compute_tpoff(lib, sym, addend, lib_base_offset, tls, tls_info); + for (offset, r) in lib.tls_entries(tpoff32, |c| &c.tpoff32) { + let tpoff = tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff as i32) }; + unsafe { lib.write_at::(offset, tpoff) }; count32 += 1; } if count64 > 0 || count32 > 0 { @@ -160,61 +198,80 @@ pub fn apply_tpoff_relocs( count64, count32, lib_base_offset, tls.total_memsz() ); } + Ok(()) +} + +/// 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 { + i32::try_from(tpoff).map_err(|_| RelocError::TpoffOverflows) } -/// Apply `R_X86_64_DTPMOD64` and `R_X86_64_DTPOFF64`: the general-dynamic TLS -/// model resolved through the DTV. +/// Apply `R_X86_64_DTPMOD64`: the general-dynamic TLS model's module id. pub fn apply_dtpmod_relocs(lib: &LoadedLib, module_id: u64, tls_info: &TlsModuleInfo) { - let mut count_mod = 0u64; - for (offset, sym, _) in lib.typed_entries(RelocKind::DtpMod64, |r| &r.dtpmod64) { + let mut count = 0u64; + for (offset, sym) in lib.dtpmod_entries() { let mid = resolve_dtpmod(lib, sym, module_id, tls_info); // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, mid) }; - count_mod += 1; + count += 1; + } + if count > 0 { + log!("dlopen: applied {} DTPMOD64 relocs (module_id={})", count, module_id); } - let mut count_off = 0u64; - for (offset, sym, addend) in lib.typed_entries(RelocKind::DtpOff64, |r| &r.dtpoff64) { - let value = resolve_dtpoff(lib, sym, addend, tls_info); +} + +/// Apply `R_X86_64_DTPOFF64`: the general-dynamic TLS model's offset within the +/// defining module's block. Every value is resolved before the first is +/// written, so a refused module is left as it was found. +pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result<(), RelocError> { + let dtpoff = |op| match op { + Op::DtpOff64(t) => Some(t), + _ => None, + }; + for (_, r) in lib.tls_entries(dtpoff, |c| &c.dtpoff64) { + resolve_tls(lib, r, 0, tls_info)?; + } + let mut count = 0u64; + for (offset, r) in lib.tls_entries(dtpoff, |c| &c.dtpoff64) { + let value = resolve_tls(lib, r, 0, tls_info)?.map_or(0, |(_, at)| at.get()); // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, value as u64) }; - count_off += 1; + unsafe { lib.write_at::(offset, value) }; + count += 1; } - if count_mod > 0 || count_off > 0 { - log!( - "dlopen: applied {} DTPMOD64 + {} DTPOFF64 relocs (module_id={})", - count_mod, count_off, module_id - ); + if count > 0 { + log!("dlopen: applied {} DTPOFF64 relocs", count); } + Ok(()) } -/// The module in `tls_info` that defines `name`, or `None` if none does. -pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, u64)> { +/// The module in `tls_info` that defines `name`, and the symbol as it defines +/// it, or `None` if none does. +pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, Sym)> { for lib in tls_info.libs { if lib.tls_memsz == 0 { continue; } - if let Some(sym_offset) = lib.resolve_tls(name) { + if let Some(sym) = lib.resolve_tls(name) { // Template pointer is unique per module: each points into a distinct image. // No matching module here means inconsistent tables; treated as unresolved, not a bug. let module = tls_info .modules .iter() .find(|m| m.template == lib.tls_template)?; - return Some((module, sym_offset)); + return Some((module, sym)); } } None } -fn resolve_dtpmod(lib: &LoadedLib, r_sym: u32, self_module_id: u64, tls_info: &TlsModuleInfo) -> u64 { - if r_sym == 0 { - return self_module_id; - } +fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, tls_info: &TlsModuleInfo) -> u64 { + let Some(sym) = sym else { return self_module_id }; let symbols = lib.symbols(); - if symbols.get(r_sym as usize).is_some_and(|s| s.is_defined()) { + if symbols.get(sym.get()).is_some_and(|s| s.is_defined()) { return self_module_id; } - let name = symbols.name(r_sym as usize); + let name = symbols.name(sym.get()); match defining_module(name, tls_info) { Some((module, _)) => module.module_id, None => { @@ -224,49 +281,49 @@ fn resolve_dtpmod(lib: &LoadedLib, r_sym: u32, self_module_id: u64, tls_info: &T } } -fn resolve_dtpoff(lib: &LoadedLib, r_sym: u32, r_addend: i64, tls_info: &TlsModuleInfo) -> i64 { - if r_sym == 0 { - return r_addend; - } +/// `S + A` for one TLS reference, and the static-block offset of the module +/// it lies in: this module's own (`own_base_offset`), or the module defining +/// the symbol. `None` is a symbol no module defines, which is logged; a sum +/// outside the defining module's segment refuses the module. +fn resolve_tls( + lib: &LoadedLib, + r: TlsRef, + own_base_offset: usize, + tls_info: &TlsModuleInfo, +) -> Result, RelocError> { + let s = match r { + TlsRef::Own(at) => return Ok(Some((own_base_offset, at))), + TlsRef::Symbol(s) => s, + }; let symbols = lib.symbols(); - if let Some(sym) = symbols.get(r_sym as usize).filter(|s| s.is_defined()) { - return sym.value as i64 + r_addend; + if let Some(defined) = symbols.get(s.sym().get()).filter(|d| d.is_defined()) { + let at = s.offset_in(&defined, lib.tls_memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + return Ok(Some((own_base_offset, at))); } - let name = symbols.name(r_sym as usize); + let name = symbols.name(s.sym().get()); match defining_module(name, tls_info) { - Some((_, sym_offset)) => sym_offset as i64 + r_addend, + Some((module, defined)) => { + let at = s.offset_in(&defined, module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + Ok(Some((module.base_offset, at))) + } None => { - log!("dtpoff: unresolved TLS symbol: {}", name); - r_addend + log!("tls: unresolved TLS symbol: {}", name); + Ok(None) } } } -/// `S + A - tp` for one initial-exec reference: `S`'s place in the block and -/// the addend go through `tls::tpoff`, which folds in `A` on every branch. +/// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module +/// defines. fn compute_tpoff( lib: &LoadedLib, - r_sym: u32, - r_addend: i64, + r: TlsRef, lib_base_offset: usize, tls: toyos_elf::tls::Static, tls_info: &TlsModuleInfo, -) -> i64 { - if r_sym == 0 { - return tls.tpoff(lib_base_offset as u64, r_addend); - } - let symbols = lib.symbols(); - if let Some(sym) = symbols.get(r_sym as usize).filter(|s| s.is_defined()) { - return tls.tpoff(lib_base_offset as u64 + sym.value, r_addend); - } - let name = symbols.name(r_sym as usize); - match defining_module(name, tls_info) { - Some((module, sym_offset)) => { - tls.tpoff(module.base_offset as u64 + sym_offset, r_addend) - } - None => { - log!("tpoff: unresolved TLS symbol: {}", name); - 0 - } +) -> Result { + match resolve_tls(lib, r, lib_base_offset, tls_info)? { + Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), + None => Ok(0), } } diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 8dc2109c481..b6c3df9541e 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -17,8 +17,7 @@ mod tls; pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN}; pub(crate) use start::alloc_kernel_stack; pub(crate) use crate::arch::entry::{kernel_start, process_start, thread_start}; -pub use tls::{setup_combined_tls, setup_tls, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; -pub(crate) use tls::rebase_block; +pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; use alloc::string::String; use alloc::sync::Arc; @@ -39,7 +38,8 @@ use toyos_abi::handle::Rights; use toyos_abi::syscall::SyscallError; use toyos_elf::section::SectionTable; use toyos_elf::sym::{self, SymTab}; -use toyos_elf::{GnuHash, Layout}; +use toyos_elf::rela::{FillLattice, Rules, FILL_GRANULE}; +use toyos_elf::{GnuHash, Layout, RelocError, SymIndex, TlsRef}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -112,7 +112,7 @@ fn read_elf_table( fn insert_elf_regions( addr_space: &mut crate::mm::paging::AddressSpace, layout: &Layout, - base: u64, + image_start: UserAddr, backing: &Arc, ) -> Result<(), SyscallError> { use crate::vma::{Region, RegionKind}; @@ -123,16 +123,16 @@ fn insert_elf_regions( } for seg in layout.segments() { - let (lo, hi) = seg.page_range(layout.vaddr_min, 4096); - let (seg_start, seg_end) = (base + layout.vaddr_min + lo, base + layout.vaddr_min + hi); + let (lo, hi) = seg.page_range(4096); + let (seg_start, seg_end) = ((image_start + lo).raw(), (image_start + hi).raw()); // A zero-size region would sit in the map where `find_region` can't see past it. if seg_end == seg_start { continue; } let prot = segment_prot(seg); - let file_block_start = seg.file_offset / 4096; - let file_blocks_needed = (seg.filesz + (seg.file_offset % 4096)).div_ceil(4096); + let file_block_start = seg.file_offset() / 4096; + let file_blocks_needed = (seg.filesz() + (seg.file_offset() % 4096)).div_ceil(4096); let file_backed_end = seg_start + file_blocks_needed * 4096; if file_blocks_needed > 0 { @@ -143,7 +143,7 @@ fn insert_elf_regions( kind: RegionKind::FileBacked { backing: Arc::clone(backing), file_offset: file_block_start * 4096, - file_size: seg.filesz + (seg.file_offset % 4096), + file_size: seg.filesz() + (seg.file_offset() % 4096), prot, }, }, @@ -170,9 +170,9 @@ fn insert_elf_regions( /// let a hostile ELF run as data instead. fn segment_prot(seg: &toyos_elf::Segment) -> crate::mm::paging::Prot { use crate::mm::paging::Prot; - if seg.flags.executable() { + if seg.flags().executable() { Prot::ReadExec - } else if seg.flags.writable() { + } else if seg.flags().writable() { Prot::ReadWrite } else { Prot::Read @@ -199,10 +199,10 @@ impl ExeTables { /// /// The index may exceed the `.dynsym` length the loader estimated, so the /// record is fetched directly rather than the estimate trusted. - fn symbol(&self, backing: &dyn crate::file_backing::FileBacking, r_sym: u32) -> Option { + fn symbol(&self, backing: &dyn crate::file_backing::FileBacking, r_sym: SymIndex) -> Option { let off = self .symtab_file_off? - .checked_add(r_sym as u64 * sym::ENTRY_SIZE as u64)?; + .checked_add((r_sym.get() as u64).checked_mul(sym::ENTRY_SIZE as u64)?)?; sym::parse_at(&read_file_range(backing, off, sym::ENTRY_SIZE), 0) } } @@ -245,9 +245,9 @@ fn read_exe_tables( layout: &Layout, path: &str, ) -> Result { - let (dyn_info, needed) = match layout.dynamic { - Some((dyn_off, _, dyn_size)) => { - let data = table(backing, path, "PT_DYNAMIC", dyn_off, dyn_size as usize)?; + 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)); @@ -262,7 +262,7 @@ fn read_exe_tables( 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 if layout.dynamic().is_none() => rela_dyn_from_sections(backing, layout, path)?, None => Vec::new(), }; let jmprel_data = match dyn_info.jmprel { @@ -272,12 +272,21 @@ fn read_exe_tables( } None => Vec::new(), }; - let relas = elf::parse_rela_entries(&rela_data, &jmprel_data).map_err(|refused| { + // 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. The + // symbol bound is left to `ExeTables::symbol`'s read off the file. + let extent = layout.extent(); + let rules = Rules { + extent, + window: (extent.min(), extent.max()), + sym_count: usize::MAX, + fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), + tls_memsz: layout.tls_memsz(), + }; + let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules).map_err(|refused| { log!("spawn: {}: {}", path, refused.as_str()); - match refused { - toyos_elf::rela::ExeRefusal::TooLarge => SyscallError::ResourceExhausted, - toyos_elf::rela::ExeRefusal::TlsDescriptor => SyscallError::InvalidArgument, - } + refused.error() })?; let dynstr = match dyn_info.strtab_table() { @@ -335,7 +344,7 @@ fn rela_dyn_from_sections( layout: &Layout, path: &str, ) -> Result, SyscallError> { - let Some(sections) = layout.section_headers else { + let Some(sections) = layout.section_headers() else { return Ok(Vec::new()); }; let shdrs = table(backing, path, "e_shnum", sections.file_offset, sections.byte_len())?; @@ -392,33 +401,20 @@ pub fn spawn( // The rebase base is the file's numbers, so `rebase_base` refuses a vaddr_min // that underflows the subtraction or a span that leaves the user half. - let Some(base) = toyos_userbound::rebase_base(USER_VM_BASE, layout.vaddr_min, layout.span()) + let extent = layout.extent(); + let Some(base) = toyos_userbound::rebase_base(USER_VM_BASE, extent.min(), layout.span()) else { log!("spawn: {}: image at vaddr_min {:#x} spanning {:#x} cannot rebase to {:#x}", - path, layout.vaddr_min, layout.span(), USER_VM_BASE); + path, extent.min(), layout.span(), USER_VM_BASE); return Err(SyscallError::InvalidArgument.into()); }; + // 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); let exe = read_exe_tables(backing.as_ref(), &layout, path)?; let t1 = crate::clock::nanos_since_boot(); - // The exe's relocations, validated like a library's but for the fill page: - // applied one demand-fault page at a time, a crossing write is refused, not - // silently dropped. The symbol bound is left to `exe.symbol`'s backing read. - let fill = toyos_elf::rela::FillLattice { - base: layout.vaddr_min, - granule: toyos_elf::rela::FILL_GRANULE, - }; - if let Err(e) = toyos_elf::rela::validate( - exe.relas.as_relas(), - (layout.vaddr_min, layout.vaddr_max), - usize::MAX, - Some(fill), - ) { - log!("spawn: {}: {}", path, e.as_str()); - return Err(SyscallError::InvalidArgument.into()); - } - // 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(); @@ -428,8 +424,8 @@ pub fn spawn( log!("spawn: {}: {} relocations do not fit one index", path, u64_writes); return Err(SyscallError::ResourceExhausted.into()); }; - for &(r_offset, r_addend) in &exe.relas.relative { - reloc_index.add_u64(r_offset, (base as i64 + r_addend) as u64); + 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(); @@ -443,15 +439,14 @@ pub fn spawn( }; crate::clock::map_page(&mut space); let child_pt: PageTables = Arc::new(Lock::new(space)); - insert_elf_regions(&mut child_pt.lock(), &layout, base, &backing)?; + 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 { - let delta = lib.user_base.raw() as i64 - lib.phys_base as i64; - if delta != 0 { - elf::rebase_relative_relocs(lib, delta); + if lib.user_base.raw() != lib.phys_base { + elf::rebase_relative_relocs(lib); } } @@ -467,23 +462,27 @@ pub fn spawn( .flatten(); let exe_sym_map = match &fallback { Some((syms, strs)) => { - symbols::static_map(&SymTab::new(syms, strs), UserAddr::new(base)) + symbols::static_map(&SymTab::new(syms, strs), image_start, extent) } - None => symbols::dynamic_map(&exe.symbols(), UserAddr::new(base)), + 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 + })?; log!("dynamic: {} exe symbols available to libraries", exe_sym_map.len()); for lib in &loaded_libs.libs { 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 == 0 { + for &(r_offset, r_sym) in &exe.relas.glob_dat { + if r_sym.get() == 0 { continue; } let Some(sym) = exe.symbol(backing.as_ref(), r_sym) else { continue; }; - let name = toyos_elf::cstr(&exe.dynstr, sym.name as u64); + let name = 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 => log!("dynamic: unresolved exe symbol: {}", name), @@ -514,21 +513,24 @@ pub fn spawn( }); } - let exe_tls_template = match layout.tls.filter(|t| t.memsz > 0) { + let exe_tls_template = match layout.tls().filter(|t| t.memsz() > 0) { Some(tls) => { - let tls_file_off = file_off(&layout, path, "PT_TLS", tls.vaddr)?; + 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); + return Err(SyscallError::InvalidArgument.into()); + }; // Read directly into the `memsz`-sized buffer: `OwnedAlloc` zeroes // (no second pass for `.tbss`) and refuses a size past one heap // allocation itself. - let Some(tls_buf) = OwnedAlloc::new(tls.memsz as usize, 16) else { - log!("spawn: {}: cannot allocate a {}-byte TLS template", path, tls.memsz); + let Some(tls_buf) = OwnedAlloc::new(tls.memsz() as usize, 16) else { + log!("spawn: {}: cannot allocate a {}-byte TLS template", path, tls.memsz()); return Err(SyscallError::ResourceExhausted.into()); }; // `slice` bounds `filesz` against the `memsz` allocation, re-checking what `Layout::parse` already refused. if elf::read_backing_into( backing.as_ref(), tls_file_off, - tls_buf.slice(tls.filesz as usize), + tls_buf.slice(tls.template().len() as usize), ) .is_err() { @@ -547,8 +549,12 @@ pub fn spawn( return Err(SyscallError::ResourceExhausted.into()); }; - apply_tls_relocs(&exe, backing.as_ref(), &loaded_libs.libs, &tls_modules, - tls, &mut reloc_index); + if let Err(refused) = apply_tls_relocs(&exe, backing.as_ref(), &layout, &loaded_libs.libs, + &tls_modules, tls, &mut reloc_index) + { + log!("spawn: {}: {}", path, refused.as_str()); + return Err(SyscallError::InvalidArgument.into()); + } reloc_index.finalize(); let reloc_index = if reloc_index.len() > 0 { @@ -566,13 +572,14 @@ pub fn spawn( return Err(SyscallError::ResourceExhausted.into()); }; - let entry = base + layout.entry; + let entry = (image_start + layout.entry().get()).raw(); + let image_end = (image_start + layout.span()).raw(); let sp = user_stack.write_argv(argv); let t_tls = crate::clock::nanos_since_boot(); let syms = symbols::read_backtrace_table( backing.as_ref(), &layout, path, base, - base + layout.vaddr_min, base + layout.vaddr_max, + image_start.raw(), image_end, user_stack.base().raw(), user_stack.top(), ); let sym_bytes = syms.resident_bytes(); @@ -604,9 +611,10 @@ pub fn spawn( loaded_libs, reloc_index, elf_base: UserAddr::new(base), - exe_eh_frame_hdr_vaddr: layout.eh_frame_hdr.map_or(0, |(v, _)| v), - exe_eh_frame_hdr_size: layout.eh_frame_hdr.map_or(0, |(_, s)| s), - exe_vaddr_max: base + layout.vaddr_max, + 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, lib_paths, }, mmap_regions: Vec::new(), @@ -793,23 +801,26 @@ fn map_libs( /// /// Libraries' land directly; the executable's go into the relocation index /// because its pages do not exist yet. +#[allow(clippy::too_many_arguments)] fn apply_tls_relocs( exe: &ExeTables, backing: &dyn crate::file_backing::FileBacking, + layout: &Layout, loaded_libs: &[elf::LoadedLib], tls_modules: &[elf::TlsModule], tls: toyos_elf::tls::Static, reloc_index: &mut elf::RelocationIndex, -) { +) -> Result<(), RelocError> { let tls_info = elf::TlsModuleInfo { libs: loaded_libs, modules: tls_modules }; 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. - elf::apply_tpoff_relocs(lib, base_offset, tls, &tls_info); + 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 { + elf::apply_dtpoff_relocs(lib, &tls_info)?; elf::apply_dtpmod_relocs(lib, m.module_id, &tls_info); } } @@ -818,56 +829,53 @@ fn apply_tls_relocs( .iter() .find(|m| m.module_id == 1) .map_or(0, |m| m.base_offset); - for &(r_offset, r_sym, r_addend) in &exe.relas.tpoff64 { - let tpoff = exe_tpoff(exe, backing, r_sym, r_addend, exe_base_offset, - tls, &tls_info); - reloc_index.add_u64(r_offset, tpoff as u64); + let exe_tpoff = |r: TlsRef| { + exe_tpoff(exe, backing, layout, r, exe_base_offset, tls, &tls_info) + }; + for &(r_offset, r) in &exe.relas.tpoff64 { + reloc_index.add_u64(r_offset, exe_tpoff(r)? as u64); } - for &(r_offset, r_sym, r_addend) in &exe.relas.tpoff32 { - let tpoff = exe_tpoff(exe, backing, r_sym, r_addend, exe_base_offset, - tls, &tls_info); - reloc_index.add_i32(r_offset, tpoff as i32); + for &(r_offset, r) in &exe.relas.tpoff32 { + reloc_index.add_i32(r_offset, elf::tpoff32_value(exe_tpoff(r)?)?); } + Ok(()) } -/// One of the executable's `TPOFF` relocations, resolved to a value. -/// -/// A symbol the file does not hold resolves the same as `r_sym == 0`: the -/// module-relative offset, with nothing to resolve against. +/// One of the executable's `TPOFF` relocations, resolved to a value: `0` for +/// a symbol no module defines, a refusal for one the file does not hold or +/// whose `S + A` leaves the defining module's segment. #[allow(clippy::too_many_arguments)] fn exe_tpoff( exe: &ExeTables, backing: &dyn crate::file_backing::FileBacking, - r_sym: u32, - r_addend: i64, + layout: &Layout, + r: TlsRef, exe_base_offset: usize, tls: toyos_elf::tls::Static, tls_info: &elf::TlsModuleInfo, -) -> i64 { - let unnamed = tls.tpoff(exe_base_offset as u64, r_addend); - if r_sym == 0 { - return unnamed; - } - let Some(sym) = exe.symbol(backing, r_sym) else { - return unnamed; - }; - if sym.is_defined() { - return tls.tpoff(exe_base_offset as u64 + sym.value, r_addend); - } - - let name = toyos_elf::cstr(&exe.dynstr, sym.name as u64); - // `defining_module` returning `None` means a lib resolved the symbol but - // has no TLS module in the combined block — an inconsistency, refused - // rather than guessed at with base_offset 0. - match elf::defining_module(name, tls_info) { - Some((module, sym_offset)) => { - tls.tpoff(module.base_offset as u64 + sym_offset, r_addend) - } - None => { - log!("tpoff: unresolved exe TLS symbol: {}", name); - 0 +) -> Result { + let (base_offset, at) = match r { + TlsRef::Own(at) => (exe_base_offset, at), + TlsRef::Symbol(s) => { + let sym = exe.symbol(backing, s.sym()).ok_or(RelocError::SymbolPastTable)?; + if sym.is_defined() { + let memsz = layout.tls_memsz().ok_or(RelocError::TlsOutsideSegment)?; + (exe_base_offset, s.offset_in(&sym, memsz).ok_or(RelocError::TlsOutsideSegment)?) + } else { + let name = sym.name_in(&exe.dynstr); + // `None` means a lib resolved the symbol but has no TLS module + // in the combined block — an inconsistency, logged rather than + // guessed at with base_offset 0. + let Some((module, defined)) = elf::defining_module(name, tls_info) else { + log!("tpoff: unresolved exe TLS symbol: {}", name); + return Ok(0); + }; + let at = s.offset_in(&defined, module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + (module.base_offset, at) + } } - } + }; + tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows) } /// The one program the kernel starts. `src/build.rs` puts this binary in every diff --git a/kernel/src/loader/symbols.rs b/kernel/src/loader/symbols.rs index 48121f07257..b3e90add22a 100644 --- a/kernel/src/loader/symbols.rs +++ b/kernel/src/loader/symbols.rs @@ -14,40 +14,53 @@ use crate::process::PageAlloc; use crate::symbols::SymbolTable; use crate::UserAddr; use toyos_elf::section::{SectionTable, SHT_SYMTAB}; -use toyos_elf::sym::SymTab; -use toyos_elf::Layout; +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>, base: UserAddr) -> BTreeMap<&'a str, UserAddr> { - map(symbols, base, |_| true) +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>, base: UserAddr) -> BTreeMap<&'a str, UserAddr> { - map(symbols, base, |s: &toyos_elf::Sym| s.is_exported()) +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>, - base: UserAddr, + image_start: UserAddr, + extent: Extent, keep: impl Fn(&toyos_elf::Sym) -> bool, -) -> BTreeMap<&'a str, UserAddr> { +) -> Result, Error> { let mut map = BTreeMap::new(); for (i, sym) in symbols.defined() { let name = symbols.name(i); - if !name.is_empty() && keep(&sym) { - map.insert(name, base + sym.value); + 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()); } - map + Ok(map) } /// `.symtab` and its `.strtab`, read whole — the fallback for a PIE that /// exports nothing through `.dynsym`. pub fn read_symtab(backing: &dyn FileBacking, layout: &Layout) -> Option<(Vec, Vec)> { - let table = layout.section_headers?; + 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)?; @@ -80,7 +93,7 @@ pub fn read_backtrace_table( ) -> SymbolTable { let empty = || SymbolTable::empty_with_bounds(prog_base, prog_end, stack_base, stack_end); - let Some(table) = layout.section_headers else { return empty() }; + let Some(table) = layout.section_headers() else { return empty() }; let shdrs = super::read_file_range(backing, table.file_offset, table.byte_len()); let Some((syms, strs)) = SectionTable::new(&shdrs).symbols(SHT_SYMTAB) else { return empty(); diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index 2ddd19c4e69..cb17bc829d0 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -1,11 +1,14 @@ -//! A thread's TLS block, in this machine's psABI variant with the DTV in front of it, built -//! holding physical addresses that `rebase_block` shifts once the block is mapped. The layout +//! A thread's TLS block, in this machine's psABI variant with the DTV in front of it. The layout //! arithmetic is `toyos_elf::tls`; this is the allocation, the template copies, the TCB and the DTV. +//! +//! A block is built in frames no user mapping reaches ([`crate::process::Unpublished`]), holding +//! physical addresses, and rebased to the address it is given inside the one call that maps it: +//! a process's other threads never see a pointer the kernel has yet to fix, and the kernel never +//! reads the block once they can write it. use crate::elf::TlsModule; -use crate::mm::KernelSlice; -use crate::process::{OwnedAlloc, PageAlloc}; -use crate::DirectMap; +use crate::process::{MappedPages, OwnedAlloc, PageAlloc, PageTables, Unpublished}; +use crate::UserAddr; use toyos_elf::tls::{Static, Variant}; use toyos_elf::Layout; @@ -22,30 +25,46 @@ const DTV_BYTES: usize = DTV_HEADER_SIZE + DTV_INITIAL_CAPACITY * 8; /// A DTV slot for a module whose block has not been allocated yet. const DTV_UNALLOCATED: u64 = !0u64; -/// One module's TLS area, for a thread that has only the executable's. -pub fn setup_tls( - tls_template: Option, - tls_memsz: usize, - tls_align: usize, -) -> Option<(PageAlloc, u64)> { - let tls = Static::new(VARIANT, tls_memsz, tls_align, tls_align)?; - setup_combined_tls( - &[TlsModule { - template: tls_template, - memsz: tls_memsz, - base_offset: 0, - module_id: 1, - is_static: true, - }], - tls, - ) +/// One thread's TLS block, built and not yet mapped. +pub struct TlsBlock { + frames: Unpublished, + /// Where the thread pointer goes, from the block's first byte. + tp_offset: usize, } -/// One thread's TLS block for every static module; `None` when no allocation holds the layout. -pub fn setup_combined_tls(modules: &[TlsModule], tls: Static) -> Option<(PageAlloc, u64)> { +impl TlsBlock { + /// The block for every static module in `modules`, laid out by `tls`; a + /// DTV and TCB alone when there is none. `None` when no allocation holds + /// 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())?; + return build_combined(&[TlsModule { template: None, memsz: 0, base_offset: 0, module_id: 1, is_static: true }], alone); + } + build_combined(modules, tls) + } + + /// Map the block into `pt`, returning its mapping, the thread pointer, and + /// where that pointer lies in the block. `None`, with the frames freed, + /// when `pt` has no room. + pub fn publish(self, pt: &PageTables) -> Option<(MappedPages, u64, usize)> { + let tp_offset = self.tp_offset; + let pages = self.frames.publish(pt, crate::mm::paging::Prot::ReadWrite, |frames, at| { + // SAFETY: `frames` is the block `build_combined` wrote, reachable + // by no mapping until `publish` maps it after this returns. + unsafe { rebase(frames, tp_offset, at) } + })?; + let fs_base = (pages.vaddr() + tp_offset as u64).raw(); + Some((pages, fs_base, tp_offset)) + } +} + +/// Every static module's template copied in, and the TCB and DTV pointing at +/// the block's physical address, which [`rebase`] moves once it has another. +fn build_combined(modules: &[TlsModule], tls: Static) -> Option { let plan = tls.plan(TCB_SIZE, DTV_BYTES, crate::mm::PAGE_2M as usize)?; - let page_alloc = PageAlloc::new(plan.alloc_size, crate::mm::pmm::Category::InitTls)?; - let block = page_alloc.ptr(); + let frames = Unpublished::new(PageAlloc::new(plan.alloc_size, crate::mm::pmm::Category::InitTls)?); + let block = frames.ptr(); // SAFETY: `block` is the fresh, unpublished `plan.alloc_size`-byte allocation above. unsafe { @@ -65,8 +84,8 @@ pub fn setup_combined_tls(modules: &[TlsModule], tls: Static) -> Option<(PageAll } } - let block_phys = DirectMap::from_ptr(block).phys(); - let tp_user = block_phys + plan.tp_offset as u64; + let block_phys = frames.phys(); + let tp_phys = block_phys + plan.tp_offset as u64; // SAFETY: the plan reserves `TCB_SIZE` bytes at `tp_offset` inside `alloc_size`. let tp_kernel = unsafe { block.add(plan.tp_offset) } as *mut u64; // The thread's id (`toyos_abi::TCB_TID`) is TP+16 on variant II and TP+8 on @@ -78,7 +97,7 @@ pub fn setup_combined_tls(modules: &[TlsModule], tls: Static) -> Option<(PageAll match VARIANT { // TP+0 the psABI self-pointer, TP+8 the DTV pointer. Variant::II => { - *tp_kernel = tp_user; + *tp_kernel = tp_phys; *tp_kernel.add(1) = block_phys; } // TP+0 the DTV pointer, TP+8 the implementation's word, the tid (zeroed above). @@ -103,31 +122,38 @@ pub fn setup_combined_tls(modules: &[TlsModule], tls: Static) -> Option<(PageAll } } - Some((page_alloc, tp_user)) + Some(TlsBlock { frames, tp_offset: plan.tp_offset }) } -/// Rebase a fresh TLS block's self-referential pointers from physical to virtual, in place. -/// No Rust type expresses a DTV whose entries point into itself; this models the psABI layout directly, the same untyped-by-nature work `elf::reloc` does one level down. +/// Move the block's self-referential pointers from its physical address to +/// `at`, in place. +/// +/// No Rust type expresses a DTV whose entries point into itself; this models the psABI layout +/// directly. The walk is `DTV_INITIAL_CAPACITY` entries, the builder's own constant: the DTV's +/// length word is the process's to rewrite once the block is mapped, and is never read here. +/// /// # Safety -/// `phys`/`tp_offset` name the block `setup_combined_tls` just built; nothing else touches it until this returns. -pub(crate) unsafe fn rebase_block(phys: u64, tp_offset: usize, fs_base: u64, rebase: i64) { - // SAFETY: the caller's contract; word 1's DTV length is the builders' own, never userland's. +/// `frames` is a block [`build_combined`] wrote with its thread pointer at `tp_offset`, and no +/// mapping reaches it. +unsafe fn rebase(frames: &Unpublished, tp_offset: usize, at: UserAddr) { + let phys = frames.phys(); + let moved = |p: u64| (at + (p - phys)).raw(); + // SAFETY: the caller's contract; every access is inside the TCB and DTV the builder reserved. unsafe { - let block = DirectMap::from_phys(phys).as_mut_ptr::(); + let block = frames.ptr(); let tp = block.add(tp_offset) as *mut u64; match VARIANT { Variant::II => { - *tp = fs_base; - *tp.add(1) = (*tp.add(1) as i64 + rebase) as u64; + *tp = moved(*tp); + *tp.add(1) = moved(*tp.add(1)); } - Variant::I => *tp = (*tp as i64 + rebase) as u64, + Variant::I => *tp = moved(*tp), } let dtv = block as *mut u64; - let dtv_len = *dtv.add(1) as usize; - for i in 0..dtv_len { + for i in 0..DTV_INITIAL_CAPACITY { let entry = *dtv.add(2 + i); - if entry != DTV_UNALLOCATED && entry != 0 { - *dtv.add(2 + i) = (entry as i64 + rebase) as u64; + if entry != DTV_UNALLOCATED { + *dtv.add(2 + i) = moved(entry); } } } @@ -135,26 +161,12 @@ pub(crate) unsafe fn rebase_block(phys: u64, tp_offset: usize, fs_base: u64, reb /// Build one thread's TLS block and map it into the child address space; `None` when either fails. pub fn map_block( - child_pt: &crate::process::PageTables, + child_pt: &PageTables, modules: &[TlsModule], tls: Static, -) -> Option<(crate::process::MappedPages, u64)> { - let (alloc, fs_base) = if tls.total_memsz() > 0 { - setup_combined_tls(modules, tls)? - } else { - setup_tls(None, 0, 1)? - }; - - let phys = alloc.phys(); - let (vaddr, _) = crate::process::vma_map(child_pt, phys, alloc.size() as u64, - crate::mm::paging::Prot::ReadWrite)?; - let rebase = vaddr.raw() as i64 - phys as i64; - let fs_base = (fs_base as i64 + rebase) as u64; - // SAFETY: nothing runs in the unscheduled child yet, and `fs_base - vaddr` is the `tp_offset` the builder bounded. - unsafe { - rebase_block(phys, (fs_base - vaddr.raw()) as usize, fs_base, rebase); - } - Some((crate::process::MappedPages::new(vaddr, alloc), fs_base)) +) -> Option<(MappedPages, u64)> { + let (pages, fs_base, _) = TlsBlock::build(modules, tls)?.publish(child_pt)?; + Some((pages, fs_base)) } /// 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. @@ -167,17 +179,17 @@ pub fn build_tls_layout( ) -> 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.filter(|t| t.memsz > 0) { + let exe = match layout.tls().filter(|t| t.memsz() > 0) { None => None, Some(tls) => { - let (memsz, align) = (tls.memsz as usize, tls.align as usize); + 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.filesz as usize)), memsz, placed, align, 1)) + Some((exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)), memsz, placed, align, 1)) } }; let libs = loaded_libs diff --git a/kernel/src/mm/region.rs b/kernel/src/mm/region.rs index 3020d9102ea..c868fcb2aca 100644 --- a/kernel/src/mm/region.rs +++ b/kernel/src/mm/region.rs @@ -51,13 +51,7 @@ impl KernelSlice { "KernelSlice OOB: offset={:#x} len={} size={:#x}", offset, len, self.size); } - /// # Safety: nothing may concurrently write `size_of::()` bytes at `offset` while this read runs. - pub unsafe fn read(&self, offset: usize) -> T { - self.check(offset, core::mem::size_of::()); - core::ptr::read_unaligned(self.base.add(offset) as *const T) - } - - /// # Safety: same as `read`, plus nothing else may concurrently read or write this range. + /// # Safety: nothing else may concurrently read or write `size_of::()` bytes at `offset`. pub unsafe fn write(&self, offset: usize, value: T) { self.check(offset, core::mem::size_of::()); core::ptr::write_unaligned(self.base.add(offset) as *mut T, value); diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 072cba886a7..24870d86751 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -22,9 +22,7 @@ use crate::sched::payload::ThreadSched; use crate::time::{Deadline, Duration}; use crate::{elf, pipe, scheduler}; use crate::UserAddr; -use crate::loader::{ - setup_tls, setup_combined_tls, alloc_kernel_stack, thread_start, rebase_block, -}; +use crate::loader::{alloc_kernel_stack, thread_start, TlsBlock}; pub use toyos_abi::{Pid, Tid}; pub use crate::scheduler::TaskId; @@ -133,6 +131,46 @@ unsafe impl crate::mm::Allocation for PageAlloc { } +/// Frames no user mapping reaches yet. +/// +/// The kernel builds what a process will see in these while they are its +/// alone, and [`publish`](Self::publish) is the only way to map them: it +/// consumes this, and runs the caller's last fix-up — the one that needs the +/// address the frames will have — under the address-space lock that then maps +/// them. Nothing is written into the frames with a mapping in place, and +/// nothing is read back out of them once one is. +pub struct Unpublished(PageAlloc); + +impl Unpublished { + pub fn new(frames: PageAlloc) -> Self { + Self(frames) + } + + /// Kernel pointer to the start, via the direct map; the frames are this + /// value's alone, so writes through it race nothing. + pub fn ptr(&self) -> *mut u8 { + self.0.ptr() + } + + pub fn phys(&self) -> u64 { + self.0.phys() + } + + /// Choose an address in `pt`, hand it to `fix`, then map the frames there + /// at `prot` — all under one hold of `pt`'s lock, so no thread of the + /// process can reach the frames before `fix` returns. `None`, with the + /// frames freed, when `pt` has no room. + pub fn publish(self, pt: &PageTables, prot: Prot, fix: impl FnOnce(&Self, UserAddr)) -> Option { + let size = self.0.size() as u64; + let mut space = pt.lock(); + let at = space.alloc_region(size, crate::vma::RegionKind::Mapped)?; + fix(&self, at); + space.map_range(at, self.0.phys(), size, prot, CachePolicy::Normal); + drop(space); + Some(MappedPages::new(at, self.0)) + } +} + /// Pages handed to userland through [`vma_map`], paired with the mapping address so the two are unmapped together; dropping without unmapping is sound only when the address space itself is being destroyed. pub struct MappedPages { vaddr: UserAddr, @@ -447,10 +485,9 @@ pub struct ElfInfo { /// RELATIVE relocation index for demand-paged ELF (applied per-page on fault). pub reloc_index: Option>, pub elf_base: UserAddr, - /// Executable .eh_frame_hdr vaddr (stated ELF vaddr, before base offset). - pub exe_eh_frame_hdr_vaddr: u64, - pub exe_eh_frame_hdr_size: u64, - /// Executable virtual address extent (elf_base + vaddr_max - vaddr_min). + /// The executable's `.eh_frame_hdr` as (address, size), `(0, 0)` without one. + pub exe_eh_frame_hdr: (u64, u64), + /// One past the executable's last byte. pub exe_vaddr_max: u64, /// Paths of dlopen'd libraries (parallel to loaded_libs). pub lib_paths: Vec, @@ -468,8 +505,7 @@ impl ElfInfo { loaded_libs: Vec::new(), reloc_index: None, elf_base: UserAddr::new(0), - exe_eh_frame_hdr_vaddr: 0, - exe_eh_frame_hdr_size: 0, + exe_eh_frame_hdr: (0, 0), exe_vaddr_max: 0, lib_paths: Vec::new(), } @@ -834,28 +870,19 @@ pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Op (data.elf.tls_modules.clone(), data.elf.tls) }; - // Phase 2: allocate TLS outside any lock. An empty module set still gets a DTV+TCB block via `setup_tls(None, 0, ..)`. - let (tls_alloc, fs_base) = if !tls_modules.is_empty() { - setup_combined_tls(&tls_modules, tls)? - } else { - setup_tls(None, 0, tls.max_align())? - }; - let (tls_alloc, fs_base, tcb_phys) = { - let addr_space = &parent_addr_space; + // Phase 2: build the TLS block outside any lock, then publish it into the + // running parent — whose other threads may already be touching memory the + // block's address is chosen from, so every pointer in it is final before + // the mapping exists. + let block = TlsBlock::build(&tls_modules, tls)?; + let (tls_alloc, fs_base, tp_offset) = { let parent_data = process_data_arc.lock(); - let tls_phys = tls_alloc.phys(); - // VA exhaustion is a resource failure the process caused, not a kernel bug; `tls_alloc` drops on the way out, returning its pages. - let (tls_vaddr, _) = vma_map(addr_space, tls_phys, tls_alloc.size() as u64, Prot::ReadWrite)?; - let tls_rebase = tls_vaddr.raw() as i64 - tls_phys as i64; - let fs_base = (fs_base as i64 + tls_rebase) as u64; - // SAFETY: `tls_alloc` is freshly built and solely owned by this scope; `vma_map` has published only its virtual address, which no not-yet-created thread names yet. Runs under the process-data lock. - unsafe { - rebase_block(tls_phys, (fs_base - tls_vaddr.raw()) as usize, fs_base, tls_rebase); - } + // VA exhaustion is a resource failure the process caused, not a kernel bug; the block drops on the way out, returning its pages. + let published = block.publish(&parent_addr_space)?; drop(parent_data); - let tcb_phys = tls_phys + (fs_base - tls_vaddr.raw()); - (MappedPages::new(tls_vaddr, tls_alloc), fs_base, tcb_phys) + published }; + let tls_alloc_tcb = tls_alloc.ptr().wrapping_add(tp_offset); let (ks_alloc, ks_rsp) = match alloc_kernel_stack(thread_start, entry, stack_ptr, arg) { Some(ks) => ks, @@ -894,12 +921,12 @@ pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Op let tid = proc.threads.insert(ThreadEntry::new(thread_data)); // Before the thread's first instruction, which is the enqueue below: the // thread reads its own id here without a syscall (`toyos_abi::TCB_TID`). - // SAFETY: `tcb_phys` is this thread's TCB inside the TLS block the table - // now owns, which no thread has run on yet; a 4-byte store inside the - // TCB the builder reserved. + // SAFETY: `tp_offset` is this thread's TCB inside the TLS block the table + // now owns, which no thread has run on yet; a 4-byte volatile store inside + // the TCB the builder reserved, and a store is all the kernel does there. unsafe { core::ptr::write_volatile( - crate::DirectMap::from_phys(tcb_phys + toyos_abi::TCB_TID as u64).as_mut_ptr::(), + tls_alloc_tcb.add(toyos_abi::TCB_TID).cast::(), tid.raw(), ); } diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index c714c74c5f5..441098b38bf 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -257,11 +257,10 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init if matches!(lib.memory, crate::elf::LibMemory::Shared { .. }) { crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); } - let delta = vaddr.raw() as i64 - lib.user_base.raw() as i64; - if delta != 0 { - crate::elf::rebase_relative_relocs(&lib, delta); + if vaddr != lib.user_base { + lib.user_base = vaddr; + crate::elf::rebase_relative_relocs(&lib); } - lib.user_base = vaddr; Ok::(vaddr) }); let base = match mapped { @@ -288,19 +287,30 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init let data = data_arc.lock(); crate::elf::resolve_dlopen_relocs(&lib, &data.elf.loaded_libs); - if data.elf.tls.total_memsz() > 0 { - let tls_info = crate::elf::TlsModuleInfo { - libs: &data.elf.loaded_libs, - modules: &data.elf.tls_modules, - }; - crate::elf::apply_tpoff_relocs(&lib, 0, data.elf.tls, &tls_info); + // Every TLS value is resolved here, before the point of no return: a + // reference that leaves its module's segment refuses the whole load, + // and the mapping guard takes the module back down. + let tls_info = crate::elf::TlsModuleInfo { + libs: &data.elf.loaded_libs, + modules: &data.elf.tls_modules, + }; + let refused = if data.elf.tls.total_memsz() > 0 { + crate::elf::apply_tpoff_relocs(&lib, 0, data.elf.tls, &tls_info).err() + } else { + None + }; + let refused = refused.or_else(|| { + lib_has_tls.then(|| crate::elf::apply_dtpoff_relocs(&lib, &tls_info).err()).flatten() + }); + if let Some(refused) = refused { + log!("dlopen: {}: {}", resolved, refused.as_str()); + return SyscallError::InvalidArgument.to_u64(); } - // init_info layout: [init_array_vaddr, init_array_count], vaddr rebased to user_base. - [ - if lib.init_array_vaddr != 0 { lib.user_base.raw() + lib.init_array_vaddr } else { 0 }, - lib.init_array_size / 8, - ] + // init_info layout: [init_array address, init_array count]. + lib.init_array.map_or([0, 0], |array| { + [(lib.user_base + array.range().start().get()).raw(), array.count()] + }) }; // The point of no return: copy the init info out first, then register. A @@ -469,11 +479,11 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 { // packed after it in module order. let mut path_offset = (module_count * info_size) as u32; - let (eh_vaddr, eh_size) = (data.elf.exe_eh_frame_hdr_vaddr, data.elf.exe_eh_frame_hdr_size); + let (eh_addr, eh_size) = data.elf.exe_eh_frame_hdr; let exe_info = ModuleInfo { base: data.elf.elf_base.raw(), text_end: data.elf.exe_vaddr_max, - eh_frame_hdr: if eh_vaddr != 0 { data.elf.elf_base.raw() + eh_vaddr } else { 0 }, + eh_frame_hdr: eh_addr, eh_frame_hdr_size: eh_size, path_offset, path_len: exe_path_bytes.len() as u32, @@ -491,10 +501,8 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 { let lib_info = ModuleInfo { base: lib.user_base.raw(), text_end: lib.user_end(), - eh_frame_hdr: if lib.eh_frame_hdr_vaddr != 0 { - lib.user_base.raw() + lib.eh_frame_hdr_vaddr - } else { 0 }, - eh_frame_hdr_size: lib.eh_frame_hdr_size, + eh_frame_hdr: lib.eh_frame_hdr.map_or(0, |r| (lib.user_base + r.start().get()).raw()), + eh_frame_hdr_size: lib.eh_frame_hdr.map_or(0, |r| r.len()), path_offset, path_len: lib_path_bytes.len() as u32, }; diff --git a/src/build.rs b/src/build.rs index a65b75e2709..34dba99c00e 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1575,16 +1575,21 @@ fn syscall_entry_bytes(kernel: &[u8]) -> Result<&[u8], String> { )); }; let header = toyos_elf::FileHeader::parse(kernel).map_err(|e| format!("{e:?}"))?; + let extent = toyos_elf::Layout::parse(kernel, header.machine).map_err(|e| format!("{e}"))?.extent(); + let value = entry + .address(extent) + .map(|at| extent.min() + at.get()) + .ok_or("the kernel's `syscall_entry` lies outside its own image")?; let segments = header.program_headers(kernel).map_err(|e| format!("{e:?}"))?; (0..header.phnum as usize) .filter_map(|i| ProgramHeader::parse(segments, i)) .filter(|segment| segment.kind == PT_LOAD) .find_map(|segment| { - let within = entry.value.checked_sub(segment.vaddr)?; + let within = value.checked_sub(segment.vaddr)?; let left = segment.filesz.checked_sub(within).filter(|&left| left != 0)?; toyos_symbols::file_range(kernel, segment.offset.checked_add(within)?, left) }) - .ok_or_else(|| format!("no `PT_LOAD` holds `syscall_entry` at {:#x} in the file", entry.value)) + .ok_or_else(|| format!("no `PT_LOAD` holds `syscall_entry` at {value:#x} in the file")) } /// Whether an entry switches to the kernel's `rsp` in the instruction after it diff --git a/toyos-elf/src/dynamic.rs b/toyos-elf/src/dynamic.rs index 9384353d4ca..8422f05d95f 100644 --- a/toyos-elf/src/dynamic.rs +++ b/toyos-elf/src/dynamic.rs @@ -5,7 +5,8 @@ //! and the loader has to tell "the file did not say" from "the file said //! zero", because those two get different treatment at every use site. -use crate::read; +use crate::layout::{Extent, ImageRange}; +use crate::{read, Error}; /// A table named by a (location, size) pair of tags. /// @@ -94,6 +95,38 @@ impl Dynamic { } } +/// Bytes in one `.init_array` entry, an ELF64 address. +const POINTER_SIZE: u64 = 8; + +/// `DT_INIT_ARRAY` and `DT_INIT_ARRAYSZ`: a whole number of pointers, every +/// one of them inside the image. Made only by [`InitArray::parse`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InitArray { + range: ImageRange, +} + +impl InitArray { + /// The array `table` names, placed in the image `extent` describes, or why + /// it is no array of this image's pointers. No table is no array. + pub fn parse(table: Option, extent: Extent) -> Result, Error> { + let Some(table) = table else { return Ok(None) }; + if !table.size.is_multiple_of(POINTER_SIZE) { + return Err(Error::InitArrayNotWholePointers); + } + let range = extent.range(table.vaddr, table.size).ok_or(Error::InitArrayOutsideImage)?; + Ok(Some(InitArray { range })) + } + + pub const fn range(self) -> ImageRange { + self.range + } + + /// How many constructors the array holds. + pub const fn count(self) -> u64 { + self.range.len() / POINTER_SIZE + } +} + impl Table { fn from_tags(vaddr: Option, size: Option) -> Option
{ match (vaddr, size) { diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index 5b4f1716321..fe05e323f56 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -4,6 +4,10 @@ //! is effects. Its invariants are established once, in [`Layout::parse`], so //! that no consumer re-checks them and none of them can be forgotten at a call //! site. +//! +//! Every address it hands out is an [`ImageOffset`] or an [`ImageRange`]: +//! a position inside the image's own [`Extent`], which a loader adds to the +//! address it placed the image at. A raw `p_vaddr` never leaves this module. use crate::header::{ FileHeader, Machine, ProgramHeader, PT_DYNAMIC, PT_GNU_EH_FRAME, PT_LOAD, PT_TLS, @@ -11,6 +15,99 @@ use crate::header::{ }; use crate::{Error, MAX_LOAD_SEGMENTS, MAX_TLS_ALIGN}; +/// An image's loadable extent, `[min, max]` with `min <= max`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Extent { + min: u64, + max: u64, +} + +impl Extent { + /// `None` for `min > max`, which no image has. + pub const fn new(min: u64, max: u64) -> Option { + if min > max { + return None; + } + Some(Extent { min, max }) + } + + pub const fn min(self) -> u64 { + self.min + } + + pub const fn max(self) -> u64 { + self.max + } + + /// Bytes between the lowest and highest address the image claims. + pub const fn span(self) -> u64 { + self.max.abs_diff(self.min) + } + + /// Where `vaddr` lies in the image, when it lies in it at all. + /// + /// The end is inclusive: one past the last byte is an address a pointer may + /// name — `_end`, `&array[N]` — and a linker writes it as a `RELATIVE` + /// addend or a symbol value like any other. + pub const fn offset(self, vaddr: u64) -> Option { + match vaddr.checked_sub(self.min) { + Some(off) if vaddr <= self.max => Some(ImageOffset(off)), + _ => None, + } + } + + /// `[vaddr, vaddr + len)`, when every byte of it lies in the image. + pub const fn range(self, vaddr: u64, len: u64) -> Option { + let Some(start) = vaddr.checked_sub(self.min) else { return None }; + match vaddr.checked_add(len) { + Some(end) if end <= self.max => Some(ImageRange { start, len }), + _ => None, + } + } +} + +/// A position inside one image, `0..=span` from its lowest address: made only +/// by [`Extent::offset`] and [`ImageRange`]. +/// +/// A loader turns it into an address by adding the address it placed the +/// image at, and an image placed where its whole span fits makes that sum fit +/// too — so no consumer re-checks it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ImageOffset(u64); + +impl ImageOffset { + pub const fn get(self) -> u64 { + self.0 + } +} + +/// `[start, start + len)` inside one image, `start + len <= span`: made only +/// by [`Extent::range`] and the parse that builds a [`Layout`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ImageRange { + start: u64, + len: u64, +} + +impl ImageRange { + pub const fn start(self) -> ImageOffset { + ImageOffset(self.start) + } + + pub const fn len(self) -> u64 { + self.len + } + + pub const fn is_empty(self) -> bool { + self.len == 0 + } + + /// One past the last byte, also inside the image. + pub const fn end(self) -> ImageOffset { + ImageOffset(self.start.wrapping_add(self.len)) + } +} + /// `PF_X`, `PF_W`, `PF_R` as the file declared them. /// /// Kept whole rather than reduced to `writable` at parse time: protection is a @@ -31,60 +128,96 @@ impl SegmentFlags { } } -/// One `PT_LOAD` segment. -/// -/// `filesz <= memsz`, `vaddr + memsz` and `file_offset + filesz` both fit a -/// `u64`, and `[vaddr, vaddr + memsz)` is inside the layout's own -/// `[vaddr_min, vaddr_max)`. [`Layout::parse`] is the only constructor. +/// One `PT_LOAD` segment: [`Layout::parse`] is the only constructor. #[derive(Clone, Copy, Debug)] pub struct Segment { - pub vaddr: u64, - pub memsz: u64, - pub filesz: u64, - pub file_offset: u64, - pub flags: SegmentFlags, + image: ImageRange, + filesz: u64, + file_offset: u64, + flags: SegmentFlags, } impl Segment { + /// `[p_vaddr, p_vaddr + p_memsz)`, inside the image. + pub const fn image(&self) -> ImageRange { + self.image + } + + /// `p_filesz`, never above `p_memsz`. + pub const fn filesz(&self) -> u64 { + self.filesz + } + + /// `p_offset`; `p_offset + p_filesz` fits a `u64`. + pub const fn file_offset(&self) -> u64 { + self.file_offset + } + + pub const fn flags(&self) -> SegmentFlags { + self.flags + } + pub const fn writable(&self) -> bool { self.flags.writable() } - /// `[vaddr, vaddr + memsz)` rounded out to whole pages, expressed relative - /// to `origin`. + /// The segment rounded out to whole pages, in image offsets. /// - /// Relative because the loader rebases the image: with a page-aligned base, - /// rounding an image-relative offset and rounding the rebased address give - /// the same answer, and only the relative form is free of the base's own - /// arithmetic. - /// A round-up that would leave the last page is `u64::MAX` instead: the - /// only consumer is an overlap test, and a range that is too long can - /// report an overlap that is not there but never miss one that is. - pub fn page_range(&self, origin: u64, page_size: u64) -> (u64, u64) { + /// Image-relative because the loader rebases the image: with a page-aligned + /// placement, rounding an image offset and rounding the rebased address + /// give the same answer, and only the relative form is free of the + /// placement's own arithmetic. A round-up that would leave the last page is + /// `u64::MAX` instead: the only consumer is an overlap test, and a range + /// that is too long can report an overlap that is not there but never miss + /// one that is. + pub fn page_range(&self, page_size: u64) -> (u64, u64) { debug_assert!(page_size.is_power_of_two()); - let mask = page_size - 1; - let start = self.vaddr - origin; - let end = start + self.memsz; + let mask = page_size.wrapping_sub(1); + let end = self.image.end().get(); let end_page = match end.checked_add(mask) { Some(rounded) => rounded & !mask, None => u64::MAX, }; - (start & !mask, end_page) + (self.image.start & !mask, end_page) } } /// A `PT_TLS` segment. /// -/// `align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], and -/// `filesz <= memsz`. Absent TLS is `None`, never a zero `memsz`: a module with -/// a `PT_TLS` of zero size still gets a DTV slot, and the two cases are not the -/// same question. +/// `align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], and the +/// file-backed template lies inside the image. Absent TLS is `None`, never a +/// zero `memsz`: a module with a `PT_TLS` of zero size still gets a DTV slot, +/// and the two cases are not the same question. #[derive(Clone, Copy, Debug)] pub struct TlsSegment { - pub vaddr: u64, - pub filesz: u64, - pub memsz: u64, - pub align: u64, + template: ImageRange, + memsz: u64, + align: u64, +} + +impl TlsSegment { + /// The `.tdata` bytes, `p_filesz` of them at `p_vaddr`. + pub const fn template(&self) -> ImageRange { + self.template + } + + /// `.tdata` plus `.tbss`, never below the template's length. `.tbss` + /// occupies address space no `PT_LOAD` need cover, so this is not bounded + /// by the image. + pub const fn memsz(&self) -> u64 { + self.memsz + } + + pub const fn align(&self) -> u64 { + self.align + } +} + +/// `PT_DYNAMIC`, where the file holds it and where the image does. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DynamicSegment { + pub file_offset: u64, + pub image: ImageRange, } /// Where the section header table is, when the file has a usable one. @@ -118,33 +251,37 @@ impl SectionTableRef { /// - one to [`MAX_LOAD_SEGMENTS`] `PT_LOAD` segments, each with /// `filesz <= memsz` and neither `vaddr + memsz` nor `file_offset + filesz` /// overflowing; -/// - `vaddr_min` is the smallest `p_vaddr` and `vaddr_max` the largest -/// `p_vaddr + p_memsz` over those segments, so `[vaddr_min, vaddr_max)` -/// covers every one of them and `vaddr_max - vaddr_min` cannot underflow; -/// - `entry`, the file-backed extent of `PT_TLS`, and all of `PT_DYNAMIC` and -/// `PT_GNU_EH_FRAME`, lie inside `[vaddr_min, vaddr_max)`; -/// - `tls.align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], so -/// that `!(align - 1)` is a mask and the TLS block's size cannot be dominated -/// by a number the file chose. +/// - the extent runs from the smallest `p_vaddr` to the largest +/// `p_vaddr + p_memsz` over those segments, so it covers every one of them; +/// - the entry point, the file-backed extent of `PT_TLS`, and all of +/// `PT_DYNAMIC` and `PT_GNU_EH_FRAME`, lie inside the extent; +/// - the TLS alignment is zero or a power of two no larger than +/// [`MAX_TLS_ALIGN`], so that `!(align - 1)` is a mask and the TLS block's +/// size cannot be dominated by a number the file chose. /// /// Downstream every size pair is a (copy length, destination size) pair — an /// allocation of `memsz` then a copy of `filesz` — so `filesz <= memsz` is a -/// memory-safety invariant here and not an ELF formality. Likewise the vaddr -/// ranges: a module's image is addressed as `vaddr - vaddr_min`, which is a -/// wrapping subtraction on anything outside them. +/// memory-safety invariant here and not an ELF formality. #[derive(Clone, Debug)] pub struct Layout { - pub entry: u64, - pub vaddr_min: u64, - pub vaddr_max: u64, + extent: Extent, + entry: ImageOffset, segments: [Segment; MAX_LOAD_SEGMENTS], segment_count: usize, - pub tls: Option, - /// `PT_DYNAMIC` as (file offset, vaddr, size). - pub dynamic: Option<(u64, u64, u64)>, - pub section_headers: Option, - /// `PT_GNU_EH_FRAME` as (vaddr, memsz), for DWARF unwinding. - pub eh_frame_hdr: Option<(u64, u64)>, + tls: Option, + dynamic: Option, + section_headers: Option, + eh_frame_hdr: Option, +} + +/// A `PT_LOAD` as read, before the extent it belongs to is known. +#[derive(Clone, Copy)] +struct RawLoad { + vaddr: u64, + memsz: u64, + filesz: u64, + file_offset: u64, + flags: u32, } impl Layout { @@ -160,13 +297,8 @@ impl Layout { } let phdrs = ehdr.program_headers(data)?; - let mut segments = [Segment { - vaddr: 0, - memsz: 0, - filesz: 0, - file_offset: 0, - flags: SegmentFlags(0), - }; MAX_LOAD_SEGMENTS]; + let mut loads = [RawLoad { vaddr: 0, memsz: 0, filesz: 0, file_offset: 0, flags: 0 }; + MAX_LOAD_SEGMENTS]; let mut segment_count = 0usize; let mut vaddr_min = u64::MAX; let mut vaddr_max = 0u64; @@ -174,7 +306,7 @@ impl Layout { let mut dynamic = None; let mut eh_frame_hdr = None; - for i in 0..ehdr.phnum as usize { + for i in 0..usize::from(ehdr.phnum) { let Some(phdr) = ProgramHeader::parse(phdrs, i) else { return Err(Error::ProgramHeadersOutsideBuffer); }; @@ -190,107 +322,142 @@ impl Layout { if phdr.offset.checked_add(phdr.filesz).is_none() { return Err(Error::FileExtentOverflows); } - if segment_count == MAX_LOAD_SEGMENTS { - return Err(Error::TooManyLoadSegments); - } - vaddr_min = vaddr_min.min(phdr.vaddr); - vaddr_max = vaddr_max.max(seg_end); - segments[segment_count] = Segment { + let slot = loads.get_mut(segment_count).ok_or(Error::TooManyLoadSegments)?; + *slot = RawLoad { vaddr: phdr.vaddr, memsz: phdr.memsz, filesz: phdr.filesz, file_offset: phdr.offset, - flags: SegmentFlags(phdr.flags), + flags: phdr.flags, }; - segment_count += 1; + segment_count = segment_count.wrapping_add(1); + vaddr_min = vaddr_min.min(phdr.vaddr); + vaddr_max = vaddr_max.max(seg_end); } // Last one wins, as it does for every other singleton header: // a file with two of these is malformed and the loader has no // better answer than a consistent one. - PT_TLS => { - tls = Some(TlsSegment { - vaddr: phdr.vaddr, - filesz: phdr.filesz, - memsz: phdr.memsz, - align: phdr.align, - }) - } - PT_DYNAMIC => dynamic = Some((phdr.offset, phdr.vaddr, phdr.filesz)), - PT_GNU_EH_FRAME => eh_frame_hdr = Some((phdr.vaddr, phdr.memsz)), + PT_TLS => tls = Some(phdr), + PT_DYNAMIC => dynamic = Some(phdr), + PT_GNU_EH_FRAME => eh_frame_hdr = Some(phdr), _ => {} } } - if segment_count == 0 { - return Err(Error::NoLoadSegments); + let extent = Extent::new(vaddr_min, vaddr_max).ok_or(Error::NoLoadSegments)?; + + let blank = Segment { + image: ImageRange { start: 0, len: 0 }, + filesz: 0, + file_offset: 0, + flags: SegmentFlags(0), + }; + let mut segments = [blank; MAX_LOAD_SEGMENTS]; + for (slot, raw) in segments.iter_mut().zip(loads.iter().take(segment_count)) { + // Inside by construction: the extent is the hull of these. + let image = extent.range(raw.vaddr, raw.memsz).ok_or(Error::SegmentExtentOverflows)?; + *slot = Segment { + image, + filesz: raw.filesz, + file_offset: raw.file_offset, + flags: SegmentFlags(raw.flags), + }; } - let layout = Layout { - entry: ehdr.entry, - vaddr_min, - vaddr_max, + // Every other program header names a vaddr the loader turns into an + // offset into the image. Outside the extent that is a wrapping + // subtraction into an out-of-bounds pointer, so bound them here rather + // than at each use site. Only the file-backed part of `PT_TLS` is + // checked: `.tbss` occupies address space the containing `PT_LOAD` need + // not cover, and it is never read from, only zeroed in a buffer of its + // own. + let entry = extent + .range(ehdr.entry, 1) + .ok_or(Error::EntryOutsideImage)? + .start(); + let tls = match tls { + None => None, + Some(t) => { + let template = extent.range(t.vaddr, t.filesz).ok_or(Error::TlsOutsideImage)?; + // `p_align` reaches the TLS block as both an addend to its size + // and the mask `!(align - 1)`. Neither survives an arbitrary + // u64: the addition overflows, and a non-power-of-two turns the + // mask into noise that can place the TLS data on top of the DTV. + // Zero and one mean "no alignment constraint". + if t.align > MAX_TLS_ALIGN || !(t.align == 0 || t.align.is_power_of_two()) { + return Err(Error::BadTlsAlign); + } + Some(TlsSegment { template, memsz: t.memsz, align: t.align }) + } + }; + let dynamic = match dynamic { + None => None, + Some(d) => Some(DynamicSegment { + file_offset: d.offset, + image: extent.range(d.vaddr, d.filesz).ok_or(Error::DynamicOutsideImage)?, + }), + }; + let eh_frame_hdr = match eh_frame_hdr { + None => None, + Some(e) => Some(extent.range(e.vaddr, e.memsz).ok_or(Error::EhFrameOutsideImage)?), + }; + + Ok(Layout { + extent, + entry, segments, segment_count, tls, dynamic, section_headers: section_table(&ehdr), eh_frame_hdr, - }; - - // Every other program header names a vaddr the loader turns into an - // offset into the image as `vaddr - vaddr_min`. Outside - // `[vaddr_min, vaddr_max)` that is a wrapping subtraction into an - // out-of-bounds pointer, so bound them here rather than at each use - // site. Only the file-backed part of `PT_TLS` is checked: `.tbss` - // occupies address space the containing `PT_LOAD` need not cover, and - // it is never read from, only zeroed in a buffer of its own. - if !layout.contains(layout.entry, 1) { - return Err(Error::EntryOutsideImage); - } - if let Some(tls) = layout.tls { - if !layout.contains(tls.vaddr, tls.filesz) { - return Err(Error::TlsOutsideImage); - } - // `p_align` reaches the TLS block as both an addend to its size and - // the mask `!(align - 1)`. Neither survives an arbitrary u64: the - // addition overflows, and a non-power-of-two turns the mask into - // noise that can place the TLS data on top of the DTV. Zero and one - // mean "no alignment constraint". - if tls.align > MAX_TLS_ALIGN || !(tls.align == 0 || tls.align.is_power_of_two()) { - return Err(Error::BadTlsAlign); - } - } - if let Some((_, vaddr, size)) = layout.dynamic { - if !layout.contains(vaddr, size) { - return Err(Error::DynamicOutsideImage); - } - } - if let Some((vaddr, size)) = layout.eh_frame_hdr { - if !layout.contains(vaddr, size) { - return Err(Error::EhFrameOutsideImage); - } - } - - Ok(layout) + }) } pub fn segments(&self) -> &[Segment] { - &self.segments[..self.segment_count] + self.segments.get(..self.segment_count).unwrap_or(&[]) + } + + /// `[vaddr_min, vaddr_max]`, the bound every address the file names is + /// held to. + pub const fn extent(&self) -> Extent { + self.extent } /// Bytes between the lowest and highest address any segment claims. pub const fn span(&self) -> u64 { - self.vaddr_max - self.vaddr_min + self.extent.span() } - /// Whether `[vaddr, vaddr + size)` is inside the loadable image. - pub fn contains(&self, vaddr: u64, size: u64) -> bool { - vaddr >= self.vaddr_min - && vaddr.checked_add(size).is_some_and(|end| end <= self.vaddr_max) + /// `e_entry`: inside the image, with at least one byte after it. + pub const fn entry(&self) -> ImageOffset { + self.entry } - /// The writable window `[lo, hi)` in image-relative coordinates, or `None` - /// when no segment is writable. + pub const fn tls(&self) -> Option { + self.tls + } + + /// The `PT_TLS` `p_memsz`, when the image has a TLS segment. + pub fn tls_memsz(&self) -> Option { + self.tls.map(|t| t.memsz) + } + + pub const fn dynamic(&self) -> Option { + self.dynamic + } + + pub const fn section_headers(&self) -> Option { + self.section_headers + } + + /// `PT_GNU_EH_FRAME`, for DWARF unwinding. + pub const fn eh_frame_hdr(&self) -> Option { + self.eh_frame_hdr + } + + /// The writable window `[lo, hi)` in image offsets, or `None` when no + /// segment is writable. /// /// This is the extent a relocation may write into once the module's /// read-only pages are shared between processes: past it the write would @@ -301,8 +468,7 @@ impl Layout { if !seg.writable() { continue; } - let lo = seg.vaddr - self.vaddr_min; - let hi = lo + seg.memsz; + let (lo, hi) = (seg.image.start().get(), seg.image.end().get()); window = Some(match window { Some((w_lo, w_hi)) => (w_lo.min(lo), w_hi.max(hi)), None => (lo, hi), @@ -320,9 +486,9 @@ impl Layout { pub fn overlapping_load_pages(&self, page_size: u64) -> Option<(usize, usize)> { let segs = self.segments(); for (i, a) in segs.iter().enumerate() { - let (a_start, a_end) = a.page_range(self.vaddr_min, page_size); - for (j, b) in segs.iter().enumerate().skip(i + 1) { - let (b_start, b_end) = b.page_range(self.vaddr_min, page_size); + let (a_start, a_end) = a.page_range(page_size); + for (j, b) in segs.iter().enumerate().skip(i.wrapping_add(1)) { + let (b_start, b_end) = b.page_range(page_size); if a_start < b_end && b_start < a_end { return Some((i, j)); } @@ -339,23 +505,31 @@ impl Layout { /// /// `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 or a program-header field, so the answer to "this address is - /// in no segment" is that the binary is malformed, not that the kernel - /// dies. + /// `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 vaddr >= seg.vaddr && vaddr - seg.vaddr < seg.filesz { - return seg.file_offset.checked_add(vaddr - seg.vaddr); + if let Some(within) = into(seg).filter(|&w| w < seg.filesz) { + return seg.file_offset.checked_add(within); } } - let mut best: Option<&Segment> = None; + let mut best: Option<(&Segment, u64)> = None; for seg in self.segments() { - if seg.vaddr <= vaddr && best.is_none_or(|b| seg.vaddr > b.vaddr) { - best = Some(seg); + if let Some(within) = into(seg) { + if best.is_none_or(|(_, w)| within < w) { + best = Some((seg, within)); + } } } - let seg = best?; - seg.file_offset.checked_add(vaddr - seg.vaddr) + 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 @@ -367,12 +541,19 @@ impl Layout { /// image covers `vaddr`. pub fn file_bytes_from(&self, vaddr: u64) -> Option { for seg in self.segments() { - if vaddr >= seg.vaddr && vaddr - seg.vaddr < seg.filesz { - return Some(seg.filesz - (vaddr - seg.vaddr)); + 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) + } } /// A section header table the loader can index, or `None`. @@ -381,7 +562,7 @@ impl Layout { /// read 64-byte fields out of each entry, so a smaller stride is a short read /// and a larger one is a table this crate does not know the shape of. fn section_table(ehdr: &FileHeader) -> Option { - if ehdr.shoff == 0 || ehdr.shnum == 0 || ehdr.shentsize as usize != SECTION_HEADER_SIZE { + if ehdr.shoff == 0 || ehdr.shnum == 0 || usize::from(ehdr.shentsize) != SECTION_HEADER_SIZE { return None; } Some(SectionTableRef { diff --git a/toyos-elf/src/lib.rs b/toyos-elf/src/lib.rs index 31507564069..46ee094f9e3 100644 --- a/toyos-elf/src/lib.rs +++ b/toyos-elf/src/lib.rs @@ -42,13 +42,17 @@ pub mod section; pub mod sym; pub mod tls; -pub use dynamic::{Dynamic, Table}; +pub use dynamic::{Dynamic, InitArray, Table}; pub use gnu_hash::GnuHash; pub use header::{FileHeader, Machine}; -pub use layout::{Layout, Segment, SegmentFlags, SectionTableRef, StackedImage, TlsSegment}; -pub use rela::{Rela, RelaCounts, RelaTable, RelocError, RelocKind}; +pub use layout::{ + DynamicSegment, Extent, ImageOffset, ImageRange, Layout, Segment, SegmentFlags, + SectionTableRef, StackedImage, TlsSegment, +}; +pub use rela::{Op, Rela, RelaCounts, RelaTable, Reloc, RelocError, RelocKind, Rules, TlsRef}; pub use section::{SectionHeader, SectionTable}; -pub use sym::{Sym, SymTab}; +pub use sym::{Sym, SymIndex, SymTab}; +pub use tls::TlsOffset; /// The most `PT_LOAD` segments an image may declare. /// @@ -131,6 +135,15 @@ pub enum Error { /// `PT_TLS` `p_align` is neither zero nor a power of two no larger than /// [`MAX_TLS_ALIGN`]. BadTlsAlign, + /// `DT_INIT_ARRAY` does not lie inside the loadable segments. + InitArrayOutsideImage, + /// `DT_INIT_ARRAYSZ` is not a whole number of pointers. + InitArrayNotWholePointers, + /// A defined symbol's `st_value` is no address inside the image. + SymbolOutsideImage, + /// A defined `STT_TLS` symbol's `st_value` is outside the module's + /// `PT_TLS`, or the module has none. + TlsSymbolOutsideSegment, } impl Error { @@ -158,6 +171,10 @@ impl Error { Error::DynamicOutsideImage => "ELF: PT_DYNAMIC outside the loadable segments", Error::EhFrameOutsideImage => "ELF: PT_GNU_EH_FRAME outside the loadable segments", Error::BadTlsAlign => "ELF: PT_TLS p_align is not a power of two within a page", + Error::InitArrayOutsideImage => "ELF: DT_INIT_ARRAY outside the loadable segments", + Error::InitArrayNotWholePointers => "ELF: DT_INIT_ARRAYSZ is not a whole number of pointers", + Error::SymbolOutsideImage => "ELF: a defined symbol's value is outside the image", + Error::TlsSymbolOutsideSegment => "ELF: a TLS symbol's value is outside its PT_TLS", } } } @@ -200,6 +217,12 @@ pub(crate) mod read { Some(u64::from_le_bytes(bytes::<8>(data, off)?)) } + /// Two consecutive little-endian `u32`s, low word first: `r_info`'s type + /// and symbol. + pub fn u32_pair_at(data: &[u8], off: usize) -> Option<[u32; 2]> { + Some([u32_at(data, off)?, u32_at(data, off.checked_add(4)?)?]) + } + pub fn i64_at(data: &[u8], off: usize) -> Option { Some(i64::from_le_bytes(bytes::<8>(data, off)?)) } diff --git a/toyos-elf/src/rela.rs b/toyos-elf/src/rela.rs index b5759eb1215..9f01f8ffd19 100644 --- a/toyos-elf/src/rela.rs +++ b/toyos-elf/src/rela.rs @@ -1,14 +1,23 @@ -//! `Elf64_Rela` tables, as a view over bytes. +//! `Elf64_Rela` tables, as a view over bytes, and the parse that turns one +//! entry into a [`Reloc`] a loader may act on. //! //! A relocation is an instruction to write `width` bytes at a file-chosen -//! offset with a file-influenced value, so this is the trust boundary's -//! sharpest edge. [`RelocKind::write_width`] is the one table the validator and -//! the writers both read: a type missing from it is a type nobody patches, and -//! a type in it that no writer handles would be validated for a write that -//! never happens. Neither can drift, because there is one table. +//! offset with a file-chosen value, so this is the trust boundary's sharpest +//! edge. A raw [`Rela`] exposes nothing but its kind: every number in it +//! reaches a loader only through [`parse`], which checks the destination +//! against the window the loader writes into and the value against the image +//! or TLS segment it must name, and answers with types that cannot hold +//! anything else ([`ImageOffset`], [`SymIndex`], [`TlsOffset`]). +//! +//! [`RelocKind::write_width`] is the one table the parse and the writers both +//! read: a type missing from it is a type nobody patches, and a type in it that +//! no writer handles would be checked for a write that never happens. use crate::header::Machine; +use crate::layout::{Extent, ImageOffset}; use crate::read; +use crate::sym::{Sym, SymIndex}; +use crate::tls::TlsOffset; /// Bytes in one `Elf64_Rela`. pub const ENTRY_SIZE: usize = 24; @@ -36,7 +45,7 @@ pub enum RelocKind { /// `R_X86_64_TPOFF32`; AArch64 has no 32-bit thread-pointer offset. Tpoff32, /// `R_AARCH64_TLSDESC`: a TLS descriptor, whose resolver this loader does - /// not have, so [`validate`] refuses it. + /// not have, so [`parse`] refuses it. TlsDesc, Other(u32), } @@ -70,27 +79,22 @@ impl RelocKind { RelocKind::TlsDesc | RelocKind::Other(_) => None, } } - - /// Whether resolving this type reads the symbol table. - /// - /// `Relative` is the one written type that does not, so it is also the one - /// whose `r_sym` needs no bound. - pub const fn needs_symbol(self) -> bool { - !matches!(self, RelocKind::Relative | RelocKind::Other(_)) - } - - /// Whether this type binds a symbol's address into a GOT slot. - pub const fn is_bind(self) -> bool { - matches!(self, RelocKind::GlobDat | RelocKind::JumpSlot) - } } +/// One `Elf64_Rela` as the file wrote it. Only its kind is readable: the +/// numbers in it are the file's, and they leave this module through [`parse`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Rela { - pub offset: u64, - pub sym: u32, - pub kind: RelocKind, - pub addend: i64, + offset: u64, + sym: u32, + kind: RelocKind, + addend: i64, +} + +impl Rela { + pub const fn kind(&self) -> RelocKind { + self.kind + } } /// A relocation table, addressed by entry rather than by byte. @@ -119,16 +123,13 @@ impl<'a> RelaTable<'a> { } pub fn get(&self, i: usize) -> Option { - if i >= self.len() { - return None; - } - let off = i * ENTRY_SIZE; - let info = read::u64_at(self.data, off + 8)?; + let off = i.checked_mul(ENTRY_SIZE)?; + let [r_type, r_sym] = read::u32_pair_at(self.data, off.checked_add(8)?)?; Some(Rela { offset: read::u64_at(self.data, off)?, - sym: (info >> 32) as u32, - kind: RelocKind::from_raw(self.machine, info as u32), - addend: read::i64_at(self.data, off + 16)?, + sym: r_sym, + kind: RelocKind::from_raw(self.machine, r_type), + addend: read::i64_at(self.data, off.checked_add(16)?)?, }) } @@ -139,6 +140,153 @@ impl<'a> RelaTable<'a> { } } +/// What one module's relocations are parsed against. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rules { + /// The image a `RELATIVE` value must point into. + pub extent: Extent, + /// Where writes may land, `[lo, hi)` in `r_offset`'s own coordinates. + pub window: (u64, u64), + /// Entries in the symbol table `r_sym` indexes. + pub sym_count: usize, + /// `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` `p_memsz`, which a TLS relocation with + /// `r_sym == 0` offsets into; `None` for a module with no TLS segment. + pub tls_memsz: Option, +} + +/// A relocation whose destination lies in its window and whose value names +/// what its kind says it names: made only by [`parse`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Reloc { + offset: u64, + op: Op, +} + +impl Reloc { + /// `r_offset`: `[offset, offset + width)` lies inside the window it was + /// parsed against, and inside one fill page for a chunked writer. + pub const fn offset(&self) -> u64 { + self.offset + } + + pub const fn op(&self) -> Op { + self.op + } +} + +/// What a [`Reloc`] writes, per psABI, with every file-chosen number already +/// bounded. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Op { + /// `B + A`, 8 bytes: the address the image was placed at plus a position + /// inside it. + Relative(ImageOffset), + /// `S`, 8 bytes: `GLOB_DAT` and `JUMP_SLOT`, bound to a symbol by name. + Bind(SymIndex), + /// `S + A - tp`, 8 bytes. + Tpoff64(TlsRef), + /// `S + A - tp`, 4 bytes, sign-extended by the instruction that reads it. + Tpoff32(TlsRef), + /// The id of the module defining the symbol, 8 bytes; `None` is `r_sym == + /// 0`, the relocating module itself. + DtpMod64(Option), + /// `S + A` within its module's TLS block, 8 bytes. + DtpOff64(TlsRef), +} + +/// The `S + A` of a TLS relocation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TlsRef { + /// `r_sym == 0`: an offset into the relocating module's own TLS segment. + Own(TlsOffset), + /// A symbol the loader resolves by name, and the addend it is offset by. + Symbol(TlsSymRef), +} + +/// A TLS relocation's symbol and addend: the sum is bounded only once the +/// symbol resolves, against the TLS segment of the module defining it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TlsSymRef { + sym: SymIndex, + addend: i64, +} + +impl TlsSymRef { + pub const fn sym(self) -> SymIndex { + self.sym + } + + /// `S + A`, where `defined` is the symbol as its defining module holds it + /// and `memsz` that module's `PT_TLS` size; `None` when the sum leaves the + /// segment. + pub fn offset_in(self, defined: &Sym, memsz: u64) -> Option { + defined.tls_offset(self.addend, memsz) + } +} + +/// Parse one entry against the module's [`Rules`]: `Ok(None)` for a type this +/// loader does not write, the reason for one it must not. +/// +/// Parsed ahead of the first write, not as each one happens: a module that is +/// refused halfway through has already been modified. +/// +/// The window is the *writable* one rather than the whole image: once the +/// module is cached its read-only pages are shared between processes, and the +/// write lands in a private allocation covering only that window. +pub fn parse(rela: Rela, rules: &Rules) -> Result, RelocError> { + let Some(width) = rela.kind.write_width() else { + return match rela.kind { + RelocKind::TlsDesc => Err(RelocError::TlsDescriptor), + _ => Ok(None), + }; + }; + let (lo, hi) = rules.window; + let end = rela.offset.checked_add(width).ok_or(RelocError::OffsetOverflows)?; + if rela.offset < lo || end > 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, rules.sym_count).ok_or(RelocError::SymbolPastTable); + let tls = || -> Result { + if rela.sym != 0 { + return Ok(TlsRef::Symbol(TlsSymRef { sym: sym()?, addend: rela.addend })); + } + rules + .tls_memsz + .and_then(|memsz| TlsOffset::of(0, rela.addend, memsz)) + .map(TlsRef::Own) + .ok_or(RelocError::TlsOutsideSegment) + }; + let op = match rela.kind { + RelocKind::Relative => Op::Relative( + u64::try_from(rela.addend) + .ok() + .and_then(|a| rules.extent.offset(a)) + .ok_or(RelocError::RelativeOutsideImage)?, + ), + RelocKind::GlobDat | RelocKind::JumpSlot => Op::Bind(sym()?), + RelocKind::Tpoff64 => Op::Tpoff64(tls()?), + RelocKind::Tpoff32 => Op::Tpoff32(tls()?), + RelocKind::DtpMod64 => Op::DtpMod64(if rela.sym == 0 { None } else { Some(sym()?) }), + RelocKind::DtpOff64 => Op::DtpOff64(tls()?), + RelocKind::TlsDesc | RelocKind::Other(_) => return Ok(None), + }; + Ok(Some(Reloc { offset: rela.offset, op })) +} + /// How many entries of each kind a set of tables holds. /// /// The loader reserves exactly from these instead of letting a `Vec` double: @@ -277,6 +425,15 @@ pub enum RelocError { /// A TLS descriptor, which only a resolver this loader does not have can /// fill. TlsDescriptor, + /// A `RELATIVE` addend that is no address inside the image: psABI `B + A` + /// with `A` a link-time address, and the image is all this module has. + RelativeOutsideImage, + /// A TLS offset outside the TLS segment it names, or in a module that has + /// none. + TlsOutsideSegment, + /// A thread-pointer offset no machine word, or for `TPOFF32` no 32-bit + /// field, holds. + TpoffOverflows, } impl RelocError { @@ -287,6 +444,9 @@ impl RelocError { 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", + RelocError::TpoffOverflows => "ELF: TPOFF value does not fit the field it is written to", } } } @@ -307,78 +467,38 @@ pub struct ReadTables { pub jmprel: (u64, u64), } -/// Refuse an image that puts a table the loader reads inside the window -/// relocations may write into. +/// Refuse an image that puts a table the loader reads on a `page` of the +/// window relocations may write into. /// /// A loader resolving symbols holds a `&[u8]` over `.dynsym` and `.dynstr`, and /// one over each relocation table it is iterating, across writes into the same -/// allocation. Disjointness is what makes those borrows sound. +/// allocation — and it parses those tables again after the image is mapped, +/// where a page the window touches is one the process may write. Disjointness +/// from every such page is what makes those borrows sound and the second parse +/// the first one's answer. /// /// **A conforming image never triggers this.** The ELF gABI gives `.dynsym`, /// `.dynstr`, `.rela.dyn` and `.rela.plt` `SHF_ALLOC` without `SHF_WRITE`, so a -/// linker places them in a non-writable segment and no part of them can be -/// inside the writable window. +/// linker places them in a non-writable segment, on pages of its own. pub fn tables_outside_window( tables: &ReadTables, window: (u64, u64), + page: u64, ) -> Result<(), &'static str> { + if window.1 <= window.0 { + return Ok(()); + } + let mask = page.wrapping_sub(1); + let window = (window.0 & !mask, window.1.checked_add(mask).map_or(u64::MAX, |e| e & !mask)); for (range, refusal) in [ - (tables.dynsym, "ELF: .dynsym lies inside the module's writable window"), - (tables.dynstr, "ELF: .dynstr lies inside the module's writable window"), - (tables.rela, "ELF: .rela.dyn lies inside the module's writable window"), - (tables.jmprel, "ELF: .rela.plt lies inside the module's writable window"), + (tables.dynsym, "ELF: .dynsym lies on a page of the module's writable window"), + (tables.dynstr, "ELF: .dynstr lies on a page of the module's writable window"), + (tables.rela, "ELF: .rela.dyn lies on a page of the module's writable window"), + (tables.jmprel, "ELF: .rela.plt lies on a page of the module's writable window"), ] { - let both_hold_bytes = range.1 > range.0 && window.1 > window.0; - if both_hold_bytes && range.0 < window.1 && window.0 < range.1 { + if range.1 > range.0 && range.0 < window.1 && window.0 < range.1 { return Err(refusal); } } Ok(()) } - -/// Check every entry the loader will ever write against the window it may write -/// into and the symbol table it may resolve through. -/// -/// Validated ahead of the first write, not as each one happens: a module that -/// is refused halfway through has already been modified, and a `DTPOFF64` with -/// `r_sym == 0` writes `r_addend` verbatim — so an unvalidated `r_offset` is an -/// arbitrary 8-byte write with a file-chosen value. -/// -/// The window is the *writable* one rather than the whole image: once the -/// module is cached its read-only pages are shared between processes, and the -/// write lands in a private allocation covering only that window. -/// `fill` is `Some` for a chunked writer (the exe), refusing a page-crossing -/// write; `None` for a contiguous one (a library). -pub fn validate( - entries: impl Iterator, - window: (u64, u64), - sym_count: usize, - fill: Option, -) -> Result<(), RelocError> { - let (lo, hi) = window; - for rela in entries { - if rela.kind == RelocKind::TlsDesc { - return Err(RelocError::TlsDescriptor); - } - let Some(width) = rela.kind.write_width() else { - continue; - }; - let end = rela - .offset - .checked_add(width) - .ok_or(RelocError::OffsetOverflows)?; - if rela.offset < lo || end > hi { - return Err(RelocError::OutsideWindow); - } - if rela.kind.needs_symbol() && rela.sym as usize >= sym_count { - return Err(RelocError::SymbolPastTable); - } - if let Some(fill) = fill { - let within = rela.offset.wrapping_sub(fill.base) % fill.granule; - if within + width > fill.granule { - return Err(RelocError::StraddlesFillPage); - } - } - } - Ok(()) -} diff --git a/toyos-elf/src/section.rs b/toyos-elf/src/section.rs index 23b73cd8dc7..2b9f2456d5e 100644 --- a/toyos-elf/src/section.rs +++ b/toyos-elf/src/section.rs @@ -139,7 +139,7 @@ impl<'a> SectionTable<'a> { { continue; } - if first_entry(sh.offset).is_some_and(|r| r.kind == crate::rela::RelocKind::Relative) { + if first_entry(sh.offset).is_some_and(|r| r.kind() == crate::rela::RelocKind::Relative) { return Some((sh.offset, sh.size)); } } diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index a33501f00ae..fbe4f212f33 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -1,12 +1,16 @@ //! `Elf64_Sym` tables, as a view over bytes. //! //! [`SymTab::get`] answers `None` past the end rather than reading a partial -//! record. The shape it replaces took a `&[u8]` and an index, sliced from -//! `index * 24` and read 24 bytes through a raw pointer: an index one short of -//! the end read past the buffer, and only the arithmetic at four separate call -//! sites kept it from happening. +//! record, and a [`Sym`]'s `st_value` is readable only as what it names: an +//! [`ImageOffset`] inside its module ([`Sym::address`]) or a [`TlsOffset`] +//! inside its module's TLS segment ([`Sym::tls_offset`]). The number itself is +//! the file's, and a loader that added it to a base unchecked was a kernel +//! panic away from any process that could write a file. +use crate::layout::{Extent, ImageOffset}; use crate::read; +use crate::tls::TlsOffset; +use crate::Error; /// Bytes in one `Elf64_Sym`. pub const ENTRY_SIZE: usize = 24; @@ -16,13 +20,32 @@ pub const STB_WEAK: u8 = 2; pub const STT_FUNC: u8 = 2; pub const STT_TLS: u8 = 6; +/// An `r_sym` below the symbol count it was parsed against: made only by the +/// relocation parse. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SymIndex(u32); + +impl SymIndex { + pub(crate) fn below(raw: u32, count: usize) -> Option { + (widen(raw) < count).then_some(SymIndex(raw)) + } + + pub fn get(self) -> usize { + widen(self.0) + } +} + +fn widen(v: u32) -> usize { + usize::try_from(v).unwrap_or(usize::MAX) +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Sym { - pub name: u32, - pub info: u8, - pub shndx: u16, - pub value: u64, - pub size: u64, + name: u32, + info: u8, + shndx: u16, + value: u64, + size: u64, } impl Sym { @@ -44,6 +67,27 @@ impl Sym { pub const fn is_exported(&self) -> bool { self.is_defined() && matches!(self.bind(), STB_GLOBAL | STB_WEAK) } + + /// This symbol's name in `strings`, `""` when it names nothing readable. + pub fn name_in<'a>(&self, strings: &'a [u8]) -> &'a str { + crate::cstr(strings, u64::from(self.name)) + } + + /// Where a defined, non-TLS symbol lies in the image `extent` describes, + /// or `None`: undefined, thread-local, or a value outside the image. + pub const fn address(&self, extent: Extent) -> Option { + if !self.is_defined() || self.kind() == STT_TLS { + return None; + } + extent.offset(self.value) + } + + /// `S + A` for a symbol read as an offset into a TLS segment of `memsz` + /// bytes — psABI `st_value` for `STT_TLS` — or `None` when the sum leaves + /// the segment. + pub fn tls_offset(&self, addend: i64, memsz: u64) -> Option { + TlsOffset::of(self.value, addend, memsz) + } } /// `.dynsym` (or `.symtab`) paired with the string table its names live in. @@ -87,14 +131,14 @@ impl<'a> SymTab<'a> { if i >= self.count() { return None; } - parse_at(self.syms, i * ENTRY_SIZE) + parse_at(self.syms, i.checked_mul(ENTRY_SIZE)?) } /// The `i`th symbol's name, or `""` for an index past the end or a /// `st_name` past the string table. pub fn name(&self, i: usize) -> &'a str { match self.get(i) { - Some(sym) => read::cstr(self.strs, sym.name as usize), + Some(sym) => sym.name_in(self.strs), None => "", } } @@ -108,23 +152,40 @@ impl<'a> SymTab<'a> { }) } - /// The first defined `STT_TLS` symbol with this name, and its offset within - /// the defining module's TLS segment. - pub fn find_tls(&self, name: &str) -> Option { + /// The first defined `STT_TLS` symbol with this name. + pub fn find_tls(&self, name: &str) -> Option { (0..self.count()).find_map(|i| { let sym = self.get(i)?; - (sym.is_defined() && sym.kind() == STT_TLS && self.name(i) == name).then_some(sym.value) + (sym.is_defined() && sym.kind() == STT_TLS && self.name(i) == name).then_some(sym) }) } + /// Refuse a table any of whose defined symbols names nothing in its + /// module: a value outside the image `extent`, or a `STT_TLS` one outside + /// a TLS segment of `tls_memsz` bytes (or in a module with none). + /// + /// A loader that has asked this once answers every later lookup through + /// [`Sym::address`] and [`Sym::tls_offset`] from the same bytes, so none of + /// those can come back empty for a symbol the table defines. + pub fn bounded(&self, extent: Extent, tls_memsz: Option) -> Result<(), Error> { + for (_, sym) in self.defined() { + if sym.kind() == STT_TLS { + tls_memsz + .and_then(|memsz| sym.tls_offset(0, memsz)) + .ok_or(Error::TlsSymbolOutsideSegment)?; + } else { + sym.address(extent).ok_or(Error::SymbolOutsideImage)?; + } + } + Ok(()) + } + /// The function containing `offset`, and how far into it that is. /// /// **This is what names a backtrace frame, and its caller is a panic /// handler** — so it allocates nothing, takes no lock, indexes nothing - /// unchecked and cannot panic, exactly like every other view here. It was a - /// raw-pointer scan in `kernel/src/symbols.rs` with no test of any kind - /// until 2026-08-16; the cases it has to get right are in - /// `tests/tables.rs`. + /// unchecked and cannot panic, exactly like every other view here. The + /// cases it has to get right are in `tests/tables.rs`. /// /// `offset` is relative to the module's load base, because a symbol's /// `st_value` is. A caller holding an absolute address subtracts the base @@ -168,7 +229,7 @@ impl<'a> SymTab<'a> { } } let (index, sym) = best?; - let within = offset - sym.value; + let within = offset.checked_sub(sym.value)?; if sym.size > 0 && within >= sym.size { return None; } @@ -176,7 +237,7 @@ impl<'a> SymTab<'a> { (!name.is_empty()).then_some((name, within)) } - /// Every index whose symbol is defined and named, in order. + /// Every index whose symbol is defined, in order. pub fn defined(self) -> impl Iterator + 'a { (1..self.count()).filter_map(move |i| { let sym = self.get(i)?; @@ -193,9 +254,9 @@ pub fn parse_at(data: &[u8], off: usize) -> Option { } Some(Sym { name: read::u32_at(data, off)?, - info: *data.get(off + 4)?, - shndx: read::u16_at(data, off + 6)?, - value: read::u64_at(data, off + 8)?, - size: read::u64_at(data, off + 16)?, + info: *data.get(off.checked_add(4)?)?, + shndx: read::u16_at(data, off.checked_add(6)?)?, + value: read::u64_at(data, off.checked_add(8)?)?, + size: read::u64_at(data, off.checked_add(16)?)?, }) } diff --git a/toyos-elf/src/tls.rs b/toyos-elf/src/tls.rs index 7af17e7b35d..3d25128b607 100644 --- a/toyos-elf/src/tls.rs +++ b/toyos-elf/src/tls.rs @@ -126,14 +126,17 @@ impl Static { } /// A static-TLS datum's initial-exec offset from the thread pointer: psABI - /// `S + A - tp`, where `module_addr` is `S` (its module's `base_offset` - /// plus the datum's offset) from `tls_start`. Every `TPOFF` branch passes - /// the addend here, so none can drop `A`. - pub fn tpoff(self, module_addr: u64, addend: i64) -> i64 { - let from_start = module_addr as i64 + addend; + /// `S + A - tp`, for the datum `at` (`S + A`, already inside its module's + /// segment) in the module placed `base_offset` bytes past `tls_start`. + /// + /// `None` for a sum no `i64` holds: every input is a file's number or a + /// sum of them, and the answer is a refusal of the image, never a wrap. + pub fn tpoff(self, base_offset: usize, at: TlsOffset) -> Option { + let from_start = u64::try_from(base_offset).ok()?.checked_add(at.get())?; + let from_start = i64::try_from(from_start).ok()?; match self.variant { - Variant::II => from_start - self.total_memsz as i64, - Variant::I => from_start + self.gap() as i64, + Variant::II => from_start.checked_sub(i64::try_from(self.total_memsz).ok()?), + Variant::I => from_start.checked_add(i64::try_from(self.gap()).ok()?), } } @@ -144,6 +147,27 @@ impl Static { } } +/// `S + A` inside one module's TLS segment, `0..=p_memsz`: made only by +/// [`TlsOffset::of`]. +/// +/// The end is inclusive for the reason [`crate::Extent::offset`]'s is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TlsOffset(u64); + +impl TlsOffset { + /// `value + addend`, when it lies inside a segment of `memsz` bytes. + pub const fn of(value: u64, addend: i64, memsz: u64) -> Option { + match value.checked_add_signed(addend) { + Some(at) if at <= memsz => Some(TlsOffset(at)), + _ => None, + } + } + + pub const fn get(self) -> u64 { + self.0 + } +} + /// 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`] diff --git a/toyos-elf/tests/crafted.rs b/toyos-elf/tests/crafted.rs index f34f95ec422..79e237dbdd6 100644 --- a/toyos-elf/tests/crafted.rs +++ b/toyos-elf/tests/crafted.rs @@ -205,7 +205,7 @@ fn an_entry_point_outside_the_image_is_refused() { assert_eq!(refused(wild), Error::EntryOutsideImage); let last_byte = Elf::honest(0x1000).entry(0xFFF).build(); - assert_eq!(accepted(last_byte).entry, 0xFFF); + assert_eq!(accepted(last_byte).entry().get(), 0xFFF); } #[test] @@ -229,7 +229,7 @@ fn a_header_naming_a_vaddr_outside_the_image_is_refused_by_name() { #[test] fn tls_bss_may_extend_past_the_image() { let bytes = Elf::honest(0x1000).ph(Phdr::tls(0xF00, 0x100, 0x9000, 8)).build(); - assert_eq!(accepted(bytes).tls.unwrap().memsz, 0x9000); + assert_eq!(accepted(bytes).tls().unwrap().memsz(), 0x9000); } #[test] @@ -240,7 +240,7 @@ fn tls_alignment_is_a_power_of_two_within_a_page_or_it_is_refused() { } for align in [0u64, 1, 8, 64, 2 * 1024 * 1024] { let bytes = Elf::honest(0x1000).ph(Phdr::tls(0, 0, 8, align)).build(); - assert_eq!(accepted(bytes).tls.unwrap().align, align, "p_align {align:#x}"); + assert_eq!(accepted(bytes).tls().unwrap().align(), align, "p_align {align:#x}"); } } @@ -250,8 +250,8 @@ fn tls_alignment_is_a_power_of_two_within_a_page_or_it_is_refused() { #[test] fn a_zero_size_tls_segment_is_present_not_absent() { let with = Elf::honest(0x1000).ph(Phdr::tls(0, 0, 0, 8)).build(); - assert!(accepted(with).tls.is_some()); - assert!(accepted(Elf::honest(0x1000).build()).tls.is_none()); + assert!(accepted(with).tls().is_some()); + assert!(accepted(Elf::honest(0x1000).build()).tls().is_none()); } // ── The section header table, which is optional ───────────────────────── @@ -260,13 +260,13 @@ fn a_zero_size_tls_segment_is_present_not_absent() { fn a_section_table_the_loader_cannot_index_is_dropped_not_refused() { for entsize in [0u16, 32, 63, 65, 128] { let layout = accepted(Elf::honest(0x1000).sections(0x100, 4, entsize).build()); - assert!(layout.section_headers.is_none(), "e_shentsize {entsize}"); + assert!(layout.section_headers().is_none(), "e_shentsize {entsize}"); } - assert!(accepted(Elf::honest(0x1000).sections(0, 4, 64).build()).section_headers.is_none()); - assert!(accepted(Elf::honest(0x1000).sections(0x100, 0, 64).build()).section_headers.is_none()); + assert!(accepted(Elf::honest(0x1000).sections(0, 4, 64).build()).section_headers().is_none()); + assert!(accepted(Elf::honest(0x1000).sections(0x100, 0, 64).build()).section_headers().is_none()); let good = accepted(Elf::honest(0x1000).sections(0x100, 4, 64).build()); - assert_eq!(good.section_headers.unwrap().byte_len(), 256); + assert_eq!(good.section_headers().unwrap().byte_len(), 256); } // ── Derived answers about a valid layout ──────────────────────────────── @@ -365,9 +365,9 @@ fn segment_flags_survive_the_parse() { .ph(Phdr::load(0x1000, 0x1000, 0x1000, 0x1000, PF_R | PF_W)) .build(), ); - let text = layout.segments()[0].flags; + let text = layout.segments()[0].flags(); assert!(text.readable() && text.executable() && !text.writable()); - let data = layout.segments()[1].flags; + let data = layout.segments()[1].flags(); assert!(data.readable() && data.writable() && !data.executable()); } diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs new file mode 100644 index 00000000000..65c0689adcd --- /dev/null +++ b/toyos-elf/tests/fuzz.rs @@ -0,0 +1,498 @@ +//! Seeded random images over the ELF *value* path: `r_addend`, `st_value`, +//! `DT_INIT_ARRAY`, the TLS segment's size and alignment, and every `TPOFF` +//! derived from them. +//! +//! Every other test here varies where a table is, never what it says, and four +//! kernel panics lived in what it says. Each image is a structurally valid +//! module — text, data, `PT_DYNAMIC`, `PT_TLS`, a relocation table, a symbol +//! table, an init array — whose numbers are drawn honest most of the time and +//! hostile the rest, and then run through the calls the kernel's loader makes, +//! in the order it makes them. The property: +//! +//! - nothing panics — the test binary runs with overflow checks on, as the +//! kernel does, so an unchecked sum here is the kernel panic it stands for; +//! - an image is refused, or every address derived from it lies inside the +//! image placed at the highest address its span fits at, and every +//! thread-pointer offset inside the thread's TLS block. +//! +//! The generator also counts what it got past the parse, so a regression that +//! refuses everything cannot pass as one that accepts nothing wrong. + +#[allow(dead_code)] +mod common; + +use common::*; +use toyos_elf::dynamic::{Dynamic, InitArray}; +use toyos_elf::rela::{self, FillLattice, 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}; + +/// Images per run: every one of them reaches the relocation parse unless its +/// own headers are what the mutation broke. +const ITERATIONS: u64 = 500_000; + +const SIZE: usize = 0x4000; +const RW: u64 = 0x2000; +const DYNAMIC: u64 = 0x800; +const RELA: u64 = 0x1000; +const SYMTAB: u64 = 0x1400; +const STRTAB: u64 = 0x1800; +const INIT: u64 = 0x2100; +const TDATA: u64 = 0x3000; +const MAX_RELAS: u64 = 32; +const MAX_SYMS: u64 = 16; + +const DT_RELA: i64 = 7; +const DT_RELASZ: i64 = 8; +const DT_SYMTAB: i64 = 6; +const DT_STRTAB: i64 = 5; +const DT_STRSZ: i64 = 10; +const DT_INIT_ARRAY: i64 = 25; +const DT_INIT_ARRAYSZ: i64 = 27; + +/// The kernel's TLS constants: a 64-byte TCB, a 64-entry DTV behind a +/// two-word header, 2 MiB allocations. +const TCB: usize = 64; +const DTV: usize = 16 + 64 * 8; +const GRANULE: usize = 2 * 1024 * 1024; + +/// Where the kernel places an executable: `USER_VM_BASE`. +const USER_VM_BASE: u64 = 0x100_0000_0000; + +/// xorshift64*: deterministic, so a red names its seed and iteration. +struct Rng { + state: u64, + /// Percent of the numbers `value` draws that are hostile, per image. + hostile: u64, +} + +impl Rng { + fn next(&mut self) -> u64 { + self.state ^= self.state >> 12; + self.state ^= self.state << 25; + self.state ^= self.state >> 27; + self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) + } + + fn below(&mut self, n: u64) -> u64 { + self.next() % n.max(1) + } + + fn chance(&mut self, percent: u64) -> bool { + self.below(100) < percent + } + + fn pick(&mut self, from: &[T]) -> T { + from[self.below(from.len() as u64) as usize] + } + + /// `honest` most of the time; otherwise a number chosen to sit on an edge + /// the loader has to get right, or anywhere at all. + fn value(&mut self, honest: u64, edges: &[u64]) -> u64 { + if !self.chance(self.hostile) { + return honest; + } + const WILD: [u64; 14] = [ + 0, + 1, + 8, + 0xfff, + 0x1000, + i64::MAX as u64, + 1 << 63, + u64::MAX, + u64::MAX - 7, + u64::MAX - 0xfff, + USER_VM_BASE, + 1 << 32, + (1 << 32) - 1, + 0xFFFF_8000_0000_0000, + ]; + match self.below(3) { + 0 => self.pick(&WILD), + 1 if !edges.is_empty() => self.pick(edges), + _ => self.next(), + } + } +} + +#[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 { + // RELATIVE, GLOB_DAT, JUMP_SLOT, DTPMOD64, DTPOFF64, TPOFF64, TPOFF32. + const X86: [u32; 7] = [8, 6, 7, 16, 17, 18, 23]; + const A64: [u32; 7] = [1027, 1025, 1026, 1028, 1029, 1030, 1027]; + match machine { + Machine::X86_64 => X86[which], + Machine::Aarch64 => A64[which], + } +} + +fn generate(rng: &mut Rng) -> Case { + // Most images carry one or two hostile numbers, some none, some many: a + // 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, + }; + let bss = rng.pick(&[0u64, 0x800, 0x3000]); + let span = SIZE as u64 + bss; + + let honest_memsz = 0x40 + rng.below(0x400); + let tls_memsz = rng.value(honest_memsz, &[0, 0x40, 0x1F_FFF0, 1 << 40, i64::MAX as u64]); + let tls_align = if rng.chance(95) { rng.pick(&[0u64, 1, 8, 16, 64]) } else { rng.value(3, &[]) }; + let tls_filesz = 0x40.min(tls_memsz); + + let n_relas = rng.below(MAX_RELAS + 1); + let n_syms = 1 + rng.below(MAX_SYMS); + let n_init = rng.below(8); + + let elf_machine = match machine { + Machine::X86_64 => EM_X86_64, + Machine::Aarch64 => EM_AARCH64, + }; + let mut bytes = Elf::new(SIZE) + .machine(elf_machine) + .entry(vmin) + .ph(Phdr::load(0, vmin, RW, RW, PF_R | PF_X)) + .ph(Phdr::load(RW, vmin + RW, SIZE as u64 - RW, SIZE as u64 - RW + bss, PF_R | PF_W)) + .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: DYNAMIC, vaddr: vmin + DYNAMIC, filesz: 0x100, memsz: 0x100, align: 8 }) + .ph(Phdr { + kind: PT_TLS, + flags: PF_R, + offset: TDATA, + vaddr: vmin + TDATA, + filesz: tls_filesz, + memsz: tls_memsz, + align: tls_align, + }) + .build(); + + let edges = |v: u64| [vmin + span, vmin + span + 1, vmin.wrapping_sub(1), v.wrapping_add(1)]; + let tags = [ + (DT_RELA, rng.value(vmin + RELA, &edges(RELA))), + (DT_RELASZ, rng.value(n_relas * 24, &[24 * MAX_RELAS + 1, u64::MAX - 23])), + (DT_SYMTAB, rng.value(vmin + SYMTAB, &edges(SYMTAB))), + (DT_STRTAB, rng.value(vmin + STRTAB, &edges(STRTAB))), + (DT_STRSZ, 0x100), + (DT_INIT_ARRAY, rng.value(vmin + INIT, &edges(vmin + span - 8))), + (DT_INIT_ARRAYSZ, rng.value(n_init * 8, &[7, 9, span, u64::MAX - 7, 1 << 63])), + ]; + put(&mut bytes, DYNAMIC, &dynamic(&tags)); + + // Names "a", "b", ... so a symbol resolves by name to its own index. + for i in 0..MAX_SYMS { + put(&mut bytes, STRTAB + 1 + 2 * i, &[b'a' + i as u8, 0]); + } + for i in 1..n_syms { + let tls = rng.chance(30); + let info = (sym::STB_GLOBAL << 4) | if tls { sym::STT_TLS } else { rng.pick(&[1u8, sym::STT_FUNC]) }; + let shndx = if rng.chance(15) { 0 } else { rng.pick(&[1u16, 2, 0xfff1]) }; + let honest = if tls { rng.below(tls_memsz.min(0x400) + 1) } else { vmin + rng.below(span + 1) }; + let value = rng.value(honest, &[vmin + span, vmin + span + 1, tls_memsz, tls_memsz.wrapping_add(1)]); + put(&mut bytes, SYMTAB + 24 * i, &sym(1 + 2 * (i as u32 - 1), info, shndx, value)); + } + + for i in 0..n_relas { + let which = rng.below(7) as usize; + let r_type = if rng.chance(3) { rng.next() as u32 } else { reloc_number(machine, which) }; + let slot = rng.below((SIZE as u64 - RW) / 8); + let offset = rng.value(vmin + RW + 8 * slot, &[vmin + span - 4, vmin + span, 0xFF9]); + let sym = if rng.chance(40) { 0 } else { rng.below(n_syms + 1) as u32 }; + let sym = if rng.chance(5) { rng.next() as u32 } else { sym }; + let honest = match which { + 0 => vmin + rng.below(span + 1), + 3..=6 if sym == 0 => rng.below(tls_memsz.min(0x400) + 1), + _ => rng.below(17).wrapping_sub(8), + }; + let addend = rng.value(honest, &[vmin + span, vmin + span + 1, tls_memsz, tls_memsz.wrapping_add(1)]) as i64; + put(&mut bytes, RELA + 24 * i, &rela(offset, sym, r_type, addend)); + } + + for i in 0..n_init { + put(&mut bytes, INIT + 8 * i, &(vmin + rng.below(RW)).to_le_bytes()); + } + + // And some noise no structure chose, anywhere past the file header. + if rng.chance(10) { + for _ in 0..1 + rng.below(4) { + let at = 64 + rng.below(SIZE as u64 - 64) as usize; + bytes[at] = rng.next() as u8; + } + } + + Case { bytes, machine, mode } +} + +fn put(bytes: &mut [u8], at: u64, data: &[u8]) { + bytes[at as usize..at as usize + data.len()].copy_from_slice(data); +} + +/// How far each image got, over the whole run. +#[derive(Default, Debug)] +struct Reached { + images: u64, + accepted: u64, + relative: u64, + bind: u64, + tpoff: u64, + dtpoff: u64, + init_arrays: u64, + symbols: u64, +} + +impl Reached { + /// Fold in one accepted image's counts. + fn add(&mut self, image: &Reached) { + self.accepted += 1; + self.relative += image.relative; + self.bind += image.bind; + self.tpoff += image.tpoff; + self.dtpoff += image.dtpoff; + self.init_arrays += image.init_arrays; + self.symbols += image.symbols; + } +} + +/// The bytes the image holds at `range`, as a loader holding the image reads +/// them. This image's file offsets are its image offsets. +fn at<'a>(bytes: &'a [u8], range: ImageRange) -> &'a [u8] { + let start = range.start().get() as usize; + bytes.get(start..start + range.len() as usize).unwrap_or(&[]) +} + +/// Where the image goes: where the kernel puts an executable, or the highest +/// address its whole span fits below — a sum that fits one may not fit the +/// other. +#[derive(Clone, Copy, Debug)] +enum Placement { + UserVmBase, + Highest, +} + +/// Everything the kernel's loader decides about one image, in its order, +/// through the calls it makes. `Err` is a refusal; every address derived from +/// an accepted image is checked against the image as `placement` places it. +fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), ()> { + let layout = Layout::parse(&case.bytes, case.machine).map_err(|_| ())?; + let extent = layout.extent(); + let span = layout.span(); + let image_start = match placement { + Placement::UserVmBase => USER_VM_BASE, + Placement::Highest => u64::MAX - span, + }; + // A span no placement holds is refused before anything is derived, as + // `toyos_userbound::rebase_base` refuses it. + image_start.checked_add(span).ok_or(())?; + // Where an address `image_start + offset` has to stay: the placement is + // only legal because the whole span fits above it. + let place = |offset: u64| -> u64 { + assert!(offset <= span, "offset {offset:#x} past the span {span:#x}"); + image_start.checked_add(offset).expect("an offset inside the span leaves the placement") + }; + + let dyn_bytes = at(&case.bytes, layout.dynamic().ok_or(())?.image); + let dynamic = Dynamic::parse(dyn_bytes); + + if let Some(init) = InitArray::parse(dynamic.init_array, extent).map_err(|_| ())? { + place(init.range().end().get()); + place(init.range().start().get()); + assert_eq!(init.count() * 8, init.range().len()); + reached.init_arrays += 1; + } + + let table = |vaddr: Option, len: u64| -> Result { + vaddr.and_then(|v| extent.range(v, len)).ok_or(()) + }; + let sym_range = table(dynamic.symtab, MAX_SYMS * sym::ENTRY_SIZE as u64)?; + let str_range = table(dynamic.strtab, 0x100)?; + let symbols = SymTab::new(at(&case.bytes, sym_range), at(&case.bytes, str_range)); + let tls_memsz = layout.tls_memsz(); + symbols.bounded(extent, tls_memsz).map_err(|_| ())?; + for (_, s) in symbols.defined() { + if let Some(off) = s.address(extent) { + place(off.get()); + reached.symbols += 1; + } else { + let off = s.tls_offset(0, tls_memsz.unwrap()).expect("a TLS symbol `bounded` accepted"); + assert!(off.get() <= tls_memsz.unwrap()); + } + } + + let rela_range = match dynamic.rela { + Some(t) => extent.range(t.vaddr, t.size).ok_or(())?, + 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, + sym_count: symbols.count(), + fill: (case.mode == Mode::Exe).then_some(FillLattice { base: extent.min(), granule: 4096 }), + tls_memsz, + }; + let mut relocs = Vec::new(); + for raw in RelaTable::new(rela_bytes, case.machine).iter() { + if let Some(r) = rela::parse(raw, &rules).map_err(|_| ())? { + relocs.push(r); + } + } + + // The thread's static block: this module alone, placed as the kernel's + // `build_tls_layout` places an executable's. + let variant = Variant::of(case.machine); + let (block, base_offset, memsz) = match layout.tls().filter(|t| t.memsz() > 0) { + Some(t) => { + let memsz = usize::try_from(t.memsz()).map_err(|_| ())?; + let align = usize::try_from(t.align()).map_err(|_| ())?; + let placed = match variant { + 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()) + } + None => (Static::empty(variant), 0, 0), + }; + let plan = block.plan(TCB, DTV, GRANULE).ok_or(())?; + let thread_offset = |at: TlsOffset, width_i32: bool| -> Result { + let tpoff = block.tpoff(base_offset, at).ok_or(())?; + if width_i32 { + i32::try_from(tpoff).map_err(|_| ())?; + } + // `tp + tpoff` is the datum, and it lies in the block's TLS data. + let datum = i128::from(plan.tp_offset as u64) + i128::from(tpoff); + let data = i128::from(plan.tls_start as u64); + assert!( + (data..=data + i128::from(block.total_memsz() as u64)).contains(&datum), + "TPOFF {tpoff:#x} names {datum:#x}, outside the TLS data at {data:#x}+{:#x}", + block.total_memsz(), + ); + Ok(tpoff) + }; + let resolve = |r: TlsRef| -> Result, ()> { + match r { + TlsRef::Own(off) => Ok(Some(off)), + // Defined here: bounded against this module's segment. Undefined: + // another module's, which the kernel resolves by name and this + // image has no other module to find it in. + TlsRef::Symbol(s) => match symbols.get(s.sym().get()).filter(|d| d.is_defined()) { + Some(d) => s.offset_in(&d, memsz).map(Some).ok_or(()), + None => Ok(None), + }, + } + }; + + for r in relocs { + let width = match r.op() { + Op::Tpoff32(_) => 4, + _ => 8, + }; + assert!(r.offset() >= window.0 && r.offset() + width <= window.1, "{r:?} outside {window:x?}"); + match r.op() { + Op::Relative(off) => { + place(off.get()); + reached.relative += 1; + } + Op::Bind(idx) => { + if let Some(s) = symbols.get(idx.get()) { + if let Some(off) = s.address(extent) { + place(off.get()); + } + } + reached.bind += 1; + } + Op::Tpoff64(t) | Op::Tpoff32(t) => { + if let Some(off) = resolve(t)? { + thread_offset(off, matches!(r.op(), Op::Tpoff32(_)))?; + reached.tpoff += 1; + } + } + Op::DtpOff64(t) => { + if let Some(off) = resolve(t)? { + assert!(off.get() <= memsz, "DTPOFF {:#x} past PT_TLS {memsz:#x}", off.get()); + reached.dtpoff += 1; + } + } + Op::DtpMod64(_) => {} + } + } + Ok(()) +} + +#[test] +fn every_derived_address_lies_inside_the_image_or_the_image_is_refused() { + let mut reached = Reached::default(); + for seed in [0x9E37_79B9_7F4A_7C15u64, 0xD1B5_4A32_D192_ED03] { + let mut rng = Rng { state: seed, hostile: 0 }; + for i in 0..ITERATIONS / 2 { + let case = generate(&mut rng); + for placement in [Placement::UserVmBase, Placement::Highest] { + reached.images += 1; + let mut this = Reached::default(); + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + load(&case, placement, &mut this) + })); + match outcome { + Ok(Ok(())) => reached.add(&this), + Ok(Err(())) => {} + Err(_) => panic!( + "seed {seed:#x} iteration {i} ({:?}, {:?}, placed {placement:?}) \ + panicked; its image is {} bytes", + case.machine, + case.mode, + case.bytes.len(), + ), + } + } + } + } + println!("{reached:?}"); + + // The run reached every question, not just the headers. + assert!(reached.accepted * 10 >= reached.images, "{reached:?}"); + for (what, n) in [ + ("RELATIVE", reached.relative), + ("GLOB_DAT/JUMP_SLOT", reached.bind), + ("TPOFF", reached.tpoff), + ("DTPOFF", reached.dtpoff), + ("DT_INIT_ARRAY", reached.init_arrays), + ("defined symbols", reached.symbols), + ] { + assert!(n >= 1000, "only {n} accepted {what}: {reached:?}"); + } +} diff --git a/toyos-elf/tests/real.rs b/toyos-elf/tests/real.rs index 4e2530797b1..d1663229bb9 100644 --- a/toyos-elf/tests/real.rs +++ b/toyos-elf/tests/real.rs @@ -18,24 +18,26 @@ const HEADERS: &[u8] = include_bytes!("fixtures/toyos-ld-headers.bin"); fn a_toyos_ld_binary_parses_to_what_readelf_says() { let layout = Layout::parse(HEADERS, Machine::X86_64).expect("toyos-ld's own output"); - assert_eq!(layout.entry, 0x3261c); - assert_eq!(layout.vaddr_min, 0); - assert_eq!(layout.vaddr_max, 0x167000); + assert_eq!(layout.entry().get(), 0x3261c); + assert_eq!(layout.extent().min(), 0); + assert_eq!(layout.extent().max(), 0x167000); // text (R-X), data (RW-), rodata carrying .rela.dyn and .dynamic (R--). let segs = layout.segments(); assert_eq!(segs.len(), 3); - assert!(segs[0].flags.executable() && !segs[0].writable()); - assert!(segs[1].writable() && !segs[1].flags.executable()); - assert!(!segs[2].writable() && !segs[2].flags.executable()); + assert!(segs[0].flags().executable() && !segs[0].writable()); + assert!(segs[1].writable() && !segs[1].flags().executable()); + assert!(!segs[2].writable() && !segs[2].flags().executable()); assert_eq!(layout.writable_window(), Some((0x145000, 0x155000))); - assert_eq!(layout.dynamic, Some((0x166490, 0x166490, 0x60))); - assert_eq!(layout.eh_frame_hdr, Some((0x13e118, 0x688c))); - assert_eq!(layout.tls.unwrap().memsz, 0x90); - assert_eq!(layout.tls.unwrap().align, 0x40); + let dynamic = layout.dynamic().map(|d| (d.file_offset, d.image.start().get(), d.image.len())); + assert_eq!(dynamic, Some((0x166490, 0x166490, 0x60))); + let eh = layout.eh_frame_hdr().map(|r| (r.start().get(), r.len())); + assert_eq!(eh, Some((0x13e118, 0x688c))); + assert_eq!(layout.tls().unwrap().memsz(), 0x90); + assert_eq!(layout.tls().unwrap().align(), 0x40); - let sections = layout.section_headers.expect("a section header table"); + let sections = layout.section_headers().expect("a section header table"); assert_eq!((sections.count, sections.entry_size), (9, 64)); // Every `DT_*` vaddr in this file resolves, and no two segments contend for diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index c509e2fa392..48aa0be683b 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -15,10 +15,29 @@ mod common; use common::*; use toyos_elf::dynamic::{self, Dynamic}; use toyos_elf::gnu_hash::{self, GnuHash}; -use toyos_elf::rela::{self, RelaCounts, RelaTable, RelocError, RelocKind}; +use toyos_elf::rela::{self, FillLattice, 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; +use toyos_elf::{Extent, 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. +fn validate( + mut entries: impl Iterator, + window: (u64, u64), + sym_count: usize, + fill: Option, +) -> Result<(), RelocError> { + let rules = Rules { + extent: Extent::new(0, u64::MAX).unwrap(), + window, + sym_count, + fill, + tls_memsz: Some(u64::MAX), + }; + entries.try_for_each(|r| rela::parse(r, &rules).map(|_| ())) +} /// `st_info` for a global `STT_FUNC`, and for the data object it is told apart /// from. @@ -93,12 +112,6 @@ fn relocation_types_map_to_the_width_the_writers_use() { assert_eq!(RelocKind::from_raw(Machine::X86_64, 23).write_width(), Some(4)); assert_eq!(RelocKind::from_raw(Machine::X86_64, 0).write_width(), None); assert_eq!(RelocKind::from_raw(Machine::X86_64, 42).write_width(), None); - // RELATIVE is the one written type that resolves no symbol, so it is the - // one whose `r_sym` needs no bound. - assert!(!RelocKind::Relative.needs_symbol()); - for raw in [6u32, 7, 16, 17, 18, 23] { - assert!(RelocKind::from_raw(Machine::X86_64, raw).needs_symbol(), "type {raw}"); - } } #[test] @@ -107,25 +120,25 @@ fn validation_refuses_a_write_outside_the_window_by_name() { let overflowing = [rela(u64::MAX - 3, 0, 8, 0)].concat(); assert_eq!( - rela::validate(RelaTable::new(&overflowing, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&overflowing, Machine::X86_64).iter(), window, 4, None), Err(RelocError::OffsetOverflows), ); let below = [rela(0xFF8, 0, 8, 0)].concat(); assert_eq!( - rela::validate(RelaTable::new(&below, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&below, Machine::X86_64).iter(), window, 4, None), Err(RelocError::OutsideWindow), ); // One byte of an eight-byte write past the end. let straddling = [rela(0x1FF9, 0, 8, 0)].concat(); assert_eq!( - rela::validate(RelaTable::new(&straddling, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&straddling, Machine::X86_64).iter(), window, 4, None), Err(RelocError::OutsideWindow), ); let fits = [rela(0x1FF8, 0, 8, 0)].concat(); - assert_eq!(rela::validate(RelaTable::new(&fits, Machine::X86_64).iter(), window, 4, None), Ok(())); + assert_eq!(validate(RelaTable::new(&fits, Machine::X86_64).iter(), window, 4, None), Ok(())); } /// A table the loader reads while it writes must not lie inside the range it @@ -142,28 +155,28 @@ fn a_read_table_inside_the_write_window_is_refused_by_name() { let rounded = (0u64, 0x200000u64); let shell = rela::ReadTables { rela: (0x166490, 0x1664f0), ..Default::default() }; - assert_eq!(rela::tables_outside_window(&shell, exact), Ok(())); + assert_eq!(rela::tables_outside_window(&shell, exact, 4096), Ok(())); assert_eq!( - rela::tables_outside_window(&shell, rounded), - Err("ELF: .rela.dyn lies inside the module's writable window"), + rela::tables_outside_window(&shell, rounded, 4096), + Err("ELF: .rela.dyn lies on a page of the module's writable window"), "the rounded-down window is what covered a conforming image's own tables" ); for (tables, refusal) in [ ( rela::ReadTables { dynsym: (0x146000, 0x146030), ..Default::default() }, - "ELF: .dynsym lies inside the module's writable window", + "ELF: .dynsym lies on a page of the module's writable window", ), ( rela::ReadTables { dynstr: (0x144ff0, 0x145010), ..Default::default() }, - "ELF: .dynstr lies inside the module's writable window", + "ELF: .dynstr lies on a page of the module's writable window", ), ( rela::ReadTables { jmprel: (0x154ff8, 0x155018), ..Default::default() }, - "ELF: .rela.plt lies inside the module's writable window", + "ELF: .rela.plt lies on a page of the module's writable window", ), ] { - assert_eq!(rela::tables_outside_window(&tables, exact), Err(refusal)); + assert_eq!(rela::tables_outside_window(&tables, exact, 4096), Err(refusal)); } // Touching at an edge is not overlapping; an absent table is an empty range @@ -173,9 +186,31 @@ fn a_read_table_inside_the_write_window_is_refused_by_name() { dynstr: (0x155000, 0x156000), ..Default::default() }; - assert_eq!(rela::tables_outside_window(&abutting, exact), Ok(())); - assert_eq!(rela::tables_outside_window(&rela::ReadTables::default(), exact), Ok(())); - assert_eq!(rela::tables_outside_window(&shell, (0x145000, 0x145000)), Ok(())); + assert_eq!(rela::tables_outside_window(&abutting, exact, 4096), Ok(())); + assert_eq!(rela::tables_outside_window(&rela::ReadTables::default(), exact, 4096), Ok(())); + assert_eq!(rela::tables_outside_window(&shell, (0x145000, 0x145000), 4096), Ok(())); + // An empty window is not rounded out into a page that holds a table. + assert_eq!(rela::tables_outside_window(&shell, (0x166400, 0x166400), 4096), Ok(())); +} + +/// The pages a mapping protects are whole: a table that shares the writable +/// window's last page, though no byte of it is in the window, sits in memory +/// the process can write — and the loader parses its tables again after the +/// image is mapped. The exact bound accepted this image. +#[test] +fn a_read_table_on_a_page_of_the_write_window_is_refused() { + let window = (0x145000u64, 0x154ff0u64); + let tail = rela::ReadTables { rela: (0x154ff8, 0x155000), ..Default::default() }; + assert_eq!( + rela::tables_outside_window(&tail, window, 4096), + Err("ELF: .rela.dyn lies on a page of the module's writable window"), + ); + assert_eq!(rela::tables_outside_window(&tail, window, 1), Ok(()), "the exact bound"); + let head = rela::ReadTables { dynsym: (0x145000, 0x145008), ..Default::default() }; + assert_eq!( + rela::tables_outside_window(&head, (0x145008, 0x146000), 4096), + Err("ELF: .dynsym lies on a page of the module's writable window"), + ); } /// psABI oracle: every entry is applied or the object rejected. An 8-byte write @@ -183,17 +218,17 @@ fn a_read_table_inside_the_write_window_is_refused_by_name() { #[test] fn a_relocation_crossing_a_fill_page_is_refused_only_for_a_chunked_writer() { let window = (0u64, 0x1_0000u64); - let lattice = rela::FillLattice { base: 0, granule: 4096 }; + let lattice = FillLattice { base: 0, granule: 4096 }; for off in 0xFF9u64..=0xFFF { let straddles = [rela(off, 0, 8, 0)].concat(); assert_eq!( - rela::validate(RelaTable::new(&straddles, Machine::X86_64).iter(), window, 4, Some(lattice)), + 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!( - rela::validate(RelaTable::new(&straddles, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&straddles, Machine::X86_64).iter(), window, 4, None), Ok(()), "offset {off:#x} refused for a contiguous writer", ); @@ -201,17 +236,17 @@ fn a_relocation_crossing_a_fill_page_is_refused_only_for_a_chunked_writer() { // A write ending at the boundary fits; a 4-byte TPOFF32 fits in the last 4. assert_eq!( - rela::validate(RelaTable::new(&[rela(0xFF8, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), + validate(RelaTable::new(&[rela(0xFF8, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), Ok(()), ); assert_eq!( - rela::validate(RelaTable::new(&[rela(0xFFC, 0, 23, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), + validate(RelaTable::new(&[rela(0xFFC, 0, 23, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(lattice)), Ok(()), ); - let shifted = rela::FillLattice { base: 3, granule: 4096 }; + let shifted = FillLattice { base: 3, granule: 4096 }; assert_eq!( - rela::validate(RelaTable::new(&[rela(0x1000, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(shifted)), + validate(RelaTable::new(&[rela(0x1000, 0, 8, 0)].concat(), Machine::X86_64).iter(), window, 4, Some(shifted)), Err(RelocError::StraddlesFillPage), ); } @@ -225,14 +260,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!( - rela::validate(RelaTable::new(&bytes, Machine::X86_64).iter(), window, 4, None), + validate(RelaTable::new(&bytes, Machine::X86_64).iter(), window, 4, None), 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!(rela::validate(RelaTable::new(&ignored, Machine::X86_64).iter(), window, 0, None), Ok(())); + assert_eq!(validate(RelaTable::new(&ignored, Machine::X86_64).iter(), window, 0, None), Ok(())); } /// A TLS descriptor is filled by a resolver this loader does not have, so an @@ -244,11 +279,11 @@ 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!( - rela::validate(RelaTable::new(&desc, Machine::Aarch64).iter(), window, 4, None), + validate(RelaTable::new(&desc, Machine::Aarch64).iter(), window, 4, None), Err(RelocError::TlsDescriptor), ); assert!(RelocError::TlsDescriptor.as_str().contains("R_AARCH64_TLSDESC")); - assert_eq!(rela::validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4, None), Ok(())); + assert_eq!(validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4, None), Ok(())); let counts = RelaCounts::of(RelaTable::new(&desc, Machine::Aarch64).iter()); assert_eq!(counts.count_of(RelocKind::TlsDesc), 1); } @@ -279,15 +314,20 @@ fn an_executable_s_reservation_is_had_only_through_its_refusals() { fn a_symbol_index_past_the_table_is_refused_except_for_relative() { let window = (0u64, 0x100u64); - let bind = [rela(0x10, 4, 6, 0)].concat(); - assert_eq!( - rela::validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 4, None), - Err(RelocError::SymbolPastTable), - ); - assert_eq!(rela::validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 5, None), Ok(())); + // Every type that resolves a symbol, TLS ones included once `r_sym` is not + // the null entry. + 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), + Err(RelocError::SymbolPastTable), + "type {raw}", + ); + assert_eq!(validate(RelaTable::new(&bind, Machine::X86_64).iter(), window, 5, None), Ok(())); + } let relative = [rela(0x10, u32::MAX, 8, 0)].concat(); - assert_eq!(rela::validate(RelaTable::new(&relative, Machine::X86_64).iter(), window, 0, None), Ok(())); + assert_eq!(validate(RelaTable::new(&relative, Machine::X86_64).iter(), window, 0, None), Ok(())); } #[test] @@ -364,7 +404,8 @@ fn lookups_skip_undefined_symbols_and_the_null_entry() { ] .concat(); let table = SymTab::new(&syms, b"\0tls_var\0"); - assert_eq!(table.find_tls("tls_var"), Some(0x40)); + let tls_var = table.find_tls("tls_var").and_then(|s| s.tls_offset(0, 0x100)); + assert_eq!(tls_var.map(|o| o.get()), Some(0x40)); assert_eq!(table.find("tls_var").map(|(i, _)| i), Some(2)); assert_eq!(table.defined().count(), 1); } @@ -691,5 +732,14 @@ fn each_machine_reads_its_own_relocation_numbers() { let bytes = rela(0x10, 0, 1027, 4); let entry = RelaTable::new(&bytes, Machine::Aarch64).get(0).unwrap(); - assert_eq!((entry.kind, entry.addend), (RelocKind::Relative, 4)); + assert_eq!(entry.kind(), RelocKind::Relative); + let rules = Rules { + extent: Extent::new(0, 0x100).unwrap(), + window: (0, 0x100), + sym_count: 0, + fill: None, + tls_memsz: None, + }; + let parsed = rela::parse(entry, &rules).unwrap().unwrap(); + assert_eq!((parsed.offset(), parsed.op()), (0x10, Op::Relative(rules.extent.offset(4).unwrap()))); } diff --git a/toyos-elf/tests/tls.rs b/toyos-elf/tests/tls.rs index bf1d764310e..0cede47ad85 100644 --- a/toyos-elf/tests/tls.rs +++ b/toyos-elf/tests/tls.rs @@ -5,12 +5,17 @@ //! kernel-bug assert reached from a crafted `PT_TLS`. The property is proved //! here instead, which is what lets the assert go. -use toyos_elf::tls::{self, Static, Variant}; +use toyos_elf::tls::{self, Static, TlsOffset, Variant}; const TCB: usize = 64; const DTV: usize = 16 + 64 * 8; const GRANULE: usize = 2 * 1024 * 1024; +/// A datum `off` bytes into a segment no offset here leaves. +fn datum(off: u64) -> TlsOffset { + TlsOffset::of(off, 0, u64::MAX).unwrap() +} + #[test] fn the_dtv_is_never_overwritten_by_tls_data() { let sizes = [ @@ -105,16 +110,41 @@ fn tpoff_carries_the_addend() { let two = Static::new(Variant::II, total, 64, 64).unwrap(); let one = Static::new(Variant::I, total, 64, 64).unwrap(); for &module_addr in &[0u64, 8, 0x40, 0x1F0] { - assert_eq!(two.tpoff(module_addr, 0), module_addr as i64 - total as i64); - assert_eq!(one.tpoff(module_addr, 0), module_addr as i64 + 64); + 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)); for &addend in &[0i64, 8, -8, 0x100, -0x100] { + // `S + A` below the segment's start names nothing in it. + let Some(sum) = TlsOffset::of(module_addr, addend, u64::MAX) else { + assert!(addend < 0 && addend.unsigned_abs() > module_addr); + continue; + }; for s in [one, two] { - assert_eq!(s.tpoff(module_addr, addend) - s.tpoff(module_addr, 0), addend, "{s:?}"); + assert_eq!(s.tpoff(0, sum).unwrap() - s.tpoff(0, datum(module_addr)).unwrap(), addend, "{s:?}"); } } } } +/// The audit's two crafted `TPOFF` values: an addend of `i64::MIN` against a +/// 16-byte segment, and `i64::MAX` past a datum 16 bytes in. Each was a kernel +/// 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(); + assert_eq!(TlsOffset::of(0, i64::MIN, 16), None); + assert_eq!(TlsOffset::of(16, i64::MAX, 16), None); + // A segment as large as a file can declare: the offset exists, the + // thread-pointer offset does not. + let huge = TlsOffset::of(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(); + assert_eq!(wide.tpoff(0, datum(0)), None); + // The inclusive end is a datum: one past the segment's last byte. + assert_eq!(TlsOffset::of(8, 8, 16).map(TlsOffset::get), Some(16)); + assert_eq!(TlsOffset::of(8, 9, 16), None); +} + /// 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 @@ -132,7 +162,7 @@ fn variant_i_puts_the_first_module_where_its_linker_put_it() { 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, 0), gap as i64, "{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}"); @@ -149,8 +179,8 @@ fn variant_i_puts_the_first_module_where_its_linker_put_it() { #[test] fn variant_i_agrees_with_what_lld_linked() { let s = Static::new(Variant::I, 0xb0, 64, 64).unwrap(); - assert_eq!(s.tpoff(0, 0), 0x40); - assert_eq!(s.tpoff(0x40, 0), 0x80); + assert_eq!(s.tpoff(0, datum(0)), Some(0x40)); + assert_eq!(s.tpoff(0x40, datum(0)), Some(0x80)); } /// The machine names the variant: x86-64's psABI is variant II, AArch64's @@ -179,6 +209,6 @@ fn the_executables_extent_is_its_size_rounded_to_its_alignment() { 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 as u64, 0), -0xc0); + assert_eq!(s.tpoff(exe_base, datum(0)), Some(-0xc0)); assert_eq!(exe_base % 0x40, 0); } From 11a850dd92ce8dc8a4f96ada11b63a87a290b1b7 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 16:23:25 +0200 Subject: [PATCH 02/10] tests: crafted ELF values, refused with the kernel intact abuse_elf_loader gains the value cases its own header said it lacked: a RELATIVE addend, a TPOFF addend, a defined symbol's st_value and a DT_INIT_ARRAY set to an address no image holds, spawned and dlopened from a writable mount. Each was a kernel overflow panic; each is now an InvalidArgument return, and the heap walk and real spawn at the end of main assert the machine lived. f13's defining TLS segment grows to hold its own addend, which the new bound would otherwise refuse. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- .../src/bin/abuse_elf_loader.rs | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) 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 88f16bd3331..90ddb0d5275 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -539,6 +539,10 @@ fn main() { // `.dynsym`, inside the borrow, inside a `ReadExec` page. dlopen_refused("vaddr_min_shift.so", &so_with_non_zero_vaddr_min()); + // 18. The values a file writes, not the places: each of these was an + // overflow panic in the kernel, reached by writing the file to /tmp. + values_are_bounded_by_the_image(); + // 14. A cross-module initial-exec TLS reference resolves to `S + A - tp`. f13_cross_module_addend_is_kept(); @@ -668,6 +672,120 @@ fn so_with_non_zero_vaddr_min() -> Vec { .build() } +/// 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. +fn values_are_bounded_by_the_image() { + // RELATIVE `B + A` with `A = i64::MAX`: `USER_VM_BASE + A` overflowed. + spawn_refused( + "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( + "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. + { + 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()); + spawn_refused("export_past_image", &exe.build()); + } + + dlopen_refused("so_relative_addend_past_image.so", &so_with(&[], &[(0x1400, 0, R_X86_64_RELATIVE, i64::MAX)], None)); + dlopen_refused("so_tpoff_addend_past_tls.so", &so_with(&[], &[(0x1400, 0, R_X86_64_TPOFF64, i64::MIN)], Some(16))); + dlopen_refused( + "so_init_array_past_image.so", + &so_with(&[(DT_INIT_ARRAY, u64::MAX), (DT_INIT_ARRAYSZ, 8)], &[], None), + ); + // Refused at load now; before, `dlsym` added the value to the module's base. + let far = so_with(&[], &[], None); + let far = patch_sym(far, 0x218, 1, (STB_GLOBAL << 4) | STT_FUNC, 1, FAR_VALUE); + let path = write_file("so_export_past_image.so", &far); + if let Ok(lib) = unsafe { libloading::Library::new(&path) } { + let found = unsafe { lib.get::<*const u8>(FAR.as_bytes()) }.is_ok(); + panic!("so_export_past_image.so: dlopen loaded it (dlsym found {FAR}: {found})"); + } +} + +const FAR: &str = "far"; +const FAR_VALUE: u64 = 0xFFFF_FFFF_FFFF_F000; +const DT_INIT_ARRAY: i64 = 25; +const DT_INIT_ARRAYSZ: i64 = 27; +const STT_FUNC: u8 = 2; + + +/// An executable spanning `[0, 0x4000)` writable, `PT_DYNAMIC` at 0x1000, its +/// relocations at 0x1200, `.dynsym` at 0x1400 and `.dynstr` after it at 0x1800 — and a +/// 16-byte-aligned `PT_TLS` of `tls` bytes when asked. +fn exe_with(tags: &[(i64, u64)], relas: &[(u64, u64, i64)], tls: Option) -> Elf { + let mut all = vec![(DT_SYMTAB, 0x1400), (DT_STRTAB, 0x1800), (DT_STRSZ, 0x100)]; + if !relas.is_empty() { + all.extend([(DT_RELA, 0x1200), (DT_RELASZ, 24 * relas.len() as u64)]); + } + all.extend_from_slice(tags); + let mut elf = Elf::new(0x4000) + .ph(Phdr::load(0, 0, 0x4000, 0x4000, PF_R | PF_W)) + .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) + .entry(0) + .dynamic(0x1000, &all); + if let Some(memsz) = tls { + elf = elf.ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x3000, vaddr: 0x3000, filesz: 0, memsz, align: 16 }); + } + for (i, &(offset, r_type, addend)) in relas.iter().enumerate() { + elf = elf.rela(0x1200 + 24 * i, offset, r_type, addend); + } + elf +} + +/// A library laid out as [`so_with_reloc_below_writable`] is — text and every +/// table in `[0, 0x1000)`, writable data at `[0x1000, 0x5000)` — with these +/// extra dynamic tags, relocations `(r_offset, r_sym, type, addend)`, and a +/// `PT_TLS` of `tls` bytes when asked. +fn so_with(tags: &[(i64, u64)], relas: &[(u64, u32, u64, i64)], tls: Option) -> Vec { + let mut all = vec![(DT_SYMTAB, 0x200), (DT_STRTAB, 0x300), (DT_STRSZ, 0x100)]; + if !relas.is_empty() { + all.extend([(DT_RELA, 0x800), (DT_RELASZ, 24 * relas.len() as u64)]); + } + all.extend_from_slice(tags); + let mut elf = Elf::new(0x5000) + .ph(Phdr::load(0, 0, 0x1000, 0x1000, PF_R | PF_X)) + .ph(Phdr::load(0x1000, 0x1000, 0x4000, 0x4000, PF_R | PF_W)) + .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x600, vaddr: 0x600, filesz: 0x200, memsz: 0x200, align: 8 }) + .sections(0x900, 1, 64) + .dynamic(0x600, &all) + .poke(0x301, FAR.as_bytes()) + .shdr(0x900, SHT_DYNSYM, 0x200, 48, 24); + if let Some(memsz) = tls { + elf = elf.ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz, align: 16 }); + } + for (i, &(offset, sym, r_type, addend)) in relas.iter().enumerate() { + elf = elf.rela(0x800 + 24 * i, offset, ((sym as u64) << 32) | r_type, addend); + } + elf.build() +} + +/// One `Elf64_Sym` written into built bytes. +fn patch_sym(mut bytes: Vec, off: usize, st_name: u32, st_info: u8, st_shndx: u16, st_value: u64) -> Vec { + bytes[off..off + 4].copy_from_slice(&st_name.to_le_bytes()); + bytes[off + 4] = st_info; + bytes[off + 6..off + 8].copy_from_slice(&st_shndx.to_le_bytes()); + bytes[off + 8..off + 16].copy_from_slice(&st_value.to_le_bytes()); + bytes +} + thread_local! { // A non-empty static TLS block, so `dlopen` runs the TPOFF pass at all. static F13_KEEP_TLS: std::cell::Cell = const { std::cell::Cell::new(0) }; @@ -707,7 +825,7 @@ fn f13_cross_module_addend_is_kept() { fn tls_defs_so() -> Vec { Elf::new(0x2000) .ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_X)) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz: 0x20, align: 8 }) + .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz: 0x200, align: 8 }) .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) .sections(0x1C00, 1, 64) .dynamic(0x1000, &[(DT_SYMTAB, 0x1200), (DT_STRTAB, 0x1400), (DT_STRSZ, 0x40)]) From 74ed44233c8bc2468d8174cabffb840c4827fe83 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 17:56:14 +0200 Subject: [PATCH 03/10] review #544: the negative control, the race and refusal tests, unforgeable types Answers the round-1 review of the ELF-loader value-checking work. BLOCKERs - B1 negative control. `abuse_elf_loader`'s seven `values_are_bounded_by_the_image` cases were run one at a time against a kernel built at the merge base 16d2e645, each isolated in a throwaway worktree, and each panics the base kernel with the named overflow; all seven are refused at head. Logs in the PR body. - B2. `tls_dtv_race` builds the C1 race: one worker at a time on a reused stack, so the kernel's TLS block is reused at one virtual address; a sibling probes that address with `random` (BadAddress while unmapped) and, once mapped, hammers 0 into DTV slot 0. Green at head; with the review's mutation (`fix` after `map_range` and `drop(space)`) the kernel panics in `loader/tls.rs` `rebase`'s `p - phys`. A store is never in flight against a freeing block: an acknowledged pause stands the sibling down before every retire. - B3. The apply-time TLS refusals are reached: a `dlopen`ed and an executable fixture, each with its own defined `STT_TLS` symbol and a `TPOFF64` whose `S + A` leaves its `PT_TLS`. Both are refused, and the harness asserts the reason (`TLS relocation names an offset outside its PT_TLS`) is named beside the file, not merely that the load was refused. - B4. `Extent::new`, `TlsOffset::of` and `DynamicSegment`'s fields are `pub(crate)`; `DynamicSegment` grows `file_offset()`/`image()` readers. No `ImageOffset`/`TlsOffset`/`Extent` can be forged outside `toyos-elf`. Tests build extents through `Layout::parse` of crafted images and TLS offsets through `Sym::tls_offset`. Simplifications - One generic `tls_entries` in `elf::reloc` replaces the three cached-or-scanned copies; `bind`/`dtpmod` are one-line wrappers over it. - `exe_tpoff` folds into the shared `resolve_tls_ref`, which resolves a library's in-image symbols and the executable's file-backed symbols alike. - `map_block`, `TlsSymRef::offset_in` and `RawLoad`'s second pass are gone; `TlsSymRef::addend` and a first pass over the phdrs replace them. - The executable's relocations parse against a real `sym_count`, not `usize::MAX`, so `SymIndex` bounds them. - `Unpublished::publish` keeps `alloc_and_map`'s 2 MiB-alignment assert and says why it cannot reuse it (the `fix` runs between the reserve and the map). REMOVEs applied; the two compromises (the deferred `toyos-elf` domain lint, and M8's `write_at` `# Safety` in the `dlopen` path) are filed in `issues/`. Co-Authored-By: Claude Opus 5.5 --- .../elf-domain-lint-line-not-yet-added.md | 22 ++ ...-not-the-sole-writer-of-a-mapped-module.md | 27 +++ kernel/src/elf/mod.rs | 7 +- kernel/src/elf/reloc.rs | 83 ++++---- kernel/src/loader/mod.rs | 60 +++--- kernel/src/loader/tls.rs | 10 - kernel/src/process.rs | 9 +- kernel/src/syscall/vm.rs | 3 +- .../src/bin/abuse_elf_loader.rs | 63 +++++- .../toyos-rust-tests/src/bin/tls_dtv_race.rs | 197 ++++++++++++++++++ tests/toyos.rs | 40 ++++ toyos-elf/src/layout.rs | 65 +++--- toyos-elf/src/rela.rs | 14 +- toyos-elf/src/sym.rs | 4 +- toyos-elf/src/tls.rs | 4 +- toyos-elf/tests/common/mod.rs | 13 ++ toyos-elf/tests/fuzz.rs | 10 +- toyos-elf/tests/real.rs | 2 +- toyos-elf/tests/tables.rs | 6 +- toyos-elf/tests/tls.rs | 25 ++- 20 files changed, 516 insertions(+), 148 deletions(-) create mode 100644 issues/design-debt/elf-domain-lint-line-not-yet-added.md create mode 100644 issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md create mode 100644 tests/toyos-rust-tests/src/bin/tls_dtv_race.rs diff --git a/issues/design-debt/elf-domain-lint-line-not-yet-added.md b/issues/design-debt/elf-domain-lint-line-not-yet-added.md new file mode 100644 index 00000000000..bee721c435d --- /dev/null +++ b/issues/design-debt/elf-domain-lint-line-not-yet-added.md @@ -0,0 +1,22 @@ +--- +status: open +kind: track +opened: 2026-09-27 +--- + +# `toyos-elf` does not `forbid(clippy::arithmetic_side_effects)` yet + +The design's domain-lint line — +`#![forbid(clippy::arithmetic_side_effects, clippy::indexing_slicing, …)]` on +`toyos-elf/src/lib.rs` — is what would make an unchecked `+`/`-`/`[]` on a +file-chosen value a compile error, so the "every number is checked" property +this crate rests on is enforced rather than reviewed. + +It is not added. About 100 existing sites in the crate use plain arithmetic and +indexing on values that are already bounded by other means, and each would need +either a checked form or a justified `#[allow]`. #544 wrote its new code +lint-clean but did not add the line or do the sweep; the design stages that as +E1. + +Exit condition: the line is on `lib.rs`, every site inside it is checked or +carries a one-clause `#[allow]`, and CI runs clippy on the crate with it. diff --git a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md new file mode 100644 index 00000000000..96ef81eb592 --- /dev/null +++ b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -0,0 +1,27 @@ +--- +status: open +kind: finding +opened: 2026-09-27 +--- + +# `LoadedLib::write_at`'s "sole writer" `# Safety` is false in the `dlopen` path (M8) + +`LoadedLib::write_at`'s `# Safety` says the caller must be the sole writer of +the module's image for the call's duration. In `load_shared_lib` that holds: the +image is exclusively owned before it is mapped. In `sys_dlopen` it does not. +`map_into` maps the module into the running process first, and only then does +`resolve_dlopen_relocs` / `apply_tpoff_relocs` / `apply_dtpoff_relocs` call +`write_at` — so a peer thread of the same process can be reading (or, for a +`Shared` module's private window, touching) those pages while the relocation +writes land. + +For an `Owned` module the writes go to freshly allocated pages the peer has no +handle to yet, so the race is benign in practice; for a `Shared` module the +writes go to the private `rw_alloc`, likewise not yet handed out. But the +`# Safety` contract as written is not the one the `dlopen` caller meets, so it +cannot be the thing that makes those `unsafe` blocks sound. + +This predates the value-checking work in #544 and was noted there rather than +fixed. Exit condition: either the writes move before `map_into`, or the +`# Safety` is restated to the invariant the `dlopen` path actually upholds and +every call site's `SAFETY:` cites it. diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index fe7ee949b4d..1e235f06ea9 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -17,8 +17,9 @@ 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, defining_module, - 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, resolve_lib_bind_relocs, resolve_tls_ref, + tpoff32_value, }; use crate::mm::{align_2m_checked, KernelSlice, MAX_HEAP_ALLOC, PAGE_2M, PAGE_BYTES}; @@ -380,7 +381,7 @@ pub fn load_shared_lib( let dyn_info = match layout.dynamic() { Some(dynamic) => { - let region = module.at(dynamic.image); + let region = module.at(dynamic.image()); // SAFETY: `region` came from the layout's own range inside the image; // `image` is still exclusively owned here. Dynamic::parse(unsafe { region.as_slice() }) diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index d7b28db7285..1bafede0551 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -50,22 +50,13 @@ impl LoadedLib { } } - fn bind_entries(&self) -> impl Iterator + '_ { - let cached = self.cached_relocs.as_ref().map(|r| r.bind.iter().copied()); - let scanned = self.cached_relocs.is_none().then(|| { - self.relocations().filter_map(|r| match r.op() { - Op::Bind(sym) => Some((r.offset(), sym)), - _ => None, - }) - }); - cached.into_iter().flatten().chain(scanned.into_iter().flatten()) - } - - fn tls_entries( - &self, - of: fn(Op) -> Option, - pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, TlsRef)>, - ) -> impl Iterator + '_ { + /// Every slot of one kind and what it names — from the cache when a clone + /// holds one, else scanned off the image, the two never both present. + fn tls_entries<'a, T: Copy + 'a>( + &'a self, + of: fn(Op) -> Option, + pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, T)>, + ) -> impl Iterator + 'a { let cached = self.cached_relocs.as_ref().map(|r| pick(r).iter().copied()); let scanned = self.cached_relocs.is_none().then(move || { self.relocations().filter_map(move |r| Some((r.offset(), of(r.op())?))) @@ -73,15 +64,12 @@ impl LoadedLib { cached.into_iter().flatten().chain(scanned.into_iter().flatten()) } + fn bind_entries(&self) -> impl Iterator + '_ { + self.tls_entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }, |c| &c.bind) + } + fn dtpmod_entries(&self) -> impl Iterator)> + '_ { - let cached = self.cached_relocs.as_ref().map(|r| r.dtpmod64.iter().copied()); - let scanned = self.cached_relocs.is_none().then(|| { - self.relocations().filter_map(|r| match r.op() { - Op::DtpMod64(sym) => Some((r.offset(), sym)), - _ => None, - }) - }); - cached.into_iter().flatten().chain(scanned.into_iter().flatten()) + self.tls_entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }, |c| &c.dtpmod64) } /// Every `RELATIVE` slot and the position in the image it points at. @@ -281,29 +269,36 @@ fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, t } } -/// `S + A` for one TLS reference, and the static-block offset of the module -/// it lies in: this module's own (`own_base_offset`), or the module defining -/// the symbol. `None` is a symbol no module defines, which is logged; a sum -/// outside the defining module's segment refuses the module. -fn resolve_tls( - lib: &LoadedLib, +/// `S + A` for one TLS reference, and the static-block offset of the module it +/// lies in: the referencing module's own (`own_base_offset`, `own_memsz`), or +/// the module defining the symbol. `None` is a symbol no module defines, which +/// is logged; a sum outside the defining module's segment refuses the module. +/// +/// `lookup` and `strings` are the referencing module's symbol table however it +/// is held — a library's in-image `SymTab`, or the executable's read off the +/// file — so both loaders resolve through this one function. +pub fn resolve_tls_ref( r: TlsRef, own_base_offset: usize, + own_memsz: Option, + lookup: impl Fn(SymIndex) -> Option, + strings: &[u8], tls_info: &TlsModuleInfo, ) -> Result, RelocError> { let s = match r { TlsRef::Own(at) => return Ok(Some((own_base_offset, at))), TlsRef::Symbol(s) => s, }; - let symbols = lib.symbols(); - if let Some(defined) = symbols.get(s.sym().get()).filter(|d| d.is_defined()) { - let at = s.offset_in(&defined, lib.tls_memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + let sym = lookup(s.sym()).ok_or(RelocError::SymbolPastTable)?; + if sym.is_defined() { + let memsz = own_memsz.ok_or(RelocError::TlsOutsideSegment)?; + let at = sym.tls_offset(s.addend(), memsz).ok_or(RelocError::TlsOutsideSegment)?; return Ok(Some((own_base_offset, at))); } - let name = symbols.name(s.sym().get()); + let name = sym.name_in(strings); match defining_module(name, tls_info) { Some((module, defined)) => { - let at = s.offset_in(&defined, module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + let at = defined.tls_offset(s.addend(), module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; Ok(Some((module.base_offset, at))) } None => { @@ -313,6 +308,24 @@ fn resolve_tls( } } +/// [`resolve_tls_ref`] for a loaded library, whose symbol table is in-image. +fn resolve_tls( + lib: &LoadedLib, + r: TlsRef, + own_base_offset: usize, + tls_info: &TlsModuleInfo, +) -> Result, RelocError> { + let symbols = lib.symbols(); + resolve_tls_ref( + r, + own_base_offset, + Some(lib.tls_memsz as u64), + |i| symbols.get(i.get()), + symbols.strings(), + tls_info, + ) +} + /// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module /// defines. fn compute_tpoff( diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index b6c3df9541e..31a199b8799 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -247,7 +247,7 @@ fn read_exe_tables( ) -> 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 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)); @@ -272,15 +272,20 @@ fn read_exe_tables( } None => Vec::new(), }; + let symtab_file_off = match dyn_info.symtab { + Some(vaddr) => Some(file_off(layout, path, "DT_SYMTAB", vaddr)?), + None => None, + }; + let sym_count = exe_sym_count(backing, layout, &dyn_info, path)?; + // 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. The - // symbol bound is left to `ExeTables::symbol`'s read off the file. + // 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()), - sym_count: usize::MAX, + sym_count, fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), tls_memsz: layout.tls_memsz(), }; @@ -297,11 +302,6 @@ fn read_exe_tables( None => Vec::new(), }; - let symtab_file_off = match dyn_info.symtab { - Some(vaddr) => Some(file_off(layout, path, "DT_SYMTAB", vaddr)?), - None => None, - }; - let sym_count = exe_sym_count(backing, layout, &dyn_info, path)?; let dynsym = match (symtab_file_off, sym_count) { (Some(off), n) if n > 0 => table(backing, path, "symbol count", off, n * sym::ENTRY_SIZE)?, _ => Vec::new(), @@ -565,8 +565,8 @@ pub fn spawn( }; log!("spawn: TLS {} modules, total_memsz={}", tls_modules.len(), tls.total_memsz()); - let Some((tls_pages, fs_base)) = - tls::map_block(&child_pt, &tls_modules, tls) + let Some((tls_pages, fs_base, _)) = + tls::TlsBlock::build(&tls_modules, tls).and_then(|b| b.publish(&child_pt)) else { log!("spawn: {}: failed to allocate TLS ({} bytes)", path, tls.total_memsz()); return Err(SyscallError::ResourceExhausted.into()); @@ -843,7 +843,9 @@ fn apply_tls_relocs( /// One of the executable's `TPOFF` relocations, resolved to a value: `0` for /// a symbol no module defines, a refusal for one the file does not hold or -/// whose `S + A` leaves the defining module's segment. +/// whose `S + A` leaves the defining module's segment. The executable's symbols +/// are read off the file, but the resolution is `elf::reloc`'s, shared with a +/// library's. #[allow(clippy::too_many_arguments)] fn exe_tpoff( exe: &ExeTables, @@ -854,28 +856,18 @@ fn exe_tpoff( tls: toyos_elf::tls::Static, tls_info: &elf::TlsModuleInfo, ) -> Result { - let (base_offset, at) = match r { - TlsRef::Own(at) => (exe_base_offset, at), - TlsRef::Symbol(s) => { - let sym = exe.symbol(backing, s.sym()).ok_or(RelocError::SymbolPastTable)?; - if sym.is_defined() { - let memsz = layout.tls_memsz().ok_or(RelocError::TlsOutsideSegment)?; - (exe_base_offset, s.offset_in(&sym, memsz).ok_or(RelocError::TlsOutsideSegment)?) - } else { - let name = sym.name_in(&exe.dynstr); - // `None` means a lib resolved the symbol but has no TLS module - // in the combined block — an inconsistency, logged rather than - // guessed at with base_offset 0. - let Some((module, defined)) = elf::defining_module(name, tls_info) else { - log!("tpoff: unresolved exe TLS symbol: {}", name); - return Ok(0); - }; - let at = s.offset_in(&defined, module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; - (module.base_offset, at) - } - } - }; - tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows) + let resolved = elf::resolve_tls_ref( + r, + exe_base_offset, + layout.tls_memsz(), + |i| exe.symbol(backing, i), + &exe.dynstr, + tls_info, + )?; + match resolved { + Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), + None => Ok(0), + } } /// The one program the kernel starts. `src/build.rs` puts this binary in every diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index cb17bc829d0..9f54c44a004 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -159,16 +159,6 @@ unsafe fn rebase(frames: &Unpublished, tp_offset: usize, at: UserAddr) { } } -/// Build one thread's TLS block and map it into the child address space; `None` when either fails. -pub fn map_block( - child_pt: &PageTables, - modules: &[TlsModule], - tls: Static, -) -> Option<(MappedPages, u64)> { - let (pages, fs_base, _) = TlsBlock::build(modules, tls)?.publish(child_pt)?; - Some((pages, fs_base)) -} - /// 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. diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 24870d86751..f63bb015f34 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -137,8 +137,7 @@ unsafe impl crate::mm::Allocation for PageAlloc { /// alone, and [`publish`](Self::publish) is the only way to map them: it /// consumes this, and runs the caller's last fix-up — the one that needs the /// address the frames will have — under the address-space lock that then maps -/// them. Nothing is written into the frames with a mapping in place, and -/// nothing is read back out of them once one is. +/// them. pub struct Unpublished(PageAlloc); impl Unpublished { @@ -162,10 +161,14 @@ impl Unpublished { /// frames freed, when `pt` has no room. pub fn publish(self, pt: &PageTables, prot: Prot, fix: impl FnOnce(&Self, UserAddr)) -> Option { let size = self.0.size() as u64; + let phys = self.0.phys(); + // `alloc_and_map` fuses the reserve and the map with no seam for `fix`, + // whose whole point is to run between them under one lock. + assert!(phys & (PAGE_2M - 1) == 0, "publish: phys {phys:#x} not 2MB-aligned"); let mut space = pt.lock(); let at = space.alloc_region(size, crate::vma::RegionKind::Mapped)?; fix(&self, at); - space.map_range(at, self.0.phys(), size, prot, CachePolicy::Normal); + space.map_range(at, phys, size, prot, CachePolicy::Normal); drop(space); Some(MappedPages::new(at, self.0)) } diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index 441098b38bf..48d0300743c 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -429,8 +429,7 @@ fn tls_alloc_block(module_id: u64) -> Result { // Found through the thread's own kernel-side TLS allocation, never by // chasing a pointer out of the FS base, which addresses user-writable - // memory. Every thread gets an allocation from `setup_tls`/ - // `setup_combined_tls`; its absence here is a kernel bug. + // memory. process::with_current_data(|data| { let tls = data.tls_pages.as_ref().expect("sys_tls_alloc_block: thread has no TLS allocation"); let dtv_kern = tls.ptr() as *mut u64; 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 90ddb0d5275..9f138d74963 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -546,6 +546,9 @@ fn main() { // 14. A cross-module initial-exec TLS reference resolves to `S + A - tp`. f13_cross_module_addend_is_kept(); + // 19. The apply-time TLS refusals, each named by its reason in the log. + tls_apply_time_refusals_are_reached(); + // 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(); @@ -710,7 +713,6 @@ fn values_are_bounded_by_the_image() { "so_init_array_past_image.so", &so_with(&[(DT_INIT_ARRAY, u64::MAX), (DT_INIT_ARRAYSZ, 8)], &[], None), ); - // Refused at load now; before, `dlsym` added the value to the module's base. let far = so_with(&[], &[], None); let far = patch_sym(far, 0x218, 1, (STB_GLOBAL << 4) | STT_FUNC, 1, FAR_VALUE); let path = write_file("so_export_past_image.so", &far); @@ -821,7 +823,8 @@ fn f13_cross_module_addend_is_kept() { drop(lib_defs); } -/// Defines `xtls` (`STT_TLS`, offset 8) for a cross-module `TPOFF64`. +/// Defines `xtls` (`STT_TLS`, offset 8) for a cross-module `TPOFF64`, with a +/// 0x200-byte `PT_TLS` its addends stay inside. fn tls_defs_so() -> Vec { Elf::new(0x2000) .ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_X)) @@ -836,6 +839,62 @@ fn tls_defs_so() -> Vec { .build() } +/// The two apply-time TLS refusals — a `dlopen`ed library's and an +/// executable's — are new code no other case here reaches, because every other +/// TLS case uses `r_sym == 0`, which `rela::parse` refuses before either apply +/// pass runs. Both refuse a *resolved* `S + A` outside the defining module's +/// `PT_TLS`, named `TLS_OUTSIDE_SEGMENT` in the kernel log the harness checks. +/// Each references its *own* defined `STT_TLS` symbol, so the module resolved +/// against is unambiguously this fixture's and not some other loaded module's. +fn tls_apply_time_refusals_are_reached() { + // dlopen side: `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 side: `apply_tls_relocs` refuses during `spawn`. + spawn_refused("tls_apply_spawn", &exe_tls_ref_past_segment()); +} + +/// A shared object defining its own `xtls` (`STT_TLS`, offset 8) in a 0x20-byte +/// `PT_TLS`, with a `TPOFF64` against it whose `S + A` (0x148) leaves it. +fn so_tls_ref_past_segment() -> Vec { + Elf::new(0x5000) + .ph(Phdr::load(0, 0, 0x1000, 0x1000, PF_R | PF_X)) + .ph(Phdr::load(0x1000, 0x1000, 0x4000, 0x4000, PF_R | PF_W)) + .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x600, vaddr: 0x600, filesz: 0x200, memsz: 0x200, align: 8 }) + .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz: 0x20, align: 16 }) + .sections(0x900, 1, 64) + .dynamic(0x600, &[ + (DT_SYMTAB, 0x200), (DT_STRTAB, 0x300), (DT_STRSZ, 0x100), + (DT_RELA, 0x800), (DT_RELASZ, 24), + ]) + // sym[1] xtls: defined STT_TLS, offset 8 in the block. + .sym(0x218, 1, (STB_GLOBAL << 4) | STT_TLS, 1, 8) + .poke(0x301, b"xtls\0") + .rela(0x800, 0x1000, (1u64 << 32) | R_X86_64_TPOFF64, 0x140) + .shdr(0x900, SHT_DYNSYM, 0x200, 48, 24) + .build() +} + +/// An executable defining `xtls` (`STT_TLS`, offset 8) in a 0x20-byte `PT_TLS`, +/// with a `TPOFF64` against it whose `S + A` (0x148) leaves the segment. +fn exe_tls_ref_past_segment() -> Vec { + Elf::new(0x4000) + .ph(Phdr::load(0, 0, 0x4000, 0x4000, PF_R | PF_W)) + .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) + .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x3000, vaddr: 0x3000, filesz: 0, memsz: 0x20, align: 16 }) + .entry(0) + .dynamic(0x1000, &[ + (DT_SYMTAB, 0x1400), (DT_STRTAB, 0x1800), (DT_STRSZ, 0x100), + (DT_RELA, 0x1200), (DT_RELASZ, 24), + ]) + // sym[1] xtls: defined STT_TLS, offset 8 in the block. + .sym(0x1418, 1, (STB_GLOBAL << 4) | STT_TLS, 1, 8) + .poke(0x1801, b"xtls\0") + .rela(0x1200, 0x2000, (1u64 << 32) | R_X86_64_TPOFF64, 0x140) + .build() +} + /// Two `TPOFF64` relocations against the undefined `xtls`, addends 0 and /// `addend`, patching exported data `probe0`/`probeN` a reader can difference. fn tls_refs_so(addend: i64) -> Vec { diff --git a/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs new file mode 100644 index 00000000000..d8b8d58218e --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs @@ -0,0 +1,197 @@ +//! C1: a sibling thread must not be able to corrupt the next thread's TLS +//! block between the kernel mapping it and the kernel finishing its rebase. +//! +//! At head the kernel rebases the block *before* it maps it, so a sibling never +//! sees a pointer the kernel has yet to fix. With the review's mutation (`fix` +//! moved after `map_range` and `drop(space)`), the block is visible while its +//! DTV still holds physical addresses, and a sibling that stores 0 into a DTV +//! slot makes the rebase's `entry - phys` underflow — a kernel panic reached +//! from userland, which is what this test denies. +//! +//! The race is arranged so it needs no luck to be *safe*: every store the +//! sibling makes is gated by a `random` probe that answers `BadAddress` while +//! the address is unmapped, and the block it targets is either a live worker's +//! (mapped for that worker's whole life) or, during the window under test, one +//! the kernel is mapping. The worker's block is reused at one virtual address +//! round after round — one worker at a time on a fixed stack, so the only +//! churning arena allocation is the kernel's TLS block — so the sibling can +//! hammer that address in advance of each spawn. +//! +//! Kernel liveness is the verdict: under the mutation the machine panics and +//! the guest dies; at head every round completes, every worker finds its own +//! TP and DTV self-consistent, and the heap still walks. + +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}; +use toyos_abi::syscall; + +/// 2 MiB, the TLS block's alignment and (for this process's small TLS) its size. +const BLOCK: u64 = 2 * 1024 * 1024; +const MASK: u64 = BLOCK - 1; +/// DTV slot 0 (module 1) sits two words into the block: a generation word, a +/// length word, then the entries. `kernel/src/loader/tls.rs` owns this layout. +const DTV_SLOT0: u64 = 16; + +/// The block base the sibling hammers, or 0 while it must stand down. +static TARGET: AtomicU64 = AtomicU64::new(0); +/// The current worker has read its TP and published `TARGET`. +static READY: AtomicBool = AtomicBool::new(false); +/// The current worker may exit. +static GO_EXIT: AtomicBool = AtomicBool::new(false); +/// The sibling must not touch memory (a worker is being torn down). +static PAUSE: AtomicBool = AtomicBool::new(true); +/// The sibling has observed `PAUSE` and is idle at the top of its loop, past any +/// store — so the main thread may free the block without racing a store. +static PAUSED_ACK: AtomicBool = AtomicBool::new(false); +/// The sibling must end. +static STOP: AtomicBool = AtomicBool::new(false); +/// A worker found its TP and DTV inconsistent — a corruption the kernel let +/// through rather than panicking on. +static INCONSISTENT: AtomicBool = AtomicBool::new(false); +/// How many times the sibling found the block mapped and hammered it — proof it +/// engaged a real reused block rather than spinning on an unmapped address. +static ENGAGED: AtomicU64 = AtomicU64::new(0); + +/// The self-pointer the psABI puts at the thread pointer (`%fs:0` on variant +/// II); the block base is that rounded down to the allocation's alignment. +#[cfg(target_arch = "x86_64")] +fn fs_base() -> u64 { + let tp: u64; + // SAFETY: reads the thread-pointer self-word the kernel wrote at TP+0. + unsafe { core::arch::asm!("mov {}, fs:0", out(reg) tp, options(nostack, readonly)) }; + tp +} + +/// One worker: publish its block, prove its own TP/DTV consistent, and wait to +/// be retired. Runs on a fixed stack; only one worker exists at a time. +extern "C" fn worker(_arg: u64) { + let tp = fs_base(); + let v = tp & !MASK; + // TP+8 is the DTV pointer, which the kernel rebased to the block base. A + // block the kernel published half-rebased would fail this. + // SAFETY: TP+8 is inside this thread's own TCB. + let dtv = unsafe { ((tp + 8) as *const u64).read_volatile() }; + if dtv != v { + INCONSISTENT.store(true, SeqCst); + } + TARGET.store(v, SeqCst); + READY.store(true, SeqCst); + while !GO_EXIT.load(SeqCst) { + core::hint::spin_loop(); + } + syscall::thread_exit(0); +} + +/// Whether `addr` is mapped, by the `random` syscall — it answers `BadAddress` +/// while the page is unmapped and writes into it otherwise. The probe writes one +/// byte at the block's generation word (harmless). +fn mapped(v: u64) -> bool { + // A `&mut [u8]` over memory this thread does not own is the price of using + // the mandated mapped-ness probe; nothing here reads it, and the syscall + // checks the mapping before any access. + // SAFETY: on `BadAddress` nothing is touched; the caller stores only after + // this returns true and only while the block stays mapped (see `sibling`). + let probe = unsafe { core::slice::from_raw_parts_mut(v as *mut u8, 1) }; + syscall::random(probe).is_ok() +} + +fn sibling() { + while !STOP.load(SeqCst) { + if PAUSE.load(SeqCst) { + // Idle and past any store: the main thread waits for this before it + // frees the block, so no store is ever in flight against a free. + PAUSED_ACK.store(true, SeqCst); + core::hint::spin_loop(); + continue; + } + PAUSED_ACK.store(false, SeqCst); + let v = TARGET.load(SeqCst); + // Wait for the block to be mapped (a spawn is placing it), then hammer 0 + // into DTV slot 0 to arm the rebase underflow, in a tight loop with no + // syscall so the store lands inside the map-to-rebase window. `PAUSE` + // ends the loop before the main thread frees the block. + if v != 0 && mapped(v) { + ENGAGED.fetch_add(1, SeqCst); + while !PAUSE.load(SeqCst) && !STOP.load(SeqCst) { + // SAFETY: confirmed mapped; the worker holding it does not exit + // until the main thread pauses this loop and waits for the ack. + unsafe { ((v + DTV_SLOT0) as *mut u64).write_volatile(0) }; + } + } + } +} + +/// How many spawn/retire rounds to run; each is one race attempt, the sibling +/// hammering the block's DTV slot 0 through the whole map-to-rebase window, and +/// the block reused at one address across all of them. Bounded so the run +/// finishes on the dev host's TCG, where two busy vCPUs and a kernel log per +/// spawn make each round dear; under the mutation the panic comes in the first +/// rounds. +const ROUNDS: u64 = 800; + +fn main() { + if cfg!(not(target_arch = "x86_64")) { + println!("tls_dtv_race: only x86_64 reads %fs:0; skipping"); + return; + } + + let sib = std::thread::Builder::new() + .name("dtv-sibling".into()) + .spawn(sibling) + .expect("spawn sibling"); + + // One reused stack: with a single worker alive at a time, the only arena + // allocation that churns is the kernel's per-thread TLS block, so it is + // reused at one virtual address — the one the sibling hammers. + const STACK: usize = 256 * 1024; + let stack = vec![0u8; STACK].leak(); + let base = stack.as_ptr() as u64; + let top = (base + STACK as u64) & !15; + + let mut rounds = 0u64; + for _ in 0..ROUNDS { + READY.store(false, SeqCst); + GO_EXIT.store(false, SeqCst); + // Let the sibling hammer the address a retiring block last held; the + // fresh spawn maps that same address, and the window under test is + // between that map and the rebase. + PAUSE.store(false, SeqCst); + // SAFETY: `worker` is a valid entry; `top`/`base` describe the leaked stack. + let entry = (worker as *const ()).expose_provenance() as u64; + let tid = unsafe { syscall::thread_spawn(entry, top, 0, base) }; + assert!(syscall::SyscallError::from_u64(tid).is_none(), "thread_spawn failed: {tid}"); + while !READY.load(SeqCst) { + core::hint::spin_loop(); + } + // Retire the worker with the sibling stood down, so no store can land in + // a block the kernel is freeing. Wait for the sibling to acknowledge it + // is idle — a store already in flight completes while the block is still + // mapped, before the join frees it. + PAUSE.store(true, SeqCst); + while !PAUSED_ACK.load(SeqCst) { + core::hint::spin_loop(); + } + GO_EXIT.store(true, SeqCst); + syscall::thread_join(tid); + rounds += 1; + + if rounds % 4096 == 0 { + // The heap still walks: allocate, touch, free. + let probe = vec![rounds as u8; 4096]; + assert_eq!(probe[0], rounds as u8); + } + } + + STOP.store(true, SeqCst); + sib.join().expect("join sibling"); + + assert!(!INCONSISTENT.load(SeqCst), "a worker's TP and DTV disagreed — the block was corrupted"); + + // The kernel is still alive: a plain thread spawns, runs and joins. + let n = std::thread::spawn(|| 0xC1u64).join().expect("final thread"); + assert_eq!(n, 0xC1); + + println!( + "tls_dtv_race: {rounds} rounds, {} engaged, kernel alive, TP/DTV consistent", + ENGAGED.load(SeqCst) + ); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 1e413c4835a..753bcdca82d 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -3492,12 +3492,49 @@ fn check_for(name: &str) -> fn(&TestResult) -> bool { "fault_gates" => check_fault_gates, "debug_trap" => check_debug_trap, "dlopen_dedup" => check_dlopen_dedup, + "abuse_elf_loader" => check_abuse_elf_loader, "syscall_cost" => check_syscall_cost, "exit_wait_storm" => check_exit_wait_storm, _ => check_rust_result, } } +/// The reason the loader logs when a resolved TLS `S + A` leaves the defining +/// module's `PT_TLS` — `RelocError::TlsOutsideSegment`, kept in step with +/// `toyos-elf/src/rela.rs`. +const TLS_OUTSIDE_SEGMENT: &str = "ELF: TLS relocation names an offset outside its PT_TLS"; + +/// `abuse_elf_loader` plus the reason each apply-time TLS refusal must fire for. +/// +/// The two cases (`tls_apply_refs.so` on `dlopen`, `tls_apply_spawn` on +/// `spawn`) are refused for the right reason only if the kernel names +/// [`TLS_OUTSIDE_SEGMENT`] beside the file — a case refused later, for another +/// reason, would pass the exit-code check alone. +fn check_abuse_elf_loader(result: &TestResult) -> bool { + if !check_rust_result(result) { + return false; + } + let log = format!("{}{}", result.before, result.serial); + let mut ok = true; + for (file, what) in [ + ("tls_apply_refs.so", "the dlopen apply-time TLS refusal"), + ("tls_apply_spawn", "the spawn apply-time TLS refusal"), + ] { + let named = log + .lines() + .any(|l| l.contains(file) && l.contains(TLS_OUTSIDE_SEGMENT)); + if !named { + eprintln!( + "FAIL rs::abuse_elf_loader: {what} did not fire for its reason — no line names \ + {file:?} with {TLS_OUTSIDE_SEGMENT:?}{}", + kernel_account(result) + ); + ok = false; + } + } + ok +} + /// What a loader writes when it caches a library under the directory it searched /// and did not find it in. Only `dlopen_dedup`'s last arm produces this string. const FALLBACK_MISCACHED: &str = "dlopen: cached /tmp/dlopen-dedup/libtls_lib.so"; @@ -18478,6 +18515,9 @@ fn build_test_registry( // the slow one: it spends its own patience before reporting, and // the report is worth more than the harness's timeout message. "inbox_cancel_wakes" => Duration::from_secs(30), + // Twenty thousand spawn/retire rounds, each a race attempt against + // the TLS-block rebase. + "tls_dtv_race" => Duration::from_secs(60), _ => Duration::from_secs(5), }; tests.push(TestDef { diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index fe05e323f56..846009155c4 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -23,8 +23,9 @@ pub struct Extent { } impl Extent { - /// `None` for `min > max`, which no image has. - pub const fn new(min: u64, max: u64) -> Option { + /// `None` for `min > max`, which no image has. Crate-private: an extent is + /// the hull [`Layout::parse`] derives, never a bound a caller hands in. + pub(crate) const fn new(min: u64, max: u64) -> Option { if min > max { return None; } @@ -216,8 +217,20 @@ impl TlsSegment { /// `PT_DYNAMIC`, where the file holds it and where the image does. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DynamicSegment { - pub file_offset: u64, - pub image: ImageRange, + pub(crate) file_offset: u64, + pub(crate) image: ImageRange, +} + +impl DynamicSegment { + /// `p_offset`: where the file holds `PT_DYNAMIC`. + pub const fn file_offset(&self) -> u64 { + self.file_offset + } + + /// Where the image holds it, inside the extent. + pub const fn image(&self) -> ImageRange { + self.image + } } /// Where the section header table is, when the file has a usable one. @@ -274,16 +287,6 @@ pub struct Layout { eh_frame_hdr: Option, } -/// A `PT_LOAD` as read, before the extent it belongs to is known. -#[derive(Clone, Copy)] -struct RawLoad { - vaddr: u64, - memsz: u64, - filesz: u64, - file_offset: u64, - flags: u32, -} - impl Layout { /// Parse program headers out of the first bytes of a file. /// @@ -297,8 +300,6 @@ impl Layout { } let phdrs = ehdr.program_headers(data)?; - let mut loads = [RawLoad { vaddr: 0, memsz: 0, filesz: 0, file_offset: 0, flags: 0 }; - MAX_LOAD_SEGMENTS]; let mut segment_count = 0usize; let mut vaddr_min = u64::MAX; let mut vaddr_max = 0u64; @@ -306,6 +307,9 @@ impl Layout { let mut dynamic = None; let mut eh_frame_hdr = None; + // First pass: the extent, the singleton headers, and every `PT_LOAD` + // field validated — so the second pass over the same headers only places + // the segments the extent it derives here holds. for i in 0..usize::from(ehdr.phnum) { let Some(phdr) = ProgramHeader::parse(phdrs, i) else { return Err(Error::ProgramHeadersOutsideBuffer); @@ -322,14 +326,9 @@ impl Layout { if phdr.offset.checked_add(phdr.filesz).is_none() { return Err(Error::FileExtentOverflows); } - let slot = loads.get_mut(segment_count).ok_or(Error::TooManyLoadSegments)?; - *slot = RawLoad { - vaddr: phdr.vaddr, - memsz: phdr.memsz, - filesz: phdr.filesz, - file_offset: phdr.offset, - flags: phdr.flags, - }; + if segment_count >= MAX_LOAD_SEGMENTS { + return Err(Error::TooManyLoadSegments); + } segment_count = segment_count.wrapping_add(1); vaddr_min = vaddr_min.min(phdr.vaddr); vaddr_max = vaddr_max.max(seg_end); @@ -353,15 +352,21 @@ impl Layout { flags: SegmentFlags(0), }; let mut segments = [blank; MAX_LOAD_SEGMENTS]; - for (slot, raw) in segments.iter_mut().zip(loads.iter().take(segment_count)) { + let mut placed = 0usize; + for i in 0..usize::from(ehdr.phnum) { + let phdr = ProgramHeader::parse(phdrs, i).ok_or(Error::ProgramHeadersOutsideBuffer)?; + if phdr.kind != PT_LOAD { + continue; + } // Inside by construction: the extent is the hull of these. - let image = extent.range(raw.vaddr, raw.memsz).ok_or(Error::SegmentExtentOverflows)?; - *slot = Segment { + let image = extent.range(phdr.vaddr, phdr.memsz).ok_or(Error::SegmentExtentOverflows)?; + segments[placed] = Segment { image, - filesz: raw.filesz, - file_offset: raw.file_offset, - flags: SegmentFlags(raw.flags), + filesz: phdr.filesz, + file_offset: phdr.offset, + flags: SegmentFlags(phdr.flags), }; + placed = placed.wrapping_add(1); } // Every other program header names a vaddr the loader turns into an diff --git a/toyos-elf/src/rela.rs b/toyos-elf/src/rela.rs index 9f01f8ffd19..e5676f3f594 100644 --- a/toyos-elf/src/rela.rs +++ b/toyos-elf/src/rela.rs @@ -16,7 +16,7 @@ use crate::header::Machine; use crate::layout::{Extent, ImageOffset}; use crate::read; -use crate::sym::{Sym, SymIndex}; +use crate::sym::SymIndex; use crate::tls::TlsOffset; /// Bytes in one `Elf64_Rela`. @@ -219,20 +219,16 @@ impl TlsSymRef { self.sym } - /// `S + A`, where `defined` is the symbol as its defining module holds it - /// and `memsz` that module's `PT_TLS` size; `None` when the sum leaves the - /// segment. - pub fn offset_in(self, defined: &Sym, memsz: u64) -> Option { - defined.tls_offset(self.addend, memsz) + /// The addend `A`, resolved against the defining symbol's `S` through + /// [`crate::Sym::tls_offset`]. + pub const fn addend(self) -> i64 { + self.addend } } /// Parse one entry against the module's [`Rules`]: `Ok(None)` for a type this /// loader does not write, the reason for one it must not. /// -/// Parsed ahead of the first write, not as each one happens: a module that is -/// refused halfway through has already been modified. -/// /// The window is the *writable* one rather than the whole image: once the /// module is cached its read-only pages are shared between processes, and the /// write lands in a private allocation covering only that window. diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index fbe4f212f33..82b5b4755bb 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -3,9 +3,7 @@ //! [`SymTab::get`] answers `None` past the end rather than reading a partial //! record, and a [`Sym`]'s `st_value` is readable only as what it names: an //! [`ImageOffset`] inside its module ([`Sym::address`]) or a [`TlsOffset`] -//! inside its module's TLS segment ([`Sym::tls_offset`]). The number itself is -//! the file's, and a loader that added it to a base unchecked was a kernel -//! panic away from any process that could write a file. +//! inside its module's TLS segment ([`Sym::tls_offset`]). use crate::layout::{Extent, ImageOffset}; use crate::read; diff --git a/toyos-elf/src/tls.rs b/toyos-elf/src/tls.rs index 3d25128b607..f1f5782b204 100644 --- a/toyos-elf/src/tls.rs +++ b/toyos-elf/src/tls.rs @@ -156,7 +156,9 @@ pub struct TlsOffset(u64); impl TlsOffset { /// `value + addend`, when it lies inside a segment of `memsz` bytes. - pub const fn of(value: u64, addend: i64, memsz: u64) -> Option { + /// Crate-private: a TLS offset is a symbol's, reached through + /// [`crate::Sym::tls_offset`], never a bound a caller hands in. + pub(crate) const fn of(value: u64, addend: i64, memsz: u64) -> Option { match value.checked_add_signed(addend) { Some(at) if at <= memsz => Some(TlsOffset(at)), _ => None, diff --git a/toyos-elf/tests/common/mod.rs b/toyos-elf/tests/common/mod.rs index 6f3817659c5..a3a01a688e7 100644 --- a/toyos-elf/tests/common/mod.rs +++ b/toyos-elf/tests/common/mod.rs @@ -184,6 +184,19 @@ impl Elf { } } +/// The extent `Layout::parse` derives for a single `PT_LOAD` spanning +/// `[min, max]`: the only way a test names an [`toyos_elf::Extent`], since its +/// constructor is the parse's alone. +pub fn extent(min: u64, max: u64) -> toyos_elf::Extent { + let bytes = Elf::new(0x200) + .entry(min) + .ph(Phdr::load(0x100, min, 0, max - min, PF_R | PF_X)) + .build(); + toyos_elf::Layout::parse(&bytes, toyos_elf::Machine::X86_64) + .unwrap() + .extent() +} + /// One `Elf64_Rela`, as bytes. pub fn rela(r_offset: u64, r_sym: u32, r_type: u32, r_addend: i64) -> [u8; 24] { let mut out = [0u8; 24]; diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index 65c0689adcd..4ff15971bef 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -2,8 +2,8 @@ //! `DT_INIT_ARRAY`, the TLS segment's size and alignment, and every `TPOFF` //! derived from them. //! -//! Every other test here varies where a table is, never what it says, and four -//! kernel panics lived in what it says. Each image is a structurally valid +//! Every other test here varies where a table is, never what it says. Each +//! image is a structurally valid //! module — text, data, `PT_DYNAMIC`, `PT_TLS`, a relocation table, a symbol //! table, an init array — whose numbers are drawn honest most of the time and //! hostile the rest, and then run through the calls the kernel's loader makes, @@ -275,7 +275,7 @@ impl Reached { /// The bytes the image holds at `range`, as a loader holding the image reads /// them. This image's file offsets are its image offsets. -fn at<'a>(bytes: &'a [u8], range: ImageRange) -> &'a [u8] { +fn at(bytes: &[u8], range: ImageRange) -> &[u8] { let start = range.start().get() as usize; bytes.get(start..start + range.len() as usize).unwrap_or(&[]) } @@ -310,7 +310,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), image_start.checked_add(offset).expect("an offset inside the span leaves the placement") }; - let dyn_bytes = at(&case.bytes, layout.dynamic().ok_or(())?.image); + let dyn_bytes = at(&case.bytes, layout.dynamic().ok_or(())?.image()); let dynamic = Dynamic::parse(dyn_bytes); if let Some(init) = InitArray::parse(dynamic.init_array, extent).map_err(|_| ())? { @@ -411,7 +411,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), // another module's, which the kernel resolves by name and this // image has no other module to find it in. TlsRef::Symbol(s) => match symbols.get(s.sym().get()).filter(|d| d.is_defined()) { - Some(d) => s.offset_in(&d, memsz).map(Some).ok_or(()), + Some(d) => d.tls_offset(s.addend(), memsz).map(Some).ok_or(()), None => Ok(None), }, } diff --git a/toyos-elf/tests/real.rs b/toyos-elf/tests/real.rs index d1663229bb9..6da7bdaa5a9 100644 --- a/toyos-elf/tests/real.rs +++ b/toyos-elf/tests/real.rs @@ -30,7 +30,7 @@ fn a_toyos_ld_binary_parses_to_what_readelf_says() { assert!(!segs[2].writable() && !segs[2].flags().executable()); assert_eq!(layout.writable_window(), Some((0x145000, 0x155000))); - let dynamic = layout.dynamic().map(|d| (d.file_offset, d.image.start().get(), d.image.len())); + let dynamic = layout.dynamic().map(|d| (d.file_offset(), d.image().start().get(), d.image().len())); assert_eq!(dynamic, Some((0x166490, 0x166490, 0x60))); let eh = layout.eh_frame_hdr().map(|r| (r.start().get(), r.len())); assert_eq!(eh, Some((0x13e118, 0x688c))); diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index 48aa0be683b..c93f6bdde53 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -18,7 +18,7 @@ use toyos_elf::gnu_hash::{self, GnuHash}; use toyos_elf::rela::{self, FillLattice, 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::{Extent, Machine}; +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 @@ -30,7 +30,7 @@ fn validate( fill: Option, ) -> Result<(), RelocError> { let rules = Rules { - extent: Extent::new(0, u64::MAX).unwrap(), + extent: extent(0, u64::MAX), window, sym_count, fill, @@ -734,7 +734,7 @@ fn each_machine_reads_its_own_relocation_numbers() { let entry = RelaTable::new(&bytes, Machine::Aarch64).get(0).unwrap(); assert_eq!(entry.kind(), RelocKind::Relative); let rules = Rules { - extent: Extent::new(0, 0x100).unwrap(), + extent: extent(0, 0x100), window: (0, 0x100), sym_count: 0, fill: None, diff --git a/toyos-elf/tests/tls.rs b/toyos-elf/tests/tls.rs index 0cede47ad85..cadc307e56d 100644 --- a/toyos-elf/tests/tls.rs +++ b/toyos-elf/tests/tls.rs @@ -5,15 +5,26 @@ //! kernel-bug assert reached from a crafted `PT_TLS`. The property is proved //! here instead, which is what lets the assert go. +use toyos_elf::sym; use toyos_elf::tls::{self, Static, TlsOffset, Variant}; const TCB: usize = 64; const DTV: usize = 16 + 64 * 8; const GRANULE: usize = 2 * 1024 * 1024; +/// `S + A` inside a segment of `memsz` bytes, through the only public path to a +/// [`TlsOffset`]: a crafted `STT_TLS` symbol read as one. +fn tls_offset(value: u64, addend: i64, memsz: u64) -> Option { + let mut bytes = [0u8; sym::ENTRY_SIZE]; + bytes[4] = (1 << 4) | 6; // STB_GLOBAL, STT_TLS + bytes[6..8].copy_from_slice(&1u16.to_le_bytes()); // st_shndx: defined + bytes[8..16].copy_from_slice(&value.to_le_bytes()); // st_value + sym::parse_at(&bytes, 0).unwrap().tls_offset(addend, memsz) +} + /// A datum `off` bytes into a segment no offset here leaves. fn datum(off: u64) -> TlsOffset { - TlsOffset::of(off, 0, u64::MAX).unwrap() + tls_offset(off, 0, u64::MAX).unwrap() } #[test] @@ -114,7 +125,7 @@ fn tpoff_carries_the_addend() { assert_eq!(one.tpoff(0, datum(module_addr)), Some(module_addr as i64 + 64)); for &addend in &[0i64, 8, -8, 0x100, -0x100] { // `S + A` below the segment's start names nothing in it. - let Some(sum) = TlsOffset::of(module_addr, addend, u64::MAX) else { + let Some(sum) = tls_offset(module_addr, addend, u64::MAX) else { assert!(addend < 0 && addend.unsigned_abs() > module_addr); continue; }; @@ -131,18 +142,18 @@ fn tpoff_carries_the_addend() { #[test] fn a_tpoff_no_segment_or_word_holds_is_refused() { let s = Static::new(Variant::II, 16, 8, 8).unwrap(); - assert_eq!(TlsOffset::of(0, i64::MIN, 16), None); - assert_eq!(TlsOffset::of(16, i64::MAX, 16), None); + 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 // thread-pointer offset does not. - let huge = TlsOffset::of(16, i64::MAX, u64::MAX).unwrap(); + 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(); assert_eq!(wide.tpoff(0, datum(0)), None); // The inclusive end is a datum: one past the segment's last byte. - assert_eq!(TlsOffset::of(8, 8, 16).map(TlsOffset::get), Some(16)); - assert_eq!(TlsOffset::of(8, 9, 16), None); + assert_eq!(tls_offset(8, 8, 16).map(TlsOffset::get), Some(16)); + assert_eq!(tls_offset(8, 9, 16), None); } /// Variant I, as lld resolves an AArch64 executable's own local-exec access From 19c5b9e301d9d4a98d661f25bcc01e9dab048e87 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 18:43:07 +0200 Subject: [PATCH 04/10] review #544 round 2: a deterministic C1 control, TLS offsets bounded by the parse BLOCKERs - B2. `tls-rebase-window` (new actuator): a thread spawn whose argument is `loader::rebase_window::MARK` is watched inside `TlsBlock::publish`'s `fix`, before `rebase`. If the block is present in the running address space there, it holds until DTV slot 0 differs from what `build_combined` wrote (10 s bound, loud assert); if not, it logs that and proceeds. `tls_rebase_window` boots it with two CPUs and requires one "not reachable" line per watched spawn (15). `tls_dtv_race` now waits for the sibling to engage every round (bounded), so engagement is asserted rather than hoped for; the first round places the block and the rest are watched. The round-1 mutation (`fix` after `map_range` and `drop(space)`) is red on three of three runs, wide and alone, with `rebase`'s `p - phys` underflow. - B3. The f13 pair as a `dlopen_refused` case: `tls_defs_so` at `memsz: 0x20` and `tls_refs_so` at addend 0x140, under their own symbol name because `dlclose` unloads nothing and f13's `xtls` would be resolved first. The harness asserts its reason beside `f13_refs_past.so`. - B4. `TlsOffset::of` takes a `TlsSegment`; `Sym::tls_offset(addend, TlsSegment)`, `Rules.tls: Option` and `SymTab::bounded(extent, Option)`. `LoadedLib` keeps the segment in its rules (`LoadedLib::tls`) instead of `tls_memsz`/`tls_align`, and `defining_module` hands back the defining module's segment. `Layout::tls_memsz` is deleted; tests name a segment through `Layout::parse` (`common::tls_segment`). - B5. `ExeTables::symbol` and `symtab_file_off` are deleted; the executable's relocations look up through `exe.symbols().get(i)`, and `resolve_tls_ref` takes a `SymTab`. - B6. `address_of` returns `None` for an undefined or `STT_TLS` symbol and panics (`#[cold]` `bounded_symbol_outside`, beside `reparse_refused`) on an extent miss for a defined one. NOTEs - `tls_entries` is `entries`; `LoadedLib::resolve_tls` is gone (its one caller asks `symbols().find_tls`), so the free `resolve_tls` has no namesake. - `exe_tpoff` is folded into its closure; both `too_many_arguments` allows go. - The harness imports `RelocError::TlsOutsideSegment.as_str()`. - `tls_dtv_race` drops the dead heap walk, asserts `thread_join`'s 0, and reads no thread pointer: the block base is a thread-local's address, so it runs on both variants with no asm. - `DynamicSegment`'s fields are private. - Both new issues are assigned to the ELF-loader track. REMOVEs applied as deletions. Co-Authored-By: Claude Opus 5.5 --- bootloader/src/main.rs | 2 +- .../elf-domain-lint-line-not-yet-added.md | 10 +- ...-not-the-sole-writer-of-a-mapped-module.md | 7 +- kernel/src/actuator.rs | 7 + kernel/src/elf/cache.rs | 6 - kernel/src/elf/mod.rs | 45 ++--- kernel/src/elf/reloc.rs | 73 ++++---- kernel/src/loader/mod.rs | 62 ++----- kernel/src/loader/tls.rs | 77 +++++++- kernel/src/process.rs | 3 + kernel/src/syscall/vm.rs | 8 +- .../src/bin/abuse_elf_loader.rs | 50 +++--- .../toyos-rust-tests/src/bin/tls_dtv_race.rs | 168 ++++++------------ tests/toyos.rs | 44 ++++- toyos-elf/src/layout.rs | 11 +- toyos-elf/src/rela.rs | 12 +- toyos-elf/src/sym.rs | 18 +- toyos-elf/src/tls.rs | 11 +- toyos-elf/tests/common/mod.rs | 11 ++ toyos-elf/tests/fuzz.rs | 15 +- toyos-elf/tests/tables.rs | 6 +- toyos-elf/tests/tls.rs | 12 +- 22 files changed, 333 insertions(+), 325 deletions(-) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index d9fae4f92d5..a33739ee4dc 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -391,7 +391,7 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { window: (0, mem_size as u64), sym_count: 0, fill: None, - tls_memsz: None, + tls: None, }; let mut reloc_count = 0u64; for section in rela_sections { diff --git a/issues/design-debt/elf-domain-lint-line-not-yet-added.md b/issues/design-debt/elf-domain-lint-line-not-yet-added.md index bee721c435d..1080f98e4fc 100644 --- a/issues/design-debt/elf-domain-lint-line-not-yet-added.md +++ b/issues/design-debt/elf-domain-lint-line-not-yet-added.md @@ -1,22 +1,20 @@ --- -status: open +status: assigned kind: track opened: 2026-09-27 --- # `toyos-elf` does not `forbid(clippy::arithmetic_side_effects)` yet +Held by the orchestrator's ELF-loader track; its next brief carries this. + The design's domain-lint line — `#![forbid(clippy::arithmetic_side_effects, clippy::indexing_slicing, …)]` on `toyos-elf/src/lib.rs` — is what would make an unchecked `+`/`-`/`[]` on a file-chosen value a compile error, so the "every number is checked" property this crate rests on is enforced rather than reviewed. -It is not added. About 100 existing sites in the crate use plain arithmetic and -indexing on values that are already bounded by other means, and each would need -either a checked form or a justified `#[allow]`. #544 wrote its new code -lint-clean but did not add the line or do the sweep; the design stages that as -E1. +It is not added. Exit condition: the line is on `lib.rs`, every site inside it is checked or carries a one-clause `#[allow]`, and CI runs clippy on the crate with it. diff --git a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md index 96ef81eb592..2867b974b19 100644 --- a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md +++ b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -1,11 +1,13 @@ --- -status: open +status: assigned kind: finding opened: 2026-09-27 --- # `LoadedLib::write_at`'s "sole writer" `# Safety` is false in the `dlopen` path (M8) +Held by the orchestrator's ELF-loader track; its next brief carries this. + `LoadedLib::write_at`'s `# Safety` says the caller must be the sole writer of the module's image for the call's duration. In `load_shared_lib` that holds: the image is exclusively owned before it is mapped. In `sys_dlopen` it does not. @@ -21,7 +23,6 @@ writes go to the private `rw_alloc`, likewise not yet handed out. But the `# Safety` contract as written is not the one the `dlopen` caller meets, so it cannot be the thing that makes those `unsafe` blocks sound. -This predates the value-checking work in #544 and was noted there rather than -fixed. Exit condition: either the writes move before `map_into`, or the +Exit condition: either the writes move before `map_into`, or the `# Safety` is restated to the invariant the `dlopen` path actually upholds and every call site's `SAFETY:` cites it. diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 20fe2123af2..9be7cfc5912 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -187,6 +187,13 @@ actuators! { /// and `mmap` staged inside the copy. Judged by `user_copy_races_munmap`. copy_meets_a_remap = "copy-meets-a-remap"; + /// Hold a thread spawn whose argument carries `loader::rebase_window`'s + /// mark between its TLS block being given an address and the block's + /// pointers being rebased to it: where the process can already reach the + /// block, until a sibling has stored into its DTV; where it cannot, it + /// says so. Judged by `tls_rebase_window`. + tls_rebase_window = "tls-rebase-window"; + /// Stall the bind of a disk that arrives while another is held for its /// device, for less than `usb-slow-return` does, and leave every transfer /// of the operation the held call sends again on it unanswered, once, each diff --git a/kernel/src/elf/cache.rs b/kernel/src/elf/cache.rs index 261f23b3c46..b7322d5b73b 100644 --- a/kernel/src/elf/cache.rs +++ b/kernel/src/elf/cache.rs @@ -77,8 +77,6 @@ struct Snapshot { dynsym: Option, dynstr: Option, tls_template: Option, - tls_memsz: usize, - tls_align: usize, rela: Option, jmprel: Option, gnu_hash: Option, @@ -97,8 +95,6 @@ impl Snapshot { dynsym: lib.dynsym, dynstr: lib.dynstr, tls_template: lib.tls_template, - tls_memsz: lib.tls_memsz, - tls_align: lib.tls_align, rela: lib.rela, jmprel: lib.jmprel, gnu_hash: lib.gnu_hash, @@ -125,8 +121,6 @@ impl Snapshot { dynsym: self.dynsym, dynstr: self.dynstr, tls_template: self.tls_template, - tls_memsz: self.tls_memsz, - tls_align: self.tls_align, rela: self.rela, jmprel: self.jmprel, gnu_hash: self.gnu_hash, diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 8905859bdc6..f33601f365e 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -28,7 +28,7 @@ use crate::UserAddr; use toyos_elf::dynamic::{Dynamic, InitArray}; use toyos_elf::section::{SectionTable, SHT_DYNSYM}; use toyos_elf::sym::{Sym, SymTab}; -use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError}; +use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError, TlsSegment}; /// `toyos_elf::MAX_TLS_ALIGN` must equal the kernel's largest page. const _: () = assert!(toyos_elf::MAX_TLS_ALIGN == PAGE_2M); @@ -93,8 +93,6 @@ pub struct LoadedLib { dynsym: Option, dynstr: Option, pub tls_template: Option, - pub tls_memsz: usize, - pub tls_align: usize, rela: Option, jmprel: Option, gnu_hash: Option, @@ -237,18 +235,32 @@ impl LoadedLib { self.address_of(&symbols.get(idx)?) } - /// A defined `STT_TLS` symbol of this name, as this module holds it. - pub fn resolve_tls(&self, name: &str) -> Option { - self.symbols().find_tls(name) + /// This module's `PT_TLS`, as `load_shared_lib` parsed it. + pub fn tls(&self) -> Option { + self.rules.tls } - /// Where a symbol of this module lies once mapped: inside the image, which - /// `load_shared_lib` checked every defined one of against its extent. + /// Where a defined, non-TLS symbol of this module lies once mapped; `None` + /// for an undefined or `STT_TLS` one. fn address_of(&self, sym: &Sym) -> Option { - Some(self.user_base + sym.address(self.rules.extent)?.get()) + if !sym.is_defined() || sym.kind() == toyos_elf::sym::STT_TLS { + return None; + } + match sym.address(self.rules.extent) { + Some(at) => Some(self.user_base + at.get()), + None => bounded_symbol_outside(), + } } } +/// A defined symbol outside the extent `load_shared_lib` bounded every one of +/// against: the table moved under the module, which is a kernel bug. +#[cold] +#[inline(never)] +fn bounded_symbol_outside() -> ! { + panic!("ELF: a symbol load_shared_lib bounded inside the image lies outside it") +} + /// A module's tables parsed differently the second time: the bytes moved under /// a module whose tables no writer reaches, which is a kernel bug. #[cold] @@ -425,7 +437,7 @@ pub fn load_shared_lib( let (syms, strs) = unsafe { (dynsym.as_ref().map_or(&[][..], |s| s.as_slice()), dynstr.as_ref().map_or(&[][..], |s| s.as_slice())) }; - SymTab::new(syms, strs).bounded(extent, layout.tls_memsz()).map_err(|e| e.as_str())?; + SymTab::new(syms, strs).bounded(extent, layout.tls()).map_err(|e| e.as_str())?; } let init_array = InitArray::parse(dyn_info.init_array, extent).map_err(|e| e.as_str())?; @@ -459,7 +471,7 @@ pub fn load_shared_lib( sym_count, // A library's image is written contiguously, with no fill-page edge. fill: None, - tls_memsz: layout.tls_memsz(), + tls: layout.tls(), }; // Every entry is parsed here, and a refusal drops the image this pass has // written into: nothing but this function has seen it. @@ -475,14 +487,7 @@ pub fn load_shared_lib( } } - let (tls_template, tls_memsz, tls_align) = match layout.tls() { - Some(tls) => ( - Some(module.at(tls.template())), - tls.memsz() as usize, - tls.align() as usize, - ), - None => (None, 0, 0), - }; + let tls_template = layout.tls().map(|tls| module.at(tls.template())); let t4 = crate::clock::nanos_since_boot(); log!( @@ -506,8 +511,6 @@ pub fn load_shared_lib( dynsym, dynstr, tls_template, - tls_memsz, - tls_align, rela, jmprel, gnu_hash, diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 1bafede0551..5d037186c05 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -13,8 +13,8 @@ use super::{CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; use crate::UserAddr; -use toyos_elf::sym::Sym; -use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef}; +use toyos_elf::sym::{Sym, SymTab}; +use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef, TlsSegment}; impl LoadedLib { /// Write a value at a byte offset within this module's kernel mapping. @@ -52,7 +52,7 @@ impl LoadedLib { /// Every slot of one kind and what it names — from the cache when a clone /// holds one, else scanned off the image, the two never both present. - fn tls_entries<'a, T: Copy + 'a>( + fn entries<'a, T: Copy + 'a>( &'a self, of: fn(Op) -> Option, pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, T)>, @@ -65,11 +65,11 @@ impl LoadedLib { } fn bind_entries(&self) -> impl Iterator + '_ { - self.tls_entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }, |c| &c.bind) + self.entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }, |c| &c.bind) } fn dtpmod_entries(&self) -> impl Iterator)> + '_ { - self.tls_entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }, |c| &c.dtpmod64) + self.entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }, |c| &c.dtpmod64) } /// Every `RELATIVE` slot and the position in the image it points at. @@ -159,22 +159,22 @@ pub fn apply_tpoff_relocs( Op::Tpoff32(t) => Some(t), _ => None, }; - for (_, r) in lib.tls_entries(tpoff64, |c| &c.tpoff64) { + for (_, r) in lib.entries(tpoff64, |c| &c.tpoff64) { compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; } - for (_, r) in lib.tls_entries(tpoff32, |c| &c.tpoff32) { + for (_, r) in lib.entries(tpoff32, |c| &c.tpoff32) { tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; } let mut count64 = 0u64; - for (offset, r) in lib.tls_entries(tpoff64, |c| &c.tpoff64) { + for (offset, r) in lib.entries(tpoff64, |c| &c.tpoff64) { let tpoff = compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, tpoff) }; count64 += 1; } let mut count32 = 0u64; - for (offset, r) in lib.tls_entries(tpoff32, |c| &c.tpoff32) { + for (offset, r) in lib.entries(tpoff32, |c| &c.tpoff32) { let tpoff = tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, tpoff) }; @@ -217,11 +217,11 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< Op::DtpOff64(t) => Some(t), _ => None, }; - for (_, r) in lib.tls_entries(dtpoff, |c| &c.dtpoff64) { + for (_, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { resolve_tls(lib, r, 0, tls_info)?; } let mut count = 0u64; - for (offset, r) in lib.tls_entries(dtpoff, |c| &c.dtpoff64) { + for (offset, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { let value = resolve_tls(lib, r, 0, tls_info)?.map_or(0, |(_, at)| at.get()); // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, value) }; @@ -233,21 +233,21 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< Ok(()) } -/// The module in `tls_info` that defines `name`, and the symbol as it defines -/// it, or `None` if none does. -pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, Sym)> { +/// The module in `tls_info` that defines `name`, its `PT_TLS`, and the symbol +/// as it defines it, or `None` if none does. +pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, TlsSegment, Sym)> { for lib in tls_info.libs { - if lib.tls_memsz == 0 { + let Some(segment) = lib.tls().filter(|t| t.memsz() > 0) else { continue; - } - if let Some(sym) = lib.resolve_tls(name) { + }; + if let Some(sym) = lib.symbols().find_tls(name) { // Template pointer is unique per module: each points into a distinct image. // No matching module here means inconsistent tables; treated as unresolved, not a bug. let module = tls_info .modules .iter() .find(|m| m.template == lib.tls_template)?; - return Some((module, sym)); + return Some((module, segment, sym)); } } None @@ -261,7 +261,7 @@ fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, t } let name = symbols.name(sym.get()); match defining_module(name, tls_info) { - Some((module, _)) => module.module_id, + Some((module, _, _)) => module.module_id, None => { log!("dtpmod: unresolved TLS symbol: {}", name); self_module_id @@ -270,35 +270,34 @@ fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, t } /// `S + A` for one TLS reference, and the static-block offset of the module it -/// lies in: the referencing module's own (`own_base_offset`, `own_memsz`), or +/// lies in: the referencing module's own (`own_base_offset`, `own_tls`), or /// the module defining the symbol. `None` is a symbol no module defines, which /// is logged; a sum outside the defining module's segment refuses the module. /// -/// `lookup` and `strings` are the referencing module's symbol table however it -/// is held — a library's in-image `SymTab`, or the executable's read off the -/// file — so both loaders resolve through this one function. +/// `symbols` is the referencing module's table — a library's in-image one, or +/// the executable's read off the file — so both loaders resolve through this +/// one function. pub fn resolve_tls_ref( r: TlsRef, own_base_offset: usize, - own_memsz: Option, - lookup: impl Fn(SymIndex) -> Option, - strings: &[u8], + own_tls: Option, + symbols: SymTab<'_>, tls_info: &TlsModuleInfo, ) -> Result, RelocError> { let s = match r { TlsRef::Own(at) => return Ok(Some((own_base_offset, at))), TlsRef::Symbol(s) => s, }; - let sym = lookup(s.sym()).ok_or(RelocError::SymbolPastTable)?; + let sym = symbols.get(s.sym().get()).ok_or(RelocError::SymbolPastTable)?; if sym.is_defined() { - let memsz = own_memsz.ok_or(RelocError::TlsOutsideSegment)?; - let at = sym.tls_offset(s.addend(), memsz).ok_or(RelocError::TlsOutsideSegment)?; + let segment = own_tls.ok_or(RelocError::TlsOutsideSegment)?; + let at = sym.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment)?; return Ok(Some((own_base_offset, at))); } - let name = sym.name_in(strings); + let name = sym.name_in(symbols.strings()); match defining_module(name, tls_info) { - Some((module, defined)) => { - let at = defined.tls_offset(s.addend(), module.memsz as u64).ok_or(RelocError::TlsOutsideSegment)?; + Some((module, segment, defined)) => { + let at = defined.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment)?; Ok(Some((module.base_offset, at))) } None => { @@ -315,15 +314,7 @@ fn resolve_tls( own_base_offset: usize, tls_info: &TlsModuleInfo, ) -> Result, RelocError> { - let symbols = lib.symbols(); - resolve_tls_ref( - r, - own_base_offset, - Some(lib.tls_memsz as u64), - |i| symbols.get(i.get()), - symbols.strings(), - tls_info, - ) + resolve_tls_ref(r, own_base_offset, lib.tls(), lib.symbols(), tls_info) } /// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 31a199b8799..39a7157a867 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -18,6 +18,7 @@ pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN}; pub(crate) use start::alloc_kernel_stack; pub(crate) use crate::arch::entry::{kernel_start, process_start, thread_start}; pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; +pub(crate) use tls::rebase_window; use alloc::string::String; use alloc::sync::Arc; @@ -39,7 +40,7 @@ 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::{GnuHash, Layout, RelocError, SymIndex, TlsRef}; +use toyos_elf::{GnuHash, Layout, RelocError, TlsRef}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -186,7 +187,6 @@ struct ExeTables { needed: Vec, dynstr: Vec, dynsym: Vec, - symtab_file_off: Option, relas: elf::ParsedRelaEntries, } @@ -194,17 +194,6 @@ impl ExeTables { fn symbols(&self) -> SymTab<'_> { SymTab::new(&self.dynsym, &self.dynstr) } - - /// One symbol read straight off the file. - /// - /// The index may exceed the `.dynsym` length the loader estimated, so the - /// record is fetched directly rather than the estimate trusted. - fn symbol(&self, backing: &dyn crate::file_backing::FileBacking, r_sym: SymIndex) -> Option { - let off = self - .symtab_file_off? - .checked_add((r_sym.get() as u64).checked_mul(sym::ENTRY_SIZE as u64)?)?; - sym::parse_at(&read_file_range(backing, off, sym::ENTRY_SIZE), 0) - } } /// Turn a `DT_*` vaddr into a file offset, or refuse the binary. @@ -287,7 +276,7 @@ fn read_exe_tables( window: (extent.min(), extent.max()), sym_count, fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), - tls_memsz: layout.tls_memsz(), + tls: layout.tls(), }; let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules).map_err(|refused| { log!("spawn: {}: {}", path, refused.as_str()); @@ -307,7 +296,7 @@ fn read_exe_tables( _ => Vec::new(), }; - Ok(ExeTables { needed, dynstr, dynsym, symtab_file_off, relas }) + Ok(ExeTables { needed, dynstr, dynsym, relas }) } /// `.dynsym`'s entry count, from `.gnu.hash` if present, else the `DT_SYMTAB`–`DT_STRTAB` gap. @@ -479,7 +468,7 @@ pub fn spawn( if r_sym.get() == 0 { continue; } - let Some(sym) = exe.symbol(backing.as_ref(), r_sym) else { + let Some(sym) = exe.symbols().get(r_sym.get()) else { continue; }; let name = sym.name_in(&exe.dynstr); @@ -549,8 +538,8 @@ pub fn spawn( return Err(SyscallError::ResourceExhausted.into()); }; - if let Err(refused) = apply_tls_relocs(&exe, backing.as_ref(), &layout, &loaded_libs.libs, - &tls_modules, tls, &mut reloc_index) + if let Err(refused) = apply_tls_relocs(&exe, &layout, &loaded_libs.libs, &tls_modules, tls, + &mut reloc_index) { log!("spawn: {}: {}", path, refused.as_str()); return Err(SyscallError::InvalidArgument.into()); @@ -801,10 +790,8 @@ fn map_libs( /// /// Libraries' land directly; the executable's go into the relocation index /// because its pages do not exist yet. -#[allow(clippy::too_many_arguments)] fn apply_tls_relocs( exe: &ExeTables, - backing: &dyn crate::file_backing::FileBacking, layout: &Layout, loaded_libs: &[elf::LoadedLib], tls_modules: &[elf::TlsModule], @@ -829,8 +816,12 @@ fn apply_tls_relocs( .iter() .find(|m| m.module_id == 1) .map_or(0, |m| m.base_offset); + // `0` for a symbol no module defines. let exe_tpoff = |r: TlsRef| { - exe_tpoff(exe, backing, layout, r, exe_base_offset, tls, &tls_info) + match elf::resolve_tls_ref(r, exe_base_offset, layout.tls(), exe.symbols(), &tls_info)? { + Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), + None => Ok(0), + } }; for &(r_offset, r) in &exe.relas.tpoff64 { reloc_index.add_u64(r_offset, exe_tpoff(r)? as u64); @@ -841,35 +832,6 @@ fn apply_tls_relocs( Ok(()) } -/// One of the executable's `TPOFF` relocations, resolved to a value: `0` for -/// a symbol no module defines, a refusal for one the file does not hold or -/// whose `S + A` leaves the defining module's segment. The executable's symbols -/// are read off the file, but the resolution is `elf::reloc`'s, shared with a -/// library's. -#[allow(clippy::too_many_arguments)] -fn exe_tpoff( - exe: &ExeTables, - backing: &dyn crate::file_backing::FileBacking, - layout: &Layout, - r: TlsRef, - exe_base_offset: usize, - tls: toyos_elf::tls::Static, - tls_info: &elf::TlsModuleInfo, -) -> Result { - let resolved = elf::resolve_tls_ref( - r, - exe_base_offset, - layout.tls_memsz(), - |i| exe.symbol(backing, i), - &exe.dynstr, - tls_info, - )?; - match resolved { - Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), - None => Ok(0), - } -} - /// 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 INIT_PATH: &str = "/system/bin/init"; diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index 9f54c44a004..a4d09f0f0c5 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -50,6 +50,9 @@ impl TlsBlock { pub fn publish(self, pt: &PageTables) -> Option<(MappedPages, u64, usize)> { let tp_offset = self.tp_offset; let pages = self.frames.publish(pt, crate::mm::paging::Prot::ReadWrite, |frames, at| { + if crate::actuator::tls_rebase_window() { + rebase_window::hold(frames, at); + } // SAFETY: `frames` is the block `build_combined` wrote, reachable // by no mapping until `publish` maps it after this returns. unsafe { rebase(frames, tp_offset, at) } @@ -159,6 +162,66 @@ unsafe fn rebase(frames: &Unpublished, tp_offset: usize, at: UserAddr) { } } +/// `tls-rebase-window`: a sibling's store staged between a block being given +/// an address and its pointers being rebased to it. Only a spawn whose +/// argument is [`MARK`](rebase_window::MARK) is watched, so the test program +/// chooses the spawns it races. +pub(crate) mod rebase_window { + use core::sync::atomic::{AtomicU64, Ordering}; + + use crate::process::Unpublished; + use crate::time::Duration; + use crate::UserAddr; + + /// The thread argument that asks for a watched spawn. + const MARK: u64 = 0x5eed_c0de_71b0_0001; + /// How long a reachable block waits for a sibling's store before it says + /// the test staged nothing. + const BOUND: Duration = Duration::from_secs(10); + + /// The pid a watched spawn is in flight for, plus one; zero while none is. + static WATCHED: AtomicU64 = AtomicU64::new(0); + + /// `spawn_thread` is about to publish a block for a thread given `arg`. + pub(crate) fn spawning(arg: u64) { + if arg == MARK { + WATCHED.store(crate::process::current_process().0 as u64 + 1, Ordering::SeqCst); + } + } + + pub(super) fn hold(frames: &Unpublished, at: UserAddr) { + // `None` while the kernel spawns init, with no thread running. + let Some(pid) = crate::arch::percpu::current_pid() else { return }; + let pid = pid.0 as u64 + 1; + if WATCHED.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_err() { + return; + } + // SAFETY: DTV slot 0 is inside the DTV `build_combined` wrote at the front of `frames`. + let slot = unsafe { frames.ptr().add(super::DTV_HEADER_SIZE) }.cast::(); + // SAFETY: as above; a volatile read, since a user store may land there. + let written = unsafe { slot.read_volatile() }; + // `spawn_thread` publishes into the address space this CPU runs. + if !crate::mm::paging::present_in_current_tables(at.raw()) { + log!("tls-rebase-window: pid {} block at {:#x} is not reachable before its rebase", pid - 1, at.raw()); + return; + } + let deadline = crate::clock::now() + BOUND; + // SAFETY: as above. + while unsafe { slot.read_volatile() } == written { + assert!( + crate::clock::now() < deadline, + "tls-rebase-window: pid {} block at {:#x} was reachable before its rebase and nothing stored into it in {BOUND}", + pid - 1, + at.raw() + ); + // `IF` is clear in a syscall: a sibling's shootdown is answered here. + crate::arch::tlb::poll(); + core::hint::spin_loop(); + } + log!("tls-rebase-window: pid {} block at {:#x} was reachable before its rebase, and a store landed in it", pid - 1, at.raw()); + } +} + /// 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. @@ -182,12 +245,14 @@ pub fn build_tls_layout( Some((exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)), memsz, placed, align, 1)) } }; - let libs = loaded_libs - .iter() - .filter(|lib| lib.tls_memsz > 0) - .zip(2u64..) - .map(|(lib, id)| (lib.tls_template, lib.tls_memsz, lib.tls_memsz, lib.tls_align, id)); - let next_module_id = 2 + loaded_libs.iter().filter(|lib| lib.tls_memsz > 0).count() as u64; + let with_tls = || { + loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, lib.tls().filter(|t| t.memsz() > 0)?))) + }; + 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(), diff --git a/kernel/src/process.rs b/kernel/src/process.rs index f63bb015f34..b89fa88ae70 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -880,6 +880,9 @@ pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Op let block = TlsBlock::build(&tls_modules, tls)?; let (tls_alloc, fs_base, tp_offset) = { let parent_data = process_data_arc.lock(); + if crate::actuator::tls_rebase_window() { + crate::loader::rebase_window::spawning(arg); + } // VA exhaustion is a resource failure the process caused, not a kernel bug; the block drops on the way out, returning its pages. let published = block.publish(&parent_addr_space)?; drop(parent_data); diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index 2043999b812..d9e78e7bfd2 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -286,7 +286,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); }); - let lib_has_tls = lib.tls_memsz > 0; + let lib_tls = lib.tls().filter(|t| t.memsz() > 0); let data_arc = process::process_data(); let init_info = { let data = data_arc.lock(); @@ -305,7 +305,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init None }; let refused = refused.or_else(|| { - lib_has_tls.then(|| crate::elf::apply_dtpoff_relocs(&lib, &tls_info).err()).flatten() + lib_tls.and_then(|_| crate::elf::apply_dtpoff_relocs(&lib, &tls_info).err()) }); if let Some(refused) = refused { log!("dlopen: {}: {}", resolved, refused.as_str()); @@ -346,7 +346,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init // Taken and bumped under the guard that registers, so two names loading at // once are two modules: the id a library's `DTPMOD64` relocations carry is // no other library's, and `dynamic_tls_blocks` is keyed on it. - if lib_has_tls { + if let Some(lib_tls) = lib_tls { let module_id = data.elf.next_tls_module_id; data.elf.next_tls_module_id = module_id + 1; let tls_info = crate::elf::TlsModuleInfo { @@ -356,7 +356,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init crate::elf::apply_dtpmod_relocs(&lib, module_id, &tls_info); data.elf.tls_modules.push(crate::elf::TlsModule { template: lib.tls_template, - memsz: lib.tls_memsz, + memsz: lib_tls.memsz() as usize, base_offset: 0, module_id, is_static: false, 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 9f138d74963..86616a72756 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -801,8 +801,8 @@ fn f13_cross_module_addend_is_kept() { F13_KEEP_TLS.with(|c| c.set(c.get())); // `defs` loads first so it resolves `refs`'s TPOFF; both held to the read. - let defs = write_file("f13_defs.so", &tls_defs_so()); - let refs = write_file("f13_refs.so", &tls_refs_so(ADDEND)); + let defs = write_file("f13_defs.so", &tls_defs_so(b"xtls", 0x200)); + let refs = write_file("f13_refs.so", &tls_refs_so(b"xtls", ADDEND)); let lib_defs = unsafe { libloading::Library::new(&defs) }.expect("dlopen f13_defs.so"); let lib_refs = unsafe { libloading::Library::new(&refs) }.expect("dlopen f13_refs.so"); @@ -823,36 +823,41 @@ fn f13_cross_module_addend_is_kept() { drop(lib_defs); } -/// Defines `xtls` (`STT_TLS`, offset 8) for a cross-module `TPOFF64`, with a -/// 0x200-byte `PT_TLS` its addends stay inside. -fn tls_defs_so() -> Vec { +/// Defines `name` (`STT_TLS`, offset 8) for a cross-module `TPOFF64`, in a +/// `memsz`-byte `PT_TLS`. +fn tls_defs_so(name: &[u8; 4], memsz: u64) -> Vec { Elf::new(0x2000) .ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_X)) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz: 0x200, align: 8 }) + .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x1800, vaddr: 0x1800, filesz: 0, memsz, align: 8 }) .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) .sections(0x1C00, 1, 64) .dynamic(0x1000, &[(DT_SYMTAB, 0x1200), (DT_STRTAB, 0x1400), (DT_STRSZ, 0x40)]) - // sym[1] xtls: defined (st_shndx == 1), STT_TLS, offset 8 in the block. + // sym[1] `name`: defined (st_shndx == 1), STT_TLS, offset 8 in the block. .sym(0x1218, 1, (STB_GLOBAL << 4) | STT_TLS, 1, 8) - .poke(0x1401, b"xtls\0") + .poke(0x1401, name) .shdr(0x1C00, SHT_DYNSYM, 0x1200, 48, 24) .build() } -/// The two apply-time TLS refusals — a `dlopen`ed library's and an -/// executable's — are new code no other case here reaches, because every other -/// TLS case uses `r_sym == 0`, which `rela::parse` refuses before either apply -/// pass runs. Both refuse a *resolved* `S + A` outside the defining module's -/// `PT_TLS`, named `TLS_OUTSIDE_SEGMENT` in the kernel log the harness checks. -/// Each references its *own* defined `STT_TLS` symbol, so the module resolved -/// against is unambiguously this fixture's and not some other loaded module's. +/// 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. fn tls_apply_time_refusals_are_reached() { - // dlopen side: `apply_tpoff_relocs` refuses, and the mapping guard takes the - // library back down. + // 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 side: `apply_tls_relocs` refuses during `spawn`. + // spawn, the executable's own symbol: `apply_tls_relocs` refuses. spawn_refused("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 + // loaded (`dlclose` unloads nothing) and would be the module resolved. + let defs = write_file("f13_defs_small.so", &tls_defs_so(b"ytls", 0x20)); + let lib_defs = unsafe { libloading::Library::new(&defs) }.expect("dlopen f13_defs_small.so"); + dlopen_refused("f13_refs_past.so", &tls_refs_so(b"ytls", 0x140)); + drop(lib_defs); } /// A shared object defining its own `xtls` (`STT_TLS`, offset 8) in a 0x20-byte @@ -895,9 +900,9 @@ fn exe_tls_ref_past_segment() -> Vec { .build() } -/// Two `TPOFF64` relocations against the undefined `xtls`, addends 0 and +/// Two `TPOFF64` relocations against the undefined `name`, addends 0 and /// `addend`, patching exported data `probe0`/`probeN` a reader can difference. -fn tls_refs_so(addend: i64) -> Vec { +fn tls_refs_so(name: &[u8; 4], addend: i64) -> Vec { Elf::new(0x4000) .ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_X)) .ph(Phdr::load(0x2000, 0x2000, 0x2000, 0x2000, PF_R | PF_W)) @@ -907,11 +912,12 @@ fn tls_refs_so(addend: i64) -> Vec { (DT_SYMTAB, 0x1200), (DT_STRTAB, 0x1400), (DT_STRSZ, 0x40), (DT_RELA, 0x1600), (DT_RELASZ, 48), ]) - // sym[1] xtls undefined (shndx 0) → cross-module; sym[2]/[3] the probes. + // sym[1] `name` undefined (shndx 0) → cross-module; sym[2]/[3] the probes. .sym(0x1218, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) .sym(0x1230, 6, STB_GLOBAL << 4, 2, 0x2000) .sym(0x1248, 13, STB_GLOBAL << 4, 2, 0x2008) - .poke(0x1401, b"xtls\0probe0\0probeN\0") + .poke(0x1401, name) + .poke(0x1406, b"probe0\0probeN\0") .rela(0x1600, 0x2000, (1u64 << 32) | R_X86_64_TPOFF64, 0) .rela(0x1618, 0x2008, (1u64 << 32) | R_X86_64_TPOFF64, addend) .shdr(0x1C00, SHT_DYNSYM, 0x1200, 96, 24) diff --git a/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs index d8b8d58218e..ff40923f351 100644 --- a/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs +++ b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs @@ -1,39 +1,39 @@ -//! C1: a sibling thread must not be able to corrupt the next thread's TLS -//! block between the kernel mapping it and the kernel finishing its rebase. +//! C1: no thread of a process may reach a new thread's TLS block before the +//! kernel has rebased the block's pointers to the address it maps it at. //! -//! At head the kernel rebases the block *before* it maps it, so a sibling never -//! sees a pointer the kernel has yet to fix. With the review's mutation (`fix` -//! moved after `map_range` and `drop(space)`), the block is visible while its -//! DTV still holds physical addresses, and a sibling that stores 0 into a DTV -//! slot makes the rebase's `entry - phys` underflow — a kernel panic reached -//! from userland, which is what this test denies. +//! One worker at a time runs on one reused stack, so the only arena allocation +//! that churns is the worker's TLS block, placed at one address round after +//! round. A sibling probes that address with `random` — `BadAddress` while it +//! is unmapped — and once it is mapped stores 0 into its DTV slot 0, which a +//! rebase still to come turns into a `p - phys` underflow: a kernel panic +//! reached from userland. //! -//! The race is arranged so it needs no luck to be *safe*: every store the -//! sibling makes is gated by a `random` probe that answers `BadAddress` while -//! the address is unmapped, and the block it targets is either a live worker's -//! (mapped for that worker's whole life) or, during the window under test, one -//! the kernel is mapping. The worker's block is reused at one virtual address -//! round after round — one worker at a time on a fixed stack, so the only -//! churning arena allocation is the kernel's TLS block — so the sibling can -//! hammer that address in advance of each spawn. -//! -//! Kernel liveness is the verdict: under the mutation the machine panics and -//! the guest dies; at head every round completes, every worker finds its own -//! TP and DTV self-consistent, and the heap still walks. +//! Every spawn after the first, which places the block, carries +//! `kernel/src/loader/tls.rs`'s `rebase_window::MARK`. A kernel armed with +//! `tls-rebase-window` holds such a spawn before its rebase until the sibling's +//! store lands if the block is already reachable, and says it is not if it is +//! not; `tls_rebase_window` runs this there and reads which. Unarmed, the +//! window is the scheduler's to hit. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}; +use std::time::{Duration, Instant}; use toyos_abi::syscall; +/// `kernel/src/loader/tls.rs`'s `rebase_window::MARK`. +const MARK: u64 = 0x5eed_c0de_71b0_0001; /// 2 MiB, the TLS block's alignment and (for this process's small TLS) its size. const BLOCK: u64 = 2 * 1024 * 1024; -const MASK: u64 = BLOCK - 1; /// DTV slot 0 (module 1) sits two words into the block: a generation word, a /// length word, then the entries. `kernel/src/loader/tls.rs` owns this layout. const DTV_SLOT0: u64 = 16; +/// Spawn/retire rounds; every one after the first is watched. +const ROUNDS: u64 = 16; +/// A liveness bound on another thread's store, never a pace. +const BOUND: Duration = Duration::from_secs(10); -/// The block base the sibling hammers, or 0 while it must stand down. +/// The block base the sibling stores into. static TARGET: AtomicU64 = AtomicU64::new(0); -/// The current worker has read its TP and published `TARGET`. +/// The current worker has published `TARGET`. static READY: AtomicBool = AtomicBool::new(false); /// The current worker may exit. static GO_EXIT: AtomicBool = AtomicBool::new(false); @@ -44,50 +44,37 @@ static PAUSE: AtomicBool = AtomicBool::new(true); static PAUSED_ACK: AtomicBool = AtomicBool::new(false); /// The sibling must end. static STOP: AtomicBool = AtomicBool::new(false); -/// A worker found its TP and DTV inconsistent — a corruption the kernel let -/// through rather than panicking on. -static INCONSISTENT: AtomicBool = AtomicBool::new(false); -/// How many times the sibling found the block mapped and hammered it — proof it -/// engaged a real reused block rather than spinning on an unmapped address. +/// Rounds in which the sibling found the block mapped and stored into it. static ENGAGED: AtomicU64 = AtomicU64::new(0); -/// The self-pointer the psABI puts at the thread pointer (`%fs:0` on variant -/// II); the block base is that rounded down to the allocation's alignment. -#[cfg(target_arch = "x86_64")] -fn fs_base() -> u64 { - let tp: u64; - // SAFETY: reads the thread-pointer self-word the kernel wrote at TP+0. - unsafe { core::arch::asm!("mov {}, fs:0", out(reg) tp, options(nostack, readonly)) }; - tp +thread_local! { + /// A datum in this program's static TLS, which lies in the thread's block. + static ANCHOR: u8 = const { 0 }; } -/// One worker: publish its block, prove its own TP/DTV consistent, and wait to -/// be retired. Runs on a fixed stack; only one worker exists at a time. -extern "C" fn worker(_arg: u64) { - let tp = fs_base(); - let v = tp & !MASK; - // TP+8 is the DTV pointer, which the kernel rebased to the block base. A - // block the kernel published half-rebased would fail this. - // SAFETY: TP+8 is inside this thread's own TCB. - let dtv = unsafe { ((tp + 8) as *const u64).read_volatile() }; - if dtv != v { - INCONSISTENT.store(true, SeqCst); - } - TARGET.store(v, SeqCst); - READY.store(true, SeqCst); - while !GO_EXIT.load(SeqCst) { +/// Spin until `done`, or panic naming `what` past [`BOUND`]. +fn until(what: &str, done: impl Fn() -> bool) { + let deadline = Instant::now() + BOUND; + while !done() { + assert!(Instant::now() < deadline, "tls_dtv_race: {what} did not come within {BOUND:?}"); core::hint::spin_loop(); } +} + +/// One worker: publish its block's base and wait to be retired. Runs on a +/// fixed stack; only one worker exists at a time. +extern "C" fn worker(_arg: u64) { + let block = ANCHOR.with(|anchor| anchor as *const u8 as u64) & !(BLOCK - 1); + TARGET.store(block, SeqCst); + READY.store(true, SeqCst); + until("leave to exit", || GO_EXIT.load(SeqCst)); syscall::thread_exit(0); } -/// Whether `addr` is mapped, by the `random` syscall — it answers `BadAddress` +/// Whether `v` is mapped, by the `random` syscall — it answers `BadAddress` /// while the page is unmapped and writes into it otherwise. The probe writes one /// byte at the block's generation word (harmless). fn mapped(v: u64) -> bool { - // A `&mut [u8]` over memory this thread does not own is the price of using - // the mandated mapped-ness probe; nothing here reads it, and the syscall - // checks the mapping before any access. // SAFETY: on `BadAddress` nothing is touched; the caller stores only after // this returns true and only while the block stays mapped (see `sibling`). let probe = unsafe { core::slice::from_raw_parts_mut(v as *mut u8, 1) }; @@ -97,18 +84,14 @@ fn mapped(v: u64) -> bool { fn sibling() { while !STOP.load(SeqCst) { if PAUSE.load(SeqCst) { - // Idle and past any store: the main thread waits for this before it - // frees the block, so no store is ever in flight against a free. PAUSED_ACK.store(true, SeqCst); core::hint::spin_loop(); continue; } PAUSED_ACK.store(false, SeqCst); let v = TARGET.load(SeqCst); - // Wait for the block to be mapped (a spawn is placing it), then hammer 0 - // into DTV slot 0 to arm the rebase underflow, in a tight loop with no - // syscall so the store lands inside the map-to-rebase window. `PAUSE` - // ends the loop before the main thread frees the block. + // Wait for the block to be mapped (a spawn is placing it), then store 0 + // into DTV slot 0 in a tight loop with no syscall, until `PAUSE`. if v != 0 && mapped(v) { ENGAGED.fetch_add(1, SeqCst); while !PAUSE.load(SeqCst) && !STOP.load(SeqCst) { @@ -120,78 +103,37 @@ fn sibling() { } } -/// How many spawn/retire rounds to run; each is one race attempt, the sibling -/// hammering the block's DTV slot 0 through the whole map-to-rebase window, and -/// the block reused at one address across all of them. Bounded so the run -/// finishes on the dev host's TCG, where two busy vCPUs and a kernel log per -/// spawn make each round dear; under the mutation the panic comes in the first -/// rounds. -const ROUNDS: u64 = 800; - fn main() { - if cfg!(not(target_arch = "x86_64")) { - println!("tls_dtv_race: only x86_64 reads %fs:0; skipping"); - return; - } - let sib = std::thread::Builder::new() .name("dtv-sibling".into()) .spawn(sibling) .expect("spawn sibling"); - // One reused stack: with a single worker alive at a time, the only arena - // allocation that churns is the kernel's per-thread TLS block, so it is - // reused at one virtual address — the one the sibling hammers. const STACK: usize = 256 * 1024; let stack = vec![0u8; STACK].leak(); let base = stack.as_ptr() as u64; let top = (base + STACK as u64) & !15; + let entry = (worker as *const ()).expose_provenance() as u64; - let mut rounds = 0u64; - for _ in 0..ROUNDS { + for round in 0..ROUNDS { READY.store(false, SeqCst); GO_EXIT.store(false, SeqCst); - // Let the sibling hammer the address a retiring block last held; the - // fresh spawn maps that same address, and the window under test is - // between that map and the rebase. PAUSE.store(false, SeqCst); + let arg = if round == 0 { 0 } else { MARK }; // SAFETY: `worker` is a valid entry; `top`/`base` describe the leaked stack. - let entry = (worker as *const ()).expose_provenance() as u64; - let tid = unsafe { syscall::thread_spawn(entry, top, 0, base) }; + let tid = unsafe { syscall::thread_spawn(entry, top, arg, base) }; assert!(syscall::SyscallError::from_u64(tid).is_none(), "thread_spawn failed: {tid}"); - while !READY.load(SeqCst) { - core::hint::spin_loop(); - } - // Retire the worker with the sibling stood down, so no store can land in - // a block the kernel is freeing. Wait for the sibling to acknowledge it - // is idle — a store already in flight completes while the block is still - // mapped, before the join frees it. + until("the worker's start", || READY.load(SeqCst)); + until("the sibling's store into the block", || ENGAGED.load(SeqCst) > round); + // Stood down and acknowledged before the join frees the block, so no + // store is in flight against a free. PAUSE.store(true, SeqCst); - while !PAUSED_ACK.load(SeqCst) { - core::hint::spin_loop(); - } + until("the sibling standing down", || PAUSED_ACK.load(SeqCst)); GO_EXIT.store(true, SeqCst); - syscall::thread_join(tid); - rounds += 1; - - if rounds % 4096 == 0 { - // The heap still walks: allocate, touch, free. - let probe = vec![rounds as u8; 4096]; - assert_eq!(probe[0], rounds as u8); - } + assert_eq!(syscall::thread_join(tid), 0, "round {round}: join"); } STOP.store(true, SeqCst); sib.join().expect("join sibling"); - - assert!(!INCONSISTENT.load(SeqCst), "a worker's TP and DTV disagreed — the block was corrupted"); - - // The kernel is still alive: a plain thread spawns, runs and joins. - let n = std::thread::spawn(|| 0xC1u64).join().expect("final thread"); - assert_eq!(n, 0xC1); - - println!( - "tls_dtv_race: {rounds} rounds, {} engaged, kernel alive, TP/DTV consistent", - ENGAGED.load(SeqCst) - ); + println!("tls_dtv_race: {ROUNDS} rounds, {} watched, the sibling stored into every one", ROUNDS - 1); } diff --git a/tests/toyos.rs b/tests/toyos.rs index 753bcdca82d..b66149bdeb5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1535,6 +1535,9 @@ const MACHINE_TESTS: &[(&str, Sched, Tier)] = &[ // and its store (`copy-meets-a-remap`): the store never reaches the region // mapped after it. ("user_copy_races_munmap", Sched::Parallel, Tier::Fast), + // A sibling's store staged between a thread's TLS block being placed and + // its rebase (`tls-rebase-window`): the block is never reachable there. + ("tls_rebase_window", Sched::Parallel, Tier::Fast), ("writeback_reopen", Sched::Parallel, Tier::Fast), ("writeback_spawn", Sched::Parallel, Tier::Nightly), ("writeback_durability", Sched::Parallel, Tier::Nightly), @@ -1652,6 +1655,7 @@ const CARRIES: &[(&str, &[&str])] = &[ ("update_refused_pass_credits_no_image", &[]), ("blocking_read_window", &["test_rs_blocking_read_stress"]), ("user_copy_races_munmap", &["test_rs_copy_out_races_munmap"]), + ("tls_rebase_window", &["test_rs_tls_dtv_race"]), ("writeback_reopen", &["test_rs_writeback_reopen"]), ("writeback_spawn", &["test_rs_writeback_spawn"]), ("xhci_second_controller", &["test_rs_input_events"]), @@ -3500,14 +3504,12 @@ fn check_for(name: &str) -> fn(&TestResult) -> bool { } /// The reason the loader logs when a resolved TLS `S + A` leaves the defining -/// module's `PT_TLS` — `RelocError::TlsOutsideSegment`, kept in step with -/// `toyos-elf/src/rela.rs`. -const TLS_OUTSIDE_SEGMENT: &str = "ELF: TLS relocation names an offset outside its PT_TLS"; +/// module's `PT_TLS`. +const TLS_OUTSIDE_SEGMENT: &str = toyos_elf::RelocError::TlsOutsideSegment.as_str(); /// `abuse_elf_loader` plus the reason each apply-time TLS refusal must fire for. /// -/// The two cases (`tls_apply_refs.so` on `dlopen`, `tls_apply_spawn` on -/// `spawn`) are refused for the right reason only if the kernel names +/// Each case is refused for the right reason only if the kernel names /// [`TLS_OUTSIDE_SEGMENT`] beside the file — a case refused later, for another /// reason, would pass the exit-code check alone. fn check_abuse_elf_loader(result: &TestResult) -> bool { @@ -3519,6 +3521,7 @@ fn check_abuse_elf_loader(result: &TestResult) -> bool { for (file, what) in [ ("tls_apply_refs.so", "the dlopen apply-time TLS refusal"), ("tls_apply_spawn", "the spawn apply-time TLS refusal"), + ("f13_refs_past.so", "the cross-module apply-time TLS refusal"), ] { let named = log .lines() @@ -3535,6 +3538,12 @@ fn check_abuse_elf_loader(result: &TestResult) -> bool { ok } +/// `kernel/src/loader/tls.rs`'s `rebase_window` line for a watched spawn's +/// block the process could not yet reach before its rebase. +const TLS_BLOCK_UNREACHABLE: &str = "is not reachable before its rebase"; +/// The spawns `tls_dtv_race` watches: every round of its `ROUNDS` but the first. +const TLS_RACE_WATCHED: usize = 15; + /// What a loader writes when it caches a library under the directory it searched /// and did not find it in. Only `dlopen_dedup`'s last arm produces this string. const FALLBACK_MISCACHED: &str = "dlopen: cached /tmp/dlopen-dedup/libtls_lib.so"; @@ -11646,6 +11655,28 @@ fn run_machine_test( } Ok(()) } + // Two CPUs: the held spawn spins in the kernel while its sibling stores + // on the other. + "tls_rebase_window" => { + let options = BootOptions { + smp: 2, + kernel_params: &["tls-rebase-window"], + ..Default::default() + }; + let mut qemu = + QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); + let result = qemu.run_test("test_rs_tls_dtv_race", Duration::from_secs(30)); + let log = format!("{}{}", result.before, result.serial); + let unreachable = log.lines().filter(|l| l.contains(TLS_BLOCK_UNREACHABLE)).count(); + if !check_rust_result(&result) || unreachable != TLS_RACE_WATCHED { + return Err(format!( + "tls_rebase_window failed: {unreachable} of {TLS_RACE_WATCHED} watched blocks \ + unreachable before their rebase:\n{}\nkernel log while it ran:\n{log}", + result.stdout + )); + } + Ok(()) + } // The write-back queue's re-open control: `writeback-stall` parks `iod` // before it drains, so the guest can prove a re-open before the flush // reads the pinned pages and not the NVMe `/home` device. @@ -18515,9 +18546,6 @@ fn build_test_registry( // the slow one: it spends its own patience before reporting, and // the report is worth more than the harness's timeout message. "inbox_cancel_wakes" => Duration::from_secs(30), - // Twenty thousand spawn/retire rounds, each a race attempt against - // the TLS-block rebase. - "tls_dtv_race" => Duration::from_secs(60), _ => Duration::from_secs(5), }; tests.push(TestDef { diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index 846009155c4..2e3436b3300 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -189,7 +189,7 @@ impl Segment { /// file-backed template lies inside the image. Absent TLS is `None`, never a /// zero `memsz`: a module with a `PT_TLS` of zero size still gets a DTV slot, /// and the two cases are not the same question. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TlsSegment { template: ImageRange, memsz: u64, @@ -217,8 +217,8 @@ impl TlsSegment { /// `PT_DYNAMIC`, where the file holds it and where the image does. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DynamicSegment { - pub(crate) file_offset: u64, - pub(crate) image: ImageRange, + file_offset: u64, + image: ImageRange, } impl DynamicSegment { @@ -443,11 +443,6 @@ impl Layout { self.tls } - /// The `PT_TLS` `p_memsz`, when the image has a TLS segment. - pub fn tls_memsz(&self) -> Option { - self.tls.map(|t| t.memsz) - } - pub const fn dynamic(&self) -> Option { self.dynamic } diff --git a/toyos-elf/src/rela.rs b/toyos-elf/src/rela.rs index e5676f3f594..6dfe5508581 100644 --- a/toyos-elf/src/rela.rs +++ b/toyos-elf/src/rela.rs @@ -14,7 +14,7 @@ //! no writer handles would be checked for a write that never happens. use crate::header::Machine; -use crate::layout::{Extent, ImageOffset}; +use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; use crate::sym::SymIndex; use crate::tls::TlsOffset; @@ -152,9 +152,9 @@ pub struct Rules { /// `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` `p_memsz`, which a TLS relocation with - /// `r_sym == 0` offsets into; `None` for a module with no TLS segment. - pub tls_memsz: 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, } /// A relocation whose destination lies in its window and whose value names @@ -261,8 +261,8 @@ pub fn parse(rela: Rela, rules: &Rules) -> Result, RelocError> { return Ok(TlsRef::Symbol(TlsSymRef { sym: sym()?, addend: rela.addend })); } rules - .tls_memsz - .and_then(|memsz| TlsOffset::of(0, rela.addend, memsz)) + .tls + .and_then(|segment| TlsOffset::of(0, rela.addend, segment)) .map(TlsRef::Own) .ok_or(RelocError::TlsOutsideSegment) }; diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index 82b5b4755bb..ede60cbfeb6 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -5,7 +5,7 @@ //! [`ImageOffset`] inside its module ([`Sym::address`]) or a [`TlsOffset`] //! inside its module's TLS segment ([`Sym::tls_offset`]). -use crate::layout::{Extent, ImageOffset}; +use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; use crate::tls::TlsOffset; use crate::Error; @@ -80,11 +80,10 @@ impl Sym { extent.offset(self.value) } - /// `S + A` for a symbol read as an offset into a TLS segment of `memsz` - /// bytes — psABI `st_value` for `STT_TLS` — or `None` when the sum leaves - /// the segment. - pub fn tls_offset(&self, addend: i64, memsz: u64) -> Option { - TlsOffset::of(self.value, addend, memsz) + /// `S + A` for a symbol read as an offset into `segment` — psABI + /// `st_value` for `STT_TLS` — or `None` when the sum leaves it. + pub fn tls_offset(&self, addend: i64, segment: TlsSegment) -> Option { + TlsOffset::of(self.value, addend, segment) } } @@ -160,16 +159,15 @@ impl<'a> SymTab<'a> { /// Refuse a table any of whose defined symbols names nothing in its /// module: a value outside the image `extent`, or a `STT_TLS` one outside - /// a TLS segment of `tls_memsz` bytes (or in a module with none). + /// the module's `tls` segment (or in a module with none). /// /// A loader that has asked this once answers every later lookup through /// [`Sym::address`] and [`Sym::tls_offset`] from the same bytes, so none of /// those can come back empty for a symbol the table defines. - pub fn bounded(&self, extent: Extent, tls_memsz: Option) -> Result<(), Error> { + pub fn bounded(&self, extent: Extent, tls: Option) -> Result<(), Error> { for (_, sym) in self.defined() { if sym.kind() == STT_TLS { - tls_memsz - .and_then(|memsz| sym.tls_offset(0, memsz)) + tls.and_then(|segment| sym.tls_offset(0, segment)) .ok_or(Error::TlsSymbolOutsideSegment)?; } else { sym.address(extent).ok_or(Error::SymbolOutsideImage)?; diff --git a/toyos-elf/src/tls.rs b/toyos-elf/src/tls.rs index f1f5782b204..46e049edac7 100644 --- a/toyos-elf/src/tls.rs +++ b/toyos-elf/src/tls.rs @@ -21,6 +21,7 @@ //! an assertion in the kernel reached from a crafted `PT_TLS`. use crate::header::Machine; +use crate::layout::TlsSegment; /// Which of the psABIs' two TLS layouts a machine uses. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -148,19 +149,17 @@ impl Static { } /// `S + A` inside one module's TLS segment, `0..=p_memsz`: made only by -/// [`TlsOffset::of`]. +/// [`TlsOffset::of`] against a [`TlsSegment`] a parse derived. /// /// The end is inclusive for the reason [`crate::Extent::offset`]'s is. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TlsOffset(u64); impl TlsOffset { - /// `value + addend`, when it lies inside a segment of `memsz` bytes. - /// Crate-private: a TLS offset is a symbol's, reached through - /// [`crate::Sym::tls_offset`], never a bound a caller hands in. - pub(crate) const fn of(value: u64, addend: i64, memsz: u64) -> Option { + /// `value + addend`, when it lies inside `segment`. + pub(crate) const fn of(value: u64, addend: i64, segment: TlsSegment) -> Option { match value.checked_add_signed(addend) { - Some(at) if at <= memsz => Some(TlsOffset(at)), + Some(at) if at <= segment.memsz() => Some(TlsOffset(at)), _ => None, } } diff --git a/toyos-elf/tests/common/mod.rs b/toyos-elf/tests/common/mod.rs index a3a01a688e7..7013e085802 100644 --- a/toyos-elf/tests/common/mod.rs +++ b/toyos-elf/tests/common/mod.rs @@ -197,6 +197,17 @@ pub fn extent(min: u64, max: u64) -> toyos_elf::Extent { .extent() } +/// The `PT_TLS` `Layout::parse` derives for a `memsz`-byte segment: the only +/// way a test names a [`toyos_elf::TlsSegment`], and so the only bound a +/// [`toyos_elf::TlsOffset`] is had against. +pub fn tls_segment(memsz: u64) -> toyos_elf::TlsSegment { + let bytes = Elf::honest(0x200).ph(Phdr::tls(0x100, 0, memsz, 8)).build(); + toyos_elf::Layout::parse(&bytes, toyos_elf::Machine::X86_64) + .unwrap() + .tls() + .unwrap() +} + /// One `Elf64_Rela`, as bytes. pub fn rela(r_offset: u64, r_sym: u32, r_type: u32, r_addend: i64) -> [u8; 24] { let mut out = [0u8; 24]; diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index 4ff15971bef..499a9262900 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -326,15 +326,16 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), let sym_range = table(dynamic.symtab, MAX_SYMS * sym::ENTRY_SIZE as u64)?; let str_range = table(dynamic.strtab, 0x100)?; let symbols = SymTab::new(at(&case.bytes, sym_range), at(&case.bytes, str_range)); - let tls_memsz = layout.tls_memsz(); - symbols.bounded(extent, tls_memsz).map_err(|_| ())?; + let tls = layout.tls(); + symbols.bounded(extent, tls).map_err(|_| ())?; for (_, s) in symbols.defined() { if let Some(off) = s.address(extent) { place(off.get()); reached.symbols += 1; } else { - let off = s.tls_offset(0, tls_memsz.unwrap()).expect("a TLS symbol `bounded` accepted"); - assert!(off.get() <= tls_memsz.unwrap()); + let segment = tls.expect("a TLS symbol `bounded` accepted has a segment"); + let off = s.tls_offset(0, segment).expect("a TLS symbol `bounded` accepted"); + assert!(off.get() <= segment.memsz()); } } @@ -363,7 +364,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), window, sym_count: symbols.count(), fill: (case.mode == Mode::Exe).then_some(FillLattice { base: extent.min(), granule: 4096 }), - tls_memsz, + tls, }; let mut relocs = Vec::new(); for raw in RelaTable::new(rela_bytes, case.machine).iter() { @@ -375,7 +376,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), // The thread's static block: this module alone, placed as the kernel's // `build_tls_layout` places an executable's. let variant = Variant::of(case.machine); - let (block, base_offset, memsz) = match layout.tls().filter(|t| t.memsz() > 0) { + let (block, base_offset, memsz) = match tls.filter(|t| t.memsz() > 0) { Some(t) => { let memsz = usize::try_from(t.memsz()).map_err(|_| ())?; let align = usize::try_from(t.align()).map_err(|_| ())?; @@ -411,7 +412,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), // another module's, which the kernel resolves by name and this // image has no other module to find it in. TlsRef::Symbol(s) => match symbols.get(s.sym().get()).filter(|d| d.is_defined()) { - Some(d) => d.tls_offset(s.addend(), memsz).map(Some).ok_or(()), + Some(d) => tls.and_then(|t| d.tls_offset(s.addend(), t)).map(Some).ok_or(()), None => Ok(None), }, } diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index c93f6bdde53..ac5e1adf9b7 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -34,7 +34,7 @@ fn validate( window, sym_count, fill, - tls_memsz: Some(u64::MAX), + tls: Some(tls_segment(u64::MAX)), }; entries.try_for_each(|r| rela::parse(r, &rules).map(|_| ())) } @@ -404,7 +404,7 @@ fn lookups_skip_undefined_symbols_and_the_null_entry() { ] .concat(); let table = SymTab::new(&syms, b"\0tls_var\0"); - let tls_var = table.find_tls("tls_var").and_then(|s| s.tls_offset(0, 0x100)); + let tls_var = table.find_tls("tls_var").and_then(|s| s.tls_offset(0, tls_segment(0x100))); assert_eq!(tls_var.map(|o| o.get()), Some(0x40)); assert_eq!(table.find("tls_var").map(|(i, _)| i), Some(2)); assert_eq!(table.defined().count(), 1); @@ -738,7 +738,7 @@ fn each_machine_reads_its_own_relocation_numbers() { window: (0, 0x100), sym_count: 0, fill: None, - tls_memsz: None, + tls: None, }; let parsed = rela::parse(entry, &rules).unwrap().unwrap(); assert_eq!((parsed.offset(), parsed.op()), (0x10, Op::Relative(rules.extent.offset(4).unwrap()))); diff --git a/toyos-elf/tests/tls.rs b/toyos-elf/tests/tls.rs index cadc307e56d..566f76f2f64 100644 --- a/toyos-elf/tests/tls.rs +++ b/toyos-elf/tests/tls.rs @@ -5,6 +5,10 @@ //! kernel-bug assert reached from a crafted `PT_TLS`. The property is proved //! here instead, which is what lets the assert go. +#[allow(dead_code)] +mod common; + +use common::tls_segment; use toyos_elf::sym; use toyos_elf::tls::{self, Static, TlsOffset, Variant}; @@ -12,17 +16,17 @@ const TCB: usize = 64; const DTV: usize = 16 + 64 * 8; const GRANULE: usize = 2 * 1024 * 1024; -/// `S + A` inside a segment of `memsz` bytes, through the only public path to a -/// [`TlsOffset`]: a crafted `STT_TLS` symbol read as one. +/// `S + A` inside a parsed `memsz`-byte `PT_TLS`, through the only public path +/// to a [`TlsOffset`]: a crafted `STT_TLS` symbol read against that segment. fn tls_offset(value: u64, addend: i64, memsz: u64) -> Option { let mut bytes = [0u8; sym::ENTRY_SIZE]; bytes[4] = (1 << 4) | 6; // STB_GLOBAL, STT_TLS bytes[6..8].copy_from_slice(&1u16.to_le_bytes()); // st_shndx: defined bytes[8..16].copy_from_slice(&value.to_le_bytes()); // st_value - sym::parse_at(&bytes, 0).unwrap().tls_offset(addend, memsz) + sym::parse_at(&bytes, 0).unwrap().tls_offset(addend, tls_segment(memsz)) } -/// A datum `off` bytes into a segment no offset here leaves. +/// A datum `off` bytes into the largest segment a file can declare. fn datum(off: u64) -> TlsOffset { tls_offset(off, 0, u64::MAX).unwrap() } From 411ded8eb2ab490b940bf1b7f17923509957bc2c Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 19:19:09 +0200 Subject: [PATCH 05/10] review #544 round 3: one TPOFF rule, GLOB_DAT past .dynsym refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `compute_tpoff` is the one `S + A - tp` rule. It takes the referencing module's segment and symbol table, so the executable's closure is a single call to it; the forwarder `resolve_tls` is deleted and `resolve_tls_ref` is private to `reloc.rs`. - The executable's `GLOB_DAT` refuses a symbol index its short-read `.dynsym` does not hold, as `SymbolPastTable`, through `SymTab::at` — the same check the TLS path now uses. It used to be skipped silently. - `elf::occupied_tls` answers "does this `PT_TLS` get a module" for all five readers. The false "a zero-size PT_TLS still gets a DTV slot" clause is deleted: no module is given for one. - `abuse_elf_loader` gains the TPOFF overflow for a dlopen and for a spawn, and the GLOB_DAT case; the harness asserts each one's reason, and checks every reason even when the guest failed. - `tls_dtv_race` joins `RUST_SKIP`; `tls_rebase_window` runs it. - The two issues go back to `open`, with no owner claimed. Co-Authored-By: Claude Opus 5.5 --- .../elf-domain-lint-line-not-yet-added.md | 6 +-- ...-not-the-sole-writer-of-a-mapped-module.md | 6 +-- kernel/src/elf/mod.rs | 11 +++-- kernel/src/elf/reloc.rs | 47 ++++++++----------- kernel/src/loader/mod.rs | 20 ++++---- kernel/src/loader/tls.rs | 4 +- kernel/src/syscall/vm.rs | 2 +- .../src/bin/abuse_elf_loader.rs | 46 ++++++++++++++++++ .../toyos-rust-tests/src/bin/tls_dtv_race.rs | 3 +- tests/toyos.rs | 40 ++++++++-------- toyos-elf/src/layout.rs | 3 +- toyos-elf/src/sym.rs | 7 +++ 12 files changed, 118 insertions(+), 77 deletions(-) diff --git a/issues/design-debt/elf-domain-lint-line-not-yet-added.md b/issues/design-debt/elf-domain-lint-line-not-yet-added.md index 1080f98e4fc..9f69d3c25ec 100644 --- a/issues/design-debt/elf-domain-lint-line-not-yet-added.md +++ b/issues/design-debt/elf-domain-lint-line-not-yet-added.md @@ -1,14 +1,12 @@ --- -status: assigned +status: open kind: track opened: 2026-09-27 --- # `toyos-elf` does not `forbid(clippy::arithmetic_side_effects)` yet -Held by the orchestrator's ELF-loader track; its next brief carries this. - -The design's domain-lint line — +The domain-lint line — `#![forbid(clippy::arithmetic_side_effects, clippy::indexing_slicing, …)]` on `toyos-elf/src/lib.rs` — is what would make an unchecked `+`/`-`/`[]` on a file-chosen value a compile error, so the "every number is checked" property diff --git a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md index 2867b974b19..cc5b4d64b33 100644 --- a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md +++ b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -1,12 +1,10 @@ --- -status: assigned +status: open kind: finding opened: 2026-09-27 --- -# `LoadedLib::write_at`'s "sole writer" `# Safety` is false in the `dlopen` path (M8) - -Held by the orchestrator's ELF-loader track; its next brief carries this. +# `LoadedLib::write_at`'s "sole writer" `# Safety` is false in the `dlopen` path `LoadedLib::write_at`'s `# Safety` says the caller must be the sole writer of the module's image for the call's duration. In `load_shared_lib` that holds: the diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index f33601f365e..831dee9142b 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -17,9 +17,8 @@ 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, - rebase_relative_relocs, resolve_dlopen_relocs, resolve_lib_bind_relocs, resolve_tls_ref, - tpoff32_value, + apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, compute_tpoff, + rebase_relative_relocs, resolve_dlopen_relocs, resolve_lib_bind_relocs, tpoff32_value, }; use crate::mm::{align_2m_checked, KernelSlice, MAX_HEAP_ALLOC, PAGE_2M, PAGE_BYTES}; @@ -76,6 +75,12 @@ pub struct TlsModule { pub is_static: bool, } +/// The `PT_TLS` a module is given a [`TlsModule`] for: a zero-size one is given +/// none. +pub fn occupied_tls(tls: Option) -> Option { + tls.filter(|t| t.memsz() > 0) +} + /// Everything a cross-module TLS relocation has to resolve against. pub struct TlsModuleInfo<'a> { pub libs: &'a [LoadedLib], diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 5d037186c05..3be39d6b7a7 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -11,7 +11,7 @@ //! process faults on the slot only if it later uses it. A resolved TLS //! reference whose `S + A` leaves the defining module's segment is refused. -use super::{CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; +use super::{occupied_tls, CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; use crate::UserAddr; use toyos_elf::sym::{Sym, SymTab}; use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef, TlsSegment}; @@ -159,25 +159,26 @@ pub fn apply_tpoff_relocs( Op::Tpoff32(t) => Some(t), _ => None, }; + let tpoff = |r| compute_tpoff(r, lib_base_offset, lib.tls(), lib.symbols(), tls, tls_info); for (_, r) in lib.entries(tpoff64, |c| &c.tpoff64) { - compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; + tpoff(r)?; } for (_, r) in lib.entries(tpoff32, |c| &c.tpoff32) { - tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; + tpoff32_value(tpoff(r)?)?; } let mut count64 = 0u64; for (offset, r) in lib.entries(tpoff64, |c| &c.tpoff64) { - let tpoff = compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?; + let value = tpoff(r)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff) }; + unsafe { lib.write_at::(offset, value) }; count64 += 1; } let mut count32 = 0u64; for (offset, r) in lib.entries(tpoff32, |c| &c.tpoff32) { - let tpoff = tpoff32_value(compute_tpoff(lib, r, lib_base_offset, tls, tls_info)?)?; + let value = tpoff32_value(tpoff(r)?)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff) }; + unsafe { lib.write_at::(offset, value) }; count32 += 1; } if count64 > 0 || count32 > 0 { @@ -217,12 +218,13 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< Op::DtpOff64(t) => Some(t), _ => None, }; + let resolve = |r| resolve_tls_ref(r, 0, lib.tls(), lib.symbols(), tls_info); for (_, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { - resolve_tls(lib, r, 0, tls_info)?; + resolve(r)?; } let mut count = 0u64; for (offset, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { - let value = resolve_tls(lib, r, 0, tls_info)?.map_or(0, |(_, at)| at.get()); + let value = resolve(r)?.map_or(0, |(_, at)| at.get()); // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, value) }; count += 1; @@ -237,7 +239,7 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< /// as it defines it, or `None` if none does. pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, TlsSegment, Sym)> { for lib in tls_info.libs { - let Some(segment) = lib.tls().filter(|t| t.memsz() > 0) else { + let Some(segment) = occupied_tls(lib.tls()) else { continue; }; if let Some(sym) = lib.symbols().find_tls(name) { @@ -277,7 +279,7 @@ fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, t /// `symbols` is the referencing module's table — a library's in-image one, or /// the executable's read off the file — so both loaders resolve through this /// one function. -pub fn resolve_tls_ref( +fn resolve_tls_ref( r: TlsRef, own_base_offset: usize, own_tls: Option, @@ -288,7 +290,7 @@ pub fn resolve_tls_ref( TlsRef::Own(at) => return Ok(Some((own_base_offset, at))), TlsRef::Symbol(s) => s, }; - let sym = symbols.get(s.sym().get()).ok_or(RelocError::SymbolPastTable)?; + let sym = symbols.at(s.sym())?; if sym.is_defined() { let segment = own_tls.ok_or(RelocError::TlsOutsideSegment)?; let at = sym.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment)?; @@ -307,26 +309,17 @@ pub fn resolve_tls_ref( } } -/// [`resolve_tls_ref`] for a loaded library, whose symbol table is in-image. -fn resolve_tls( - lib: &LoadedLib, - r: TlsRef, - own_base_offset: usize, - tls_info: &TlsModuleInfo, -) -> Result, RelocError> { - resolve_tls_ref(r, own_base_offset, lib.tls(), lib.symbols(), tls_info) -} - /// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module -/// defines. -fn compute_tpoff( - lib: &LoadedLib, +/// defines: the one rule for a library's relocations and the executable's. +pub fn compute_tpoff( r: TlsRef, - lib_base_offset: usize, + own_base_offset: usize, + own_tls: Option, + symbols: SymTab<'_>, tls: toyos_elf::tls::Static, tls_info: &TlsModuleInfo, ) -> Result { - match resolve_tls(lib, r, lib_base_offset, tls_info)? { + match resolve_tls_ref(r, own_base_offset, own_tls, symbols, tls_info)? { Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), None => Ok(0), } diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 39a7157a867..ee6a6dcfcff 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -40,7 +40,7 @@ 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::{GnuHash, Layout, RelocError, TlsRef}; +use toyos_elf::{GnuHash, Layout, RelocError}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -468,9 +468,10 @@ pub fn spawn( if r_sym.get() == 0 { continue; } - let Some(sym) = exe.symbols().get(r_sym.get()) else { - continue; - }; + let sym = exe.symbols().at(r_sym).map_err(|refused| { + log!("spawn: {}: {}", path, refused.as_str()); + SyscallError::InvalidArgument + })?; let name = 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()), @@ -502,7 +503,7 @@ pub fn spawn( }); } - let exe_tls_template = match layout.tls().filter(|t| t.memsz() > 0) { + let exe_tls_template = match elf::occupied_tls(layout.tls()) { 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); @@ -816,13 +817,8 @@ fn apply_tls_relocs( .iter() .find(|m| m.module_id == 1) .map_or(0, |m| m.base_offset); - // `0` for a symbol no module defines. - let exe_tpoff = |r: TlsRef| { - match elf::resolve_tls_ref(r, exe_base_offset, layout.tls(), exe.symbols(), &tls_info)? { - Some((base_offset, at)) => tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows), - None => Ok(0), - } - }; + 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 { reloc_index.add_u64(r_offset, exe_tpoff(r)? as u64); } diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index a4d09f0f0c5..f3909780c76 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -232,7 +232,7 @@ pub fn build_tls_layout( ) -> 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().filter(|t| t.memsz() > 0) { + let exe = match crate::elf::occupied_tls(layout.tls()) { None => None, Some(tls) => { let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); @@ -246,7 +246,7 @@ pub fn build_tls_layout( } }; let with_tls = || { - loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, lib.tls().filter(|t| t.memsz() > 0)?))) + loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, crate::elf::occupied_tls(lib.tls())?))) }; let libs = with_tls().zip(2u64..).map(|((template, tls), id)| { let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index d9e78e7bfd2..8e688b7e93d 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -286,7 +286,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); }); - let lib_tls = lib.tls().filter(|t| t.memsz() > 0); + let lib_tls = crate::elf::occupied_tls(lib.tls()); let data_arc = process::process_data(); let init_info = { let data = data_arc.lock(); 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 86616a72756..e95e87eab1b 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -66,6 +66,7 @@ const DT_GNU_HASH: i64 = 0x6fff_fef5u32 as i32 as i64; const SHT_DYNSYM: u32 = 11; +const R_X86_64_GLOB_DAT: u64 = 6; const R_X86_64_RELATIVE: u64 = 8; const R_X86_64_DTPMOD64: u64 = 16; const R_X86_64_TPOFF64: u64 = 18; @@ -549,6 +550,10 @@ fn main() { // 19. The apply-time TLS refusals, each named by its reason in the log. tls_apply_time_refusals_are_reached(); + // 20. A relocation naming a symbol the executable's short-read `.dynsym` does + // not hold is refused by name. + globdat_past_short_dynsym(); + // 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(); @@ -858,6 +863,47 @@ fn tls_apply_time_refusals_are_reached() { let lib_defs = unsafe { libloading::Library::new(&defs) }.expect("dlopen f13_defs_small.so"); dlopen_refused("f13_refs_past.so", &tls_refs_so(b"ytls", 0x140)); drop(lib_defs); + + // `S + A` (8 + `i64::MAX`) inside a `PT_TLS` declared past 2^63 bytes: only + // `S + A - tp` leaves an `i64`. A dlopen, then an executable needing the + // defining library at startup. + const PAST_I64: u64 = 0x8000_0000_0000_0010; + 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"); + dlopen_refused("tpoff_overflow.so", &tls_refs_so(b"vtls", i64::MAX)); + drop(lib_defs); + + 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()); + spawn_refused("tpoff_overflow_spawn", &exe.build()); +} + +/// An executable needing a library, whose `.gnu.hash` counts 1000 symbols +/// while the file ends 469 entries into `.dynsym`, with a `GLOB_DAT` naming +/// symbol 999: below the count the relocation parse bounds it by, past the +/// table the loader read. +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(); + for word in [1u32, 1000, 1, 0, 0, 0, 0] { + gnu_hash.extend_from_slice(&word.to_le_bytes()); + } + let exe = exe_with( + &[(DT_NEEDED, 1), (DT_GNU_HASH, 0x3000)], + &[(0x2000, (999u64 << 32) | R_X86_64_GLOB_DAT, 0)], + None, + ) + .poke(0x1801, dep.as_bytes()) + .poke(0x3000, &gnu_hash); + spawn_refused("globdat_past_dynsym", &exe.build()); } /// A shared object defining its own `xtls` (`STT_TLS`, offset 8) in a 0x20-byte diff --git a/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs index ff40923f351..dfda028af3e 100644 --- a/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs +++ b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs @@ -12,8 +12,7 @@ //! `kernel/src/loader/tls.rs`'s `rebase_window::MARK`. A kernel armed with //! `tls-rebase-window` holds such a spawn before its rebase until the sibling's //! store lands if the block is already reachable, and says it is not if it is -//! not; `tls_rebase_window` runs this there and reads which. Unarmed, the -//! window is the scheduler's to hit. +//! not; `tls_rebase_window` runs this there and reads which. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}; use std::time::{Duration, Instant}; diff --git a/tests/toyos.rs b/tests/toyos.rs index b66149bdeb5..e781c184fe3 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -250,6 +250,9 @@ const RUST_SKIP: &[&str] = &[ // It waits for a cue only a kernel armed with `copy-meets-a-remap` gives. // `user_copy_races_munmap` runs it. "copy_out_races_munmap", + // Only a kernel armed with `tls-rebase-window` holds a spawn in the window it probes. + // `tls_rebase_window` runs it. + "tls_dtv_race", // The C corpus's comparator: a helper reached through one symlink per case, // never a test of its own. `shared_metal` stages every name on this list. "ccheck", @@ -3503,33 +3506,30 @@ fn check_for(name: &str) -> fn(&TestResult) -> bool { } } -/// The reason the loader logs when a resolved TLS `S + A` leaves the defining -/// module's `PT_TLS`. -const TLS_OUTSIDE_SEGMENT: &str = toyos_elf::RelocError::TlsOutsideSegment.as_str(); - -/// `abuse_elf_loader` plus the reason each apply-time TLS refusal must fire for. +/// `abuse_elf_loader` plus the reason each apply-time refusal must fire for. /// -/// Each case is refused for the right reason only if the kernel names -/// [`TLS_OUTSIDE_SEGMENT`] beside the file — a case refused later, for another -/// reason, would pass the exit-code check alone. +/// Each case is refused for the right reason only if the kernel names its +/// [`toyos_elf::RelocError`] beside the file — a case refused later, for +/// another reason, would pass the exit-code check alone. Every reason is +/// checked even when the guest failed, so one run shows each case's verdict. fn check_abuse_elf_loader(result: &TestResult) -> bool { - if !check_rust_result(result) { - return false; - } + use toyos_elf::RelocError; + let mut ok = check_rust_result(result); let log = format!("{}{}", result.before, result.serial); - let mut ok = true; - for (file, what) in [ - ("tls_apply_refs.so", "the dlopen apply-time TLS refusal"), - ("tls_apply_spawn", "the spawn apply-time TLS refusal"), - ("f13_refs_past.so", "the cross-module apply-time TLS refusal"), + for (file, refused, what) in [ + ("tls_apply_refs.so", RelocError::TlsOutsideSegment, "the dlopen apply-time TLS refusal"), + ("tls_apply_spawn", RelocError::TlsOutsideSegment, "the spawn apply-time TLS refusal"), + ("f13_refs_past.so", RelocError::TlsOutsideSegment, "the cross-module apply-time TLS refusal"), + ("tpoff_overflow.so", RelocError::TpoffOverflows, "the dlopen TPOFF overflow"), + ("tpoff_overflow_spawn", RelocError::TpoffOverflows, "the spawn TPOFF overflow"), + ("globdat_past_dynsym", RelocError::SymbolPastTable, "the executable's GLOB_DAT past .dynsym"), ] { - let named = log - .lines() - .any(|l| l.contains(file) && l.contains(TLS_OUTSIDE_SEGMENT)); + let reason = refused.as_str(); + let named = log.lines().any(|l| l.contains(file) && l.contains(reason)); if !named { eprintln!( "FAIL rs::abuse_elf_loader: {what} did not fire for its reason — no line names \ - {file:?} with {TLS_OUTSIDE_SEGMENT:?}{}", + {file:?} with {reason:?}{}", kernel_account(result) ); ok = false; diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index 2e3436b3300..d06668336ff 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -187,8 +187,7 @@ impl Segment { /// /// `align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], and the /// file-backed template lies inside the image. Absent TLS is `None`, never a -/// zero `memsz`: a module with a `PT_TLS` of zero size still gets a DTV slot, -/// and the two cases are not the same question. +/// zero `memsz`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TlsSegment { template: ImageRange, diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index ede60cbfeb6..4003a2b0ba9 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -7,6 +7,7 @@ use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; +use crate::rela::RelocError; use crate::tls::TlsOffset; use crate::Error; @@ -124,6 +125,12 @@ impl<'a> SymTab<'a> { self.strs } + /// The symbol a relocation names. A [`SymIndex`] is below the count its + /// parse was given, and a table read short of that count does not hold it. + pub fn at(&self, i: SymIndex) -> Result { + self.get(i.get()).ok_or(RelocError::SymbolPastTable) + } + pub fn get(&self, i: usize) -> Option { if i >= self.count() { return None; From 63a1f3155f68b80d573b7df0cd7e999143a6cc79 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 19:22:48 +0200 Subject: [PATCH 06/10] abuse_elf_loader: each new case is reached before a guest panic can hide it The TPOFF overflow's spawn runs before its dlopen, and the GLOB_DAT case before the TLS cases. A mutation that lets the dlopen through panics the guest; run first, it left the spawn case unreached, so the harness's red on it measured nothing. Co-Authored-By: Claude Opus 5.5 --- .../src/bin/abuse_elf_loader.rs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index e95e87eab1b..3b823a42008 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -547,13 +547,13 @@ fn main() { // 14. A cross-module initial-exec TLS reference resolves to `S + A - tp`. f13_cross_module_addend_is_kept(); - // 19. The apply-time TLS refusals, each named by its reason in the log. - tls_apply_time_refusals_are_reached(); - - // 20. A relocation naming a symbol the executable's short-read `.dynsym` does + // 19. A relocation naming a symbol the executable's short-read `.dynsym` does // not hold is refused by name. globdat_past_short_dynsym(); + // 20. The apply-time TLS refusals, each named by its reason in the log. + tls_apply_time_refusals_are_reached(); + // 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(); @@ -865,14 +865,10 @@ fn tls_apply_time_refusals_are_reached() { drop(lib_defs); // `S + A` (8 + `i64::MAX`) inside a `PT_TLS` declared past 2^63 bytes: only - // `S + A - tp` leaves an `i64`. A dlopen, then an executable needing the - // defining library at startup. + // `S + A - tp` leaves an `i64`. An executable needing the defining library + // at startup, then a dlopen. The spawn goes first: a block that size is + // refused later for another reason, which only the harness's log check sees. const PAST_I64: u64 = 0x8000_0000_0000_0010; - 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"); - dlopen_refused("tpoff_overflow.so", &tls_refs_so(b"vtls", i64::MAX)); - drop(lib_defs); - let dep = "tpoff_overflow_dep.so"; write_file(dep, &tls_defs_so(b"wtls", PAST_I64)); // `.dynstr` at 0x1800 holds "wtls\0\0". @@ -881,6 +877,11 @@ fn tls_apply_time_refusals_are_reached() { .poke(0x1801, b"wtls\0") .poke(0x1806, dep.as_bytes()); spawn_refused("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"); + dlopen_refused("tpoff_overflow.so", &tls_refs_so(b"vtls", i64::MAX)); + drop(lib_defs); } /// An executable needing a library, whose `.gnu.hash` counts 1000 symbols From 237494e30838d8f6b71f8397d31ab2ff7f2926b5 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 19:48:15 +0200 Subject: [PATCH 07/10] review #544 round 4: one symbol-index bound, read before the parse The executable's relocations were parsed against a symbol count the file declared (`exe_sym_count`), `.dynsym` was read afterwards through the clamping `read_file_range`, and `SymTab::at` refused the gap at use. Now `read_exe_tables` reads `.dynstr` and `.dynsym` first and parses against `SymTab::count` of the table it holds, the count `load_shared_lib` already parses a library's relocations against. An `r_sym` past what was read is refused at parse as `SymbolPastTable`, so the harness's `globdat_past_dynsym` line is unchanged. A lookup by `SymIndex` is then infallible: `elf::relocated_symbol` asserts it, replacing `SymTab::at`, the `GLOB_DAT` `map_err` block and the silent `""`/`false` defaults in `resolve_dlopen_relocs`, `resolve_lib_bind_relocs` and `resolve_dtpmod`. `occupied_tls` moves into `toyos-elf` as `TlsSegment::occupied`, and the fuzz oracle's copy of the rule goes through it; a host case pins zero against one byte. The two recorded compromises are held by the orchestrator. Prose the review marked REMOVE is deleted, as is the `globdat_past_short_dynsym` clause the new bound made false. A false doc line already on main is filed, not fixed. Co-Authored-By: Claude Opus 5.5 --- .../elf-domain-lint-line-not-yet-added.md | 4 +- ...ero-size-tls-test-doc-claims-a-dtv-slot.md | 14 +++++++ ...-not-the-sole-writer-of-a-mapped-module.md | 4 +- kernel/src/elf/mod.rs | 23 ++++++------ kernel/src/elf/reloc.rs | 18 ++++----- kernel/src/loader/mod.rs | 37 ++++++++----------- kernel/src/loader/tls.rs | 6 +-- kernel/src/syscall/vm.rs | 2 +- .../src/bin/abuse_elf_loader.rs | 7 +--- toyos-elf/src/layout.rs | 9 ++++- toyos-elf/src/sym.rs | 7 ---- toyos-elf/tests/crafted.rs | 6 +++ toyos-elf/tests/fuzz.rs | 4 +- 13 files changed, 77 insertions(+), 64 deletions(-) create mode 100644 issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md diff --git a/issues/design-debt/elf-domain-lint-line-not-yet-added.md b/issues/design-debt/elf-domain-lint-line-not-yet-added.md index 9f69d3c25ec..404fc31d3ed 100644 --- a/issues/design-debt/elf-domain-lint-line-not-yet-added.md +++ b/issues/design-debt/elf-domain-lint-line-not-yet-added.md @@ -1,5 +1,5 @@ --- -status: open +status: assigned kind: track opened: 2026-09-27 --- @@ -16,3 +16,5 @@ It is not added. Exit condition: the line is on `lib.rs`, every site inside it is checked or carries a one-clause `#[allow]`, and CI runs clippy on the crate with it. + +Held by the orchestrator. diff --git a/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md b/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md new file mode 100644 index 00000000000..86405259dcf --- /dev/null +++ b/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md @@ -0,0 +1,14 @@ +--- +status: open +kind: finding +opened: 2026-09-27 +--- + +# A test's doc says a zero-size `PT_TLS` gets a DTV slot; the kernel gives it none + +`toyos-elf/tests/crafted.rs`'s `a_zero_size_tls_segment_is_present_not_absent` +says "a module with a `PT_TLS` of zero size still gets a DTV slot". The kernel +gives a zero-`memsz` segment no TLS module and so no module id +(`TlsSegment::occupied`, applied in `kernel/src/loader/tls.rs`). + +Exit condition: that doc line is deleted. diff --git a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md index cc5b4d64b33..274e8ce8eb4 100644 --- a/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md +++ b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -1,5 +1,5 @@ --- -status: open +status: assigned kind: finding opened: 2026-09-27 --- @@ -24,3 +24,5 @@ cannot be the thing that makes those `unsafe` blocks sound. Exit condition: either the writes move before `map_into`, or the `# Safety` is restated to the invariant the `dlopen` path actually upholds and every call site's `SAFETY:` cites it. + +Held by the orchestrator. diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 831dee9142b..ff791fcdd97 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -27,7 +27,7 @@ use crate::UserAddr; use toyos_elf::dynamic::{Dynamic, InitArray}; use toyos_elf::section::{SectionTable, SHT_DYNSYM}; use toyos_elf::sym::{Sym, SymTab}; -use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError, TlsSegment}; +use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError, SymIndex, TlsSegment}; /// `toyos_elf::MAX_TLS_ALIGN` must equal the kernel's largest page. const _: () = assert!(toyos_elf::MAX_TLS_ALIGN == PAGE_2M); @@ -75,12 +75,6 @@ pub struct TlsModule { pub is_static: bool, } -/// The `PT_TLS` a module is given a [`TlsModule`] for: a zero-size one is given -/// none. -pub fn occupied_tls(tls: Option) -> Option { - tls.filter(|t| t.memsz() > 0) -} - /// Everything a cross-module TLS relocation has to resolve against. pub struct TlsModuleInfo<'a> { pub libs: &'a [LoadedLib], @@ -266,6 +260,12 @@ fn bounded_symbol_outside() -> ! { panic!("ELF: a symbol load_shared_lib bounded inside the image lies outside it") } +/// The symbol a parsed relocation names, in the table whose [`SymTab::count`] +/// bounded its parse: a miss is a kernel bug. +pub fn relocated_symbol(symbols: SymTab<'_>, i: SymIndex) -> Sym { + symbols.get(i.get()).expect("ELF: a relocation's symbol lies past the table its parse was bounded by") +} + /// A module's tables parsed differently the second time: the bytes moved under /// a module whose tables no writer reaches, which is a kernel bug. #[cold] @@ -432,18 +432,19 @@ pub fn load_shared_lib( } None => None, }; - let sym_count = dynsym.as_ref().map_or(0, |s| s.size() / toyos_elf::sym::ENTRY_SIZE); let dynstr = module.optional(dyn_info.strtab, dyn_info.strsz.unwrap_or(0))?; // Every symbol another module or `dlsym` may later ask for is inside this // one, and every TLS one inside its segment — or the module is refused now. - { + let sym_count = { // SAFETY: both came from `ModuleImage::slice`'s bounds check; `image` // is still exclusively owned here. let (syms, strs) = unsafe { (dynsym.as_ref().map_or(&[][..], |s| s.as_slice()), dynstr.as_ref().map_or(&[][..], |s| s.as_slice())) }; - SymTab::new(syms, strs).bounded(extent, layout.tls()).map_err(|e| e.as_str())?; - } + let symbols = SymTab::new(syms, strs); + symbols.bounded(extent, layout.tls()).map_err(|e| e.as_str())?; + symbols.count() + }; let init_array = InitArray::parse(dyn_info.init_array, extent).map_err(|e| e.as_str())?; let rela = match dyn_info.rela { diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 3be39d6b7a7..b52ed02baae 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -11,7 +11,7 @@ //! process faults on the slot only if it later uses it. A resolved TLS //! reference whose `S + A` leaves the defining module's segment is refused. -use super::{occupied_tls, CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; +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}; @@ -100,7 +100,7 @@ pub fn resolve_dlopen_relocs(lib: &LoadedLib, other_libs: &[LoadedLib]) { let mut resolved = 0u64; let mut unresolved = 0u64; for (offset, sym) in lib.bind_entries() { - let name = symbols.name(sym.get()); + let name = relocated_symbol(symbols, sym).name_in(symbols.strings()); match other_libs.iter().find_map(|other| other.resolve(name)) { Some(addr) => { // SAFETY: rela::tables_outside_window refused any image whose tables meet the window these writes land in. @@ -127,7 +127,7 @@ pub fn resolve_lib_bind_relocs( ) { let symbols = lib.symbols(); for (offset, sym) in lib.bind_entries() { - let name = symbols.name(sym.get()); + let name = relocated_symbol(symbols, sym).name_in(symbols.strings()); let resolved = exe_sym_map .get(name) .copied() @@ -239,7 +239,7 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< /// as it defines it, or `None` if none does. pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(&'a TlsModule, TlsSegment, Sym)> { for lib in tls_info.libs { - let Some(segment) = occupied_tls(lib.tls()) else { + let Some(segment) = lib.tls().and_then(TlsSegment::occupied) else { continue; }; if let Some(sym) = lib.symbols().find_tls(name) { @@ -258,10 +258,11 @@ pub fn defining_module<'a>(name: &str, tls_info: &'a TlsModuleInfo) -> Option<(& fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, tls_info: &TlsModuleInfo) -> u64 { let Some(sym) = sym else { return self_module_id }; let symbols = lib.symbols(); - if symbols.get(sym.get()).is_some_and(|s| s.is_defined()) { + let named = relocated_symbol(symbols, sym); + if named.is_defined() { return self_module_id; } - let name = symbols.name(sym.get()); + let name = named.name_in(symbols.strings()); match defining_module(name, tls_info) { Some((module, _, _)) => module.module_id, None => { @@ -290,7 +291,7 @@ fn resolve_tls_ref( TlsRef::Own(at) => return Ok(Some((own_base_offset, at))), TlsRef::Symbol(s) => s, }; - let sym = symbols.at(s.sym())?; + let sym = relocated_symbol(symbols, s.sym()); if sym.is_defined() { let segment = own_tls.ok_or(RelocError::TlsOutsideSegment)?; let at = sym.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment)?; @@ -309,8 +310,7 @@ fn resolve_tls_ref( } } -/// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module -/// defines: the one rule for a library's relocations and the executable's. +/// `S + A - tp` for one initial-exec reference, or `0` for a symbol no module defines. pub fn compute_tpoff( r: TlsRef, own_base_offset: usize, diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index ee6a6dcfcff..6bcf4182d8d 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -40,7 +40,7 @@ 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::{GnuHash, Layout, RelocError}; +use toyos_elf::{GnuHash, Layout, RelocError, TlsSegment}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -265,7 +265,17 @@ fn read_exe_tables( Some(vaddr) => Some(file_off(layout, path, "DT_SYMTAB", vaddr)?), None => None, }; - let sym_count = exe_sym_count(backing, layout, &dyn_info, path)?; + 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 @@ -274,7 +284,7 @@ fn read_exe_tables( let rules = Rules { extent, window: (extent.min(), extent.max()), - sym_count, + sym_count: SymTab::new(&dynsym, &dynstr).count(), fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), tls: layout.tls(), }; @@ -283,19 +293,6 @@ fn read_exe_tables( refused.error() })?; - 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, sym_count) { - (Some(off), n) if n > 0 => table(backing, path, "symbol count", off, n * sym::ENTRY_SIZE)?, - _ => Vec::new(), - }; - Ok(ExeTables { needed, dynstr, dynsym, relas }) } @@ -468,11 +465,7 @@ pub fn spawn( if r_sym.get() == 0 { continue; } - let sym = exe.symbols().at(r_sym).map_err(|refused| { - log!("spawn: {}: {}", path, refused.as_str()); - SyscallError::InvalidArgument - })?; - let name = sym.name_in(&exe.dynstr); + 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 => log!("dynamic: unresolved exe symbol: {}", name), @@ -503,7 +496,7 @@ pub fn spawn( }); } - let exe_tls_template = match elf::occupied_tls(layout.tls()) { + 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); diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index f3909780c76..b91085263dd 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -10,7 +10,7 @@ use crate::elf::TlsModule; use crate::process::{MappedPages, OwnedAlloc, PageAlloc, PageTables, Unpublished}; use crate::UserAddr; use toyos_elf::tls::{Static, Variant}; -use toyos_elf::Layout; +use toyos_elf::{Layout, TlsSegment}; /// This machine's TLS layout. pub const VARIANT: Variant = Variant::of(crate::arch::ELF_MACHINE); @@ -232,7 +232,7 @@ pub fn build_tls_layout( ) -> 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 crate::elf::occupied_tls(layout.tls()) { + let exe = match layout.tls().and_then(TlsSegment::occupied) { None => None, Some(tls) => { let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); @@ -246,7 +246,7 @@ pub fn build_tls_layout( } }; let with_tls = || { - loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, crate::elf::occupied_tls(lib.tls())?))) + loaded_libs.iter().filter_map(|lib| Some((lib.tls_template, lib.tls()?.occupied()?))) }; let libs = with_tls().zip(2u64..).map(|((template, tls), id)| { let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index 8e688b7e93d..43a646cbb62 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -286,7 +286,7 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init crate::arch::tlb::shootdown(crate::arch::tlb::Origin::Dlopen); }); - let lib_tls = crate::elf::occupied_tls(lib.tls()); + let lib_tls = lib.tls().and_then(toyos_elf::TlsSegment::occupied); let data_arc = process::process_data(); let init_info = { let data = data_arc.lock(); 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 3b823a42008..fe57fcb48d7 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -865,9 +865,7 @@ fn tls_apply_time_refusals_are_reached() { drop(lib_defs); // `S + A` (8 + `i64::MAX`) inside a `PT_TLS` declared past 2^63 bytes: only - // `S + A - tp` leaves an `i64`. An executable needing the defining library - // at startup, then a dlopen. The spawn goes first: a block that size is - // refused later for another reason, which only the harness's log check sees. + // `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)); @@ -886,8 +884,7 @@ fn tls_apply_time_refusals_are_reached() { /// An executable needing a library, whose `.gnu.hash` counts 1000 symbols /// while the file ends 469 entries into `.dynsym`, with a `GLOB_DAT` naming -/// symbol 999: below the count the relocation parse bounds it by, past the -/// table the loader read. +/// symbol 999. fn globdat_past_short_dynsym() { let dep = "globdat_dep.so"; write_file(dep, &so_with(&[], &[], None)); diff --git a/toyos-elf/src/layout.rs b/toyos-elf/src/layout.rs index d06668336ff..c51bab1660e 100644 --- a/toyos-elf/src/layout.rs +++ b/toyos-elf/src/layout.rs @@ -186,8 +186,7 @@ impl Segment { /// A `PT_TLS` segment. /// /// `align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], and the -/// file-backed template lies inside the image. Absent TLS is `None`, never a -/// zero `memsz`. +/// file-backed template lies inside the image. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TlsSegment { template: ImageRange, @@ -211,6 +210,12 @@ impl TlsSegment { pub const fn align(&self) -> u64 { self.align } + + /// This segment, or `None` when a zero `memsz` takes no bytes of a thread's + /// TLS block and so is given no module there. + pub const fn occupied(self) -> Option { + if self.memsz > 0 { Some(self) } else { None } + } } /// `PT_DYNAMIC`, where the file holds it and where the image does. diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index 4003a2b0ba9..ede60cbfeb6 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -7,7 +7,6 @@ use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; -use crate::rela::RelocError; use crate::tls::TlsOffset; use crate::Error; @@ -125,12 +124,6 @@ impl<'a> SymTab<'a> { self.strs } - /// The symbol a relocation names. A [`SymIndex`] is below the count its - /// parse was given, and a table read short of that count does not hold it. - pub fn at(&self, i: SymIndex) -> Result { - self.get(i.get()).ok_or(RelocError::SymbolPastTable) - } - pub fn get(&self, i: usize) -> Option { if i >= self.count() { return None; diff --git a/toyos-elf/tests/crafted.rs b/toyos-elf/tests/crafted.rs index 79e237dbdd6..e9fea465618 100644 --- a/toyos-elf/tests/crafted.rs +++ b/toyos-elf/tests/crafted.rs @@ -254,6 +254,12 @@ fn a_zero_size_tls_segment_is_present_not_absent() { assert!(accepted(Elf::honest(0x1000).build()).tls().is_none()); } +#[test] +fn only_a_tls_segment_with_bytes_is_occupied() { + assert_eq!(tls_segment(0).occupied(), None); + assert_eq!(tls_segment(1).occupied(), Some(tls_segment(1))); +} + // ── The section header table, which is optional ───────────────────────── #[test] diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index 499a9262900..ac695dd989f 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -26,7 +26,7 @@ use toyos_elf::dynamic::{Dynamic, InitArray}; use toyos_elf::rela::{self, FillLattice, 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}; +use toyos_elf::{ImageRange, Layout, Machine, TlsSegment}; /// Images per run: every one of them reaches the relocation parse unless its /// own headers are what the mutation broke. @@ -376,7 +376,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), // The thread's static block: this module alone, placed as the kernel's // `build_tls_layout` places an executable's. let variant = Variant::of(case.machine); - let (block, base_offset, memsz) = match tls.filter(|t| t.memsz() > 0) { + let (block, base_offset, memsz) = match tls.and_then(TlsSegment::occupied) { Some(t) => { let memsz = usize::try_from(t.memsz()).map_err(|_| ())?; let align = usize::try_from(t.align()).map_err(|_| ())?; From eb0d98c8d7cd1647b8225583406c6f3924d24f16 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 20:21:30 +0200 Subject: [PATCH 08/10] review #544 round 5: a symbol index is bounded by the table it indexes `rela::Rules.sym_count` is gone. `rela::parse` takes the module's `SymTab` and bounds every `r_sym` by its `count()`, so neither loader keeps a count that could disagree with the table a `SymIndex` is later looked up in. The executable's loader passes the `SymTab` over the `.dynsym` and `.dynstr` it read; the library's passes the one over its in-image slices at load, and `LoadedLib::symbols()` on the re-parse. The bootloader, which binds no symbol, passes `SymTab::empty()`. The dlopen log line loses its symbol count, which only the deleted count fed; the `dynamic: loaded` line still prints one. A host case pins the edge: four whole entries and eight bytes, where index 4 is refused and 3 accepted for every symbol-binding type. `globdat_past_dynsym` names symbol 469, the first past the 469 whole entries and 8 bytes the executable's read holds, instead of 999. Prose the review marked REMOVE is deleted, with the issue that filed one of those lines. Co-Authored-By: Claude Opus 5.5 --- bootloader/src/main.rs | 5 ++-- ...ero-size-tls-test-doc-claims-a-dtv-slot.md | 14 ----------- kernel/src/elf/index.rs | 5 ++-- kernel/src/elf/mod.rs | 24 +++++++------------ kernel/src/elf/reloc.rs | 4 ---- kernel/src/loader/mod.rs | 3 +-- .../src/bin/abuse_elf_loader.rs | 6 ++--- toyos-elf/src/rela.rs | 10 ++++---- toyos-elf/tests/crafted.rs | 3 --- toyos-elf/tests/fuzz.rs | 3 +-- toyos-elf/tests/tables.rs | 23 ++++++++++++++---- 11 files changed, 43 insertions(+), 57 deletions(-) delete mode 100644 issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index a33739ee4dc..cfdc293def4 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -8,7 +8,7 @@ use core::mem; use alloc::vec; use alloc::alloc::Layout; use toyos_elf::section::SectionTable; -use toyos_elf::{rela, ImageOffset, Op, RelaTable}; +use toyos_elf::{rela, ImageOffset, Op, RelaTable, SymTab}; use uefi::{ prelude::*, CStr16, @@ -389,7 +389,6 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { let rules = rela::Rules { extent, window: (0, mem_size as u64), - sym_count: 0, fill: None, tls: None, }; @@ -398,7 +397,7 @@ fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { let table = file_range(kernel_elf_bytes, section.offset, section.size) .expect("kernel.elf: SHT_RELA section is past the end of the file"); for raw in RelaTable::new(table, arch::ELF_MACHINE).iter() { - let parsed = rela::parse(raw, &rules).unwrap_or_else(|e| panic!("kernel.elf: {e}")); + let parsed = rela::parse(raw, &rules, SymTab::empty()).unwrap_or_else(|e| panic!("kernel.elf: {e}")); let Some((offset, Op::Relative(target))) = parsed.map(|r| (r.offset(), r.op())) else { panic!("kernel.elf: unsupported relocation type {:?}", raw.kind()); }; diff --git a/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md b/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md deleted file mode 100644 index 86405259dcf..00000000000 --- a/issues/design-debt/zero-size-tls-test-doc-claims-a-dtv-slot.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-09-27 ---- - -# A test's doc says a zero-size `PT_TLS` gets a DTV slot; the kernel gives it none - -`toyos-elf/tests/crafted.rs`'s `a_zero_size_tls_segment_is_present_not_absent` -says "a module with a `PT_TLS` of zero size still gets a DTV slot". The kernel -gives a zero-`memsz` segment no TLS module and so no module id -(`TlsSegment::occupied`, applied in `kernel/src/loader/tls.rs`). - -Exit condition: that doc line is deleted. diff --git a/kernel/src/elf/index.rs b/kernel/src/elf/index.rs index 89354f5537a..3172aa721eb 100644 --- a/kernel/src/elf/index.rs +++ b/kernel/src/elf/index.rs @@ -9,7 +9,7 @@ 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, TlsRef}; +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. @@ -64,6 +64,7 @@ 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) @@ -82,7 +83,7 @@ pub fn parse_rela_entries( tpoff32: Vec::with_capacity(reserve.tpoff32), }; for raw in entries() { - let Some(r) = rela::parse(raw, rules).map_err(Refused::Entry)? else { continue }; + 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)), diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index ff791fcdd97..885f7911c79 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -216,7 +216,7 @@ impl LoadedLib { /// the process nor a relocation writes them. fn relocations(&self) -> impl Iterator + '_ { self.raw_relocations() - .filter_map(|raw| rela::parse(raw, &self.rules).unwrap_or_else(|e| reparse_refused(e))) + .filter_map(|raw| rela::parse(raw, &self.rules, self.symbols()).unwrap_or_else(|e| reparse_refused(e))) } /// One past the last virtual address this module occupies. @@ -435,16 +435,12 @@ pub fn load_shared_lib( let dynstr = module.optional(dyn_info.strtab, dyn_info.strsz.unwrap_or(0))?; // Every symbol another module or `dlsym` may later ask for is inside this // one, and every TLS one inside its segment — or the module is refused now. - let sym_count = { - // SAFETY: both came from `ModuleImage::slice`'s bounds check; `image` - // is still exclusively owned here. - let (syms, strs) = unsafe { - (dynsym.as_ref().map_or(&[][..], |s| s.as_slice()), dynstr.as_ref().map_or(&[][..], |s| s.as_slice())) - }; - let symbols = SymTab::new(syms, strs); - symbols.bounded(extent, layout.tls()).map_err(|e| e.as_str())?; - symbols.count() + // SAFETY: both came from `ModuleImage::slice`'s bounds check, and no write + // below reaches them: `rela::tables_outside_window` refuses that first. + let symbols = unsafe { + SymTab::new(dynsym.as_ref().map_or(&[][..], |s| s.as_slice()), dynstr.as_ref().map_or(&[][..], |s| s.as_slice())) }; + symbols.bounded(extent, layout.tls()).map_err(|e| e.as_str())?; let init_array = InitArray::parse(dyn_info.init_array, extent).map_err(|e| e.as_str())?; let rela = match dyn_info.rela { @@ -474,7 +470,6 @@ pub fn load_shared_lib( let rules = rela::Rules { extent, window, - sym_count, // A library's image is written contiguously, with no fill-page edge. fill: None, tls: layout.tls(), @@ -484,7 +479,7 @@ pub fn load_shared_lib( let base_phys = image.phys(); let mut reloc_count = 0u64; for raw in table_entries(&rela).chain(table_entries(&jmprel)) { - let Some(r) = rela::parse(raw, &rules).map_err(|e| e.as_str())? else { continue }; + let Some(r) = rela::parse(raw, &rules, symbols).map_err(|e| e.as_str())? else { continue }; if let toyos_elf::Op::Relative(target) = r.op() { // SAFETY: `module.slice(r.offset(), 8)?` bounds-checks the write // independently of the parse; `image` is still exclusively owned. @@ -497,15 +492,14 @@ pub fn load_shared_lib( let t4 = crate::clock::nanos_since_boot(); log!( - "dlopen: base={:#x} {}MB alloc={}ms zero={}ms copy={}ms reloc={}ms ({} relocs, {} syms)", + "dlopen: base={:#x} {}MB alloc={}ms zero={}ms copy={}ms reloc={}ms ({} relocs)", base_phys, load_size / (1024 * 1024), (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000, (t3 - t2) / 1_000_000, (t4 - t3) / 1_000_000, - reloc_count, - sym_count + reloc_count ); Ok(( diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index b52ed02baae..d3e50048638 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -276,10 +276,6 @@ fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, t /// lies in: the referencing module's own (`own_base_offset`, `own_tls`), or /// the module defining the symbol. `None` is a symbol no module defines, which /// is logged; a sum outside the defining module's segment refuses the module. -/// -/// `symbols` is the referencing module's table — a library's in-image one, or -/// the executable's read off the file — so both loaders resolve through this -/// one function. fn resolve_tls_ref( r: TlsRef, own_base_offset: usize, diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 6bcf4182d8d..0d5e9685cb9 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -284,11 +284,10 @@ fn read_exe_tables( let rules = Rules { extent, window: (extent.min(), extent.max()), - sym_count: SymTab::new(&dynsym, &dynstr).count(), fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), tls: layout.tls(), }; - let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules).map_err(|refused| { + 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() })?; 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 fe57fcb48d7..5e7b540b16e 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -883,8 +883,8 @@ fn tls_apply_time_refusals_are_reached() { } /// An executable needing a library, whose `.gnu.hash` counts 1000 symbols -/// while the file ends 469 entries into `.dynsym`, with a `GLOB_DAT` naming -/// symbol 999. +/// while the file ends 469 entries and 8 bytes into `.dynsym`, with a +/// `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)); @@ -896,7 +896,7 @@ fn globdat_past_short_dynsym() { } let exe = exe_with( &[(DT_NEEDED, 1), (DT_GNU_HASH, 0x3000)], - &[(0x2000, (999u64 << 32) | R_X86_64_GLOB_DAT, 0)], + &[(0x2000, (469u64 << 32) | R_X86_64_GLOB_DAT, 0)], None, ) .poke(0x1801, dep.as_bytes()) diff --git a/toyos-elf/src/rela.rs b/toyos-elf/src/rela.rs index 6dfe5508581..dd1fd07f54e 100644 --- a/toyos-elf/src/rela.rs +++ b/toyos-elf/src/rela.rs @@ -16,7 +16,7 @@ use crate::header::Machine; use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; -use crate::sym::SymIndex; +use crate::sym::{SymIndex, SymTab}; use crate::tls::TlsOffset; /// Bytes in one `Elf64_Rela`. @@ -147,8 +147,6 @@ pub struct Rules { pub extent: Extent, /// Where writes may land, `[lo, hi)` in `r_offset`'s own coordinates. pub window: (u64, u64), - /// Entries in the symbol table `r_sym` indexes. - pub sym_count: usize, /// `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, @@ -229,10 +227,12 @@ impl TlsSymRef { /// Parse one entry against the module's [`Rules`]: `Ok(None)` for a type this /// loader does not write, the reason for one it must not. /// +/// A [`SymIndex`] in the answer indexes `symbols` and no other table. +/// /// The window is the *writable* one rather than the whole image: once the /// module is cached its read-only pages are shared between processes, and the /// write lands in a private allocation covering only that window. -pub fn parse(rela: Rela, rules: &Rules) -> Result, RelocError> { +pub fn parse(rela: Rela, rules: &Rules, symbols: SymTab<'_>) -> Result, RelocError> { let Some(width) = rela.kind.write_width() else { return match rela.kind { RelocKind::TlsDesc => Err(RelocError::TlsDescriptor), @@ -255,7 +255,7 @@ pub fn parse(rela: Rela, rules: &Rules) -> Result, RelocError> { } } - let sym = || SymIndex::below(rela.sym, rules.sym_count).ok_or(RelocError::SymbolPastTable); + let sym = || SymIndex::below(rela.sym, symbols.count()).ok_or(RelocError::SymbolPastTable); let tls = || -> Result { if rela.sym != 0 { return Ok(TlsRef::Symbol(TlsSymRef { sym: sym()?, addend: rela.addend })); diff --git a/toyos-elf/tests/crafted.rs b/toyos-elf/tests/crafted.rs index e9fea465618..baff9336062 100644 --- a/toyos-elf/tests/crafted.rs +++ b/toyos-elf/tests/crafted.rs @@ -244,9 +244,6 @@ fn tls_alignment_is_a_power_of_two_within_a_page_or_it_is_refused() { } } -/// Absent TLS is `None`, never a zero `memsz`: a module with a `PT_TLS` of zero -/// size still gets a DTV slot, and telling the two apart is what the sentinel -/// could not do. #[test] fn a_zero_size_tls_segment_is_present_not_absent() { let with = Elf::honest(0x1000).ph(Phdr::tls(0, 0, 0, 8)).build(); diff --git a/toyos-elf/tests/fuzz.rs b/toyos-elf/tests/fuzz.rs index ac695dd989f..871ba3973b1 100644 --- a/toyos-elf/tests/fuzz.rs +++ b/toyos-elf/tests/fuzz.rs @@ -362,13 +362,12 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), let rules = Rules { extent, window, - sym_count: symbols.count(), fill: (case.mode == Mode::Exe).then_some(FillLattice { base: extent.min(), granule: 4096 }), tls, }; let mut relocs = Vec::new(); for raw in RelaTable::new(rela_bytes, case.machine).iter() { - if let Some(r) = rela::parse(raw, &rules).map_err(|_| ())? { + if let Some(r) = rela::parse(raw, &rules, symbols).map_err(|_| ())? { relocs.push(r); } } diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index ac5e1adf9b7..de9c183c1e5 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -32,11 +32,11 @@ fn validate( let rules = Rules { extent: extent(0, u64::MAX), window, - sym_count, fill, tls: Some(tls_segment(u64::MAX)), }; - entries.try_for_each(|r| rela::parse(r, &rules).map(|_| ())) + let syms = vec![0; sym_count * toyos_elf::sym::ENTRY_SIZE]; + entries.try_for_each(|r| rela::parse(r, &rules, SymTab::new(&syms, &[])).map(|_| ())) } /// `st_info` for a global `STT_FUNC`, and for the data object it is told apart @@ -330,6 +330,22 @@ fn a_symbol_index_past_the_table_is_refused_except_for_relative() { assert_eq!(validate(RelaTable::new(&relative, Machine::X86_64).iter(), window, 0, None), 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 syms = [0u8; 4 * 24 + 8]; + for raw in [6u32, 7, 16, 17, 18, 23] { + let parse = |r_sym| { + let entry = RelaTable::new(&rela(0x10, r_sym, raw, 0), Machine::X86_64).get(0).unwrap(); + rela::parse(entry, &rules, SymTab::new(&syms, &[])) + }; + assert_eq!(parse(4), Err(RelocError::SymbolPastTable), "type {raw}"); + assert!(parse(3).is_ok(), "type {raw}"); + } +} + #[test] fn counts_are_per_kind_over_every_table() { let a = [rela(0, 0, 8, 0), rela(8, 1, 6, 0), rela(16, 2, 18, 0)].concat(); @@ -736,10 +752,9 @@ fn each_machine_reads_its_own_relocation_numbers() { let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), - sym_count: 0, fill: None, tls: None, }; - let parsed = rela::parse(entry, &rules).unwrap().unwrap(); + let parsed = rela::parse(entry, &rules, SymTab::empty()).unwrap().unwrap(); assert_eq!((parsed.offset(), parsed.op()), (0x10, Op::Relative(rules.extent.offset(4).unwrap()))); } From 3975f8a46760652ced90c214c51ae735dc5ec146 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 20:52:05 +0200 Subject: [PATCH 09/10] review #544 round 6: one SymTab per module, exe included read_exe_tables built ExeTables and then re-derived its own SymTab from the same moved Vecs to parse against, pairing the two by inspection instead of by construction. Build ExeTables first and parse against exe.symbols(), the same accessor every later lookup uses, matching the library path. LoadedLib::sym_count had one caller, a log line; inline lib.symbols().count() there and delete the method. Co-Authored-By: Claude Opus 5.5 --- kernel/src/elf/mod.rs | 4 ---- kernel/src/loader/mod.rs | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 885f7911c79..7de691c4777 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -193,10 +193,6 @@ impl LoadedLib { unsafe { SymTab::new(syms.as_slice(), strs) } } - pub fn sym_count(&self) -> usize { - self.symbols().count() - } - fn gnu_hash(&self) -> Option> { // SAFETY: same bounds argument as `symbols` above. GnuHash::parse(unsafe { self.gnu_hash.as_ref()?.as_slice() }) diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 0d5e9685cb9..e97cb85c4e7 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -287,12 +287,24 @@ fn read_exe_tables( 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| { + let mut exe = ExeTables { + needed, + dynstr, + dynsym, + relas: elf::ParsedRelaEntries { + relative: Vec::new(), + glob_dat: Vec::new(), + tpoff64: Vec::new(), + tpoff32: Vec::new(), + }, + }; + let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules, exe.symbols()).map_err(|refused| { log!("spawn: {}: {}", path, refused.as_str()); refused.error() })?; + exe.relas = relas; - Ok(ExeTables { needed, dynstr, dynsym, relas }) + Ok(exe) } /// `.dynsym`'s entry count, from `.gnu.hash` if present, else the `DT_SYMTAB`–`DT_STRTAB` gap. @@ -747,7 +759,7 @@ fn load_needed_libs(exe: &ExeTables, path: &str) -> Result { let t_load1 = crate::clock::nanos_since_boot(); log!("dynamic: loaded {} base={:#x} ({} syms, {}ms)", - lib_name, lib.phys_base, lib.sym_count(), (t_load1 - t_load0) / 1_000_000); + lib_name, lib.phys_base, lib.symbols().count(), (t_load1 - t_load0) / 1_000_000); out.libs.push(elf::cache_loaded_lib(&lib_path, id, lib, rw_offset, rw_size)?); out.paths.push(lib_path); } From 2e2b896a8321126bc261261f1fcd88bdb5ff8bda Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 20:57:47 +0200 Subject: [PATCH 10/10] loader: one SymTab construction for the executable, without a half-built ExeTables Building ExeTables with an empty relocation set to borrow its table added a state that should not exist and twelve lines. Both tables come from the same two Vecs `ExeTables::symbols()` reads, so the direct construction already pairs parse and lookup. Co-Authored-By: Claude Opus 5.5 --- kernel/src/loader/mod.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index e97cb85c4e7..4e1230eacc1 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -287,24 +287,12 @@ fn read_exe_tables( fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), tls: layout.tls(), }; - let mut exe = ExeTables { - needed, - dynstr, - dynsym, - relas: elf::ParsedRelaEntries { - relative: Vec::new(), - glob_dat: Vec::new(), - tpoff64: Vec::new(), - tpoff32: Vec::new(), - }, - }; - let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules, exe.symbols()).map_err(|refused| { + 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() })?; - exe.relas = relas; - Ok(exe) + Ok(ExeTables { needed, dynstr, dynsym, relas }) } /// `.dynsym`'s entry count, from `.gnu.hash` if present, else the `DT_SYMTAB`–`DT_STRTAB` gap.