diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index bec945c9469..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::{RelaTable, RelocKind}; +use toyos_elf::{rela, ImageOffset, Op, RelaTable, SymTab}; 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,53 @@ 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), + fill: None, + tls: 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, 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()); + }; + // 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/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..404fc31d3ed --- /dev/null +++ b/issues/design-debt/elf-domain-lint-line-not-yet-added.md @@ -0,0 +1,20 @@ +--- +status: assigned +kind: track +opened: 2026-09-27 +--- + +# `toyos-elf` does not `forbid(clippy::arithmetic_side_effects)` yet + +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 +this crate rests on is enforced rather than reviewed. + +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/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..274e8ce8eb4 --- /dev/null +++ b/issues/kernel/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -0,0 +1,28 @@ +--- +status: assigned +kind: finding +opened: 2026-09-27 +--- + +# `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 +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. + +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/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 4bc7d919fdc..b7322d5b73b 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) @@ -74,15 +77,12 @@ struct Snapshot { dynsym: Option, dynstr: Option, tls_template: Option, - tls_memsz: usize, - tls_align: usize, 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, @@ -95,15 +95,12 @@ 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, - 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, @@ -124,16 +121,13 @@ 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, 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..3172aa721eb 100644 --- a/kernel/src/elf/index.rs +++ b/kernel/src/elf/index.rs @@ -7,66 +7,89 @@ 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, SymTab, 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, + symbols: SymTab<'_>, +) -> Result { let entries = || { RelaTable::new(rela_data, crate::arch::ELF_MACHINE) .iter() .chain(RelaTable::new(jmprel_data, crate::arch::ELF_MACHINE).iter()) }; let counts = RelaCounts::of(entries()); - // 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, symbols).map_err(Refused::Entry)? else { continue }; + match r.op() { + Op::Relative(target) => out.relative.push((r.offset(), target)), + Op::Bind(sym) => out.glob_dat.push((r.offset(), sym)), + Op::Tpoff64(t) => out.tpoff64.push((r.offset(), t)), + Op::Tpoff32(t) => out.tpoff32.push((r.offset(), t)), + Op::DtpMod64(_) | Op::DtpOff64(_) => {} } } Ok(out) diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 4972905fd77..7de691c4777 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, 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}; 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, SymIndex, TlsSegment}; /// `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"); @@ -92,18 +92,17 @@ 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, 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. @@ -194,20 +193,28 @@ 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() }) } - /// 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, self.symbols()).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 +227,80 @@ 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 { - 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 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 { + 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") +} + +/// 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] +#[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 +348,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 +382,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() }) } @@ -389,8 +428,16 @@ 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. + // 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 { Some(t) => Some(module.slice(t.vaddr, t.size)?), @@ -401,61 +448,54 @@ 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, + // A library's image is written contiguously, with no fill-page edge. + fill: None, + tls: layout.tls(), + }; + // Every entry is parsed here, and a refusal drops the image this pass has + // 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, 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. + 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 { - Some(tls) => ( - Some(module.slice(tls.vaddr, tls.filesz)?), - 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 tls_template = layout.tls().map(|tls| module.at(tls.template())); 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(( @@ -467,16 +507,13 @@ pub fn load_shared_lib( dynsym, dynstr, tls_template, - tls_memsz, - tls_align, rela, 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..d3e50048638 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 super::{relocated_symbol, CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; use crate::UserAddr; -use toyos_elf::RelocKind; +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. @@ -45,43 +50,46 @@ 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(|r| r.kind.is_bind()) - .map(|r| (r.offset, r.sym)) - }); - cached.into_iter().flatten().chain(scanned.into_iter().flatten()) - } - - fn typed_entries( - &self, - kind: RelocKind, - pick: fn(&CachedRelocs) -> &alloc::vec::Vec<(u64, u32, i64)>, - ) -> 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 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(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 bind_entries(&self) -> impl Iterator + '_ { + self.entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }, |c| &c.bind) + } + + fn dtpmod_entries(&self) -> impl Iterator)> + '_ { + 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. + 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 +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 as usize); + 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. @@ -119,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 as usize); + let name = relocated_symbol(symbols, sym).name_in(symbols.strings()); let resolved = exe_sym_map .get(name) .copied() @@ -134,24 +142,43 @@ 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, + }; + 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) { + tpoff(r)?; + } + for (_, r) in lib.entries(tpoff32, |c| &c.tpoff32) { + tpoff32_value(tpoff(r)?)?; + } + 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.entries(tpoff64, |c| &c.tpoff64) { + let value = tpoff(r)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff as u64) }; + unsafe { lib.write_at::(offset, value) }; 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.entries(tpoff32, |c| &c.tpoff32) { + let value = tpoff32_value(tpoff(r)?)?; // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff as i32) }; + unsafe { lib.write_at::(offset, value) }; count32 += 1; } if count64 > 0 || count32 > 0 { @@ -160,63 +187,84 @@ pub fn apply_tpoff_relocs( count64, count32, lib_base_offset, tls.total_memsz() ); } + Ok(()) } -/// Apply `R_X86_64_DTPMOD64` and `R_X86_64_DTPOFF64`: the general-dynamic TLS -/// model resolved through the DTV. +/// 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`: 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, + }; + let resolve = |r| resolve_tls_ref(r, 0, lib.tls(), lib.symbols(), tls_info); + for (_, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { + resolve(r)?; + } + let mut count = 0u64; + for (offset, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { + let value = resolve(r)?.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`, 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().and_then(TlsSegment::occupied) else { continue; - } - if let Some(sym_offset) = 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_offset)); + return Some((module, segment, 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()) { + let named = relocated_symbol(symbols, sym); + if named.is_defined() { return self_module_id; } - let name = symbols.name(r_sym as usize); + let name = named.name_in(symbols.strings()); 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 @@ -224,49 +272,51 @@ 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; - } - 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; +/// `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_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. +fn resolve_tls_ref( + r: TlsRef, + own_base_offset: usize, + 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 = 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)?; + return Ok(Some((own_base_offset, at))); } - let name = symbols.name(r_sym as usize); + let name = sym.name_in(symbols.strings()); match defining_module(name, tls_info) { - Some((_, sym_offset)) => sym_offset as i64 + r_addend, + Some((module, segment, defined)) => { + let at = defined.tls_offset(s.addend(), segment).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. -fn compute_tpoff( - lib: &LoadedLib, - r_sym: u32, - r_addend: i64, - lib_base_offset: usize, +/// `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, + own_tls: Option, + symbols: SymTab<'_>, 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_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 8dc2109c481..4e1230eacc1 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -17,8 +17,8 @@ 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}; +pub(crate) use tls::rebase_window; use alloc::string::String; use alloc::sync::Arc; @@ -39,7 +39,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, TlsSegment}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -112,7 +113,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 +124,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 +144,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 +171,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 @@ -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: u32) -> Option { - let off = self - .symtab_file_off? - .checked_add(r_sym as u64 * 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. @@ -245,9 +234,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 +251,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,14 +261,10 @@ fn read_exe_tables( } None => Vec::new(), }; - let relas = elf::parse_rela_entries(&rela_data, &jmprel_data).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, - } - })?; - + let symtab_file_off = match dyn_info.symtab { + Some(vaddr) => Some(file_off(layout, path, "DT_SYMTAB", vaddr)?), + None => None, + }; let dynstr = match dyn_info.strtab_table() { Some(t) => { let off = file_off(layout, path, "DT_STRTAB", t.vaddr)?; @@ -287,18 +272,27 @@ 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) { + 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(), }; - Ok(ExeTables { needed, dynstr, dynsym, symtab_file_off, relas }) + // Parsed like a library's but for the window and the fill page: the + // executable's writes land anywhere in its own image, one demand-fault page + // at a time, so a crossing write is refused, not silently dropped. + let extent = layout.extent(); + let rules = Rules { + extent, + window: (extent.min(), extent.max()), + fill: Some(FillLattice { base: extent.min(), granule: FILL_GRANULE }), + tls: layout.tls(), + }; + let relas = elf::parse_rela_entries(&rela_data, &jmprel_data, &rules, SymTab::new(&dynsym, &dynstr)).map_err(|refused| { + log!("spawn: {}: {}", path, refused.as_str()); + refused.error() + })?; + + Ok(ExeTables { needed, dynstr, dynsym, relas }) } /// `.dynsym`'s entry count, from `.gnu.hash` if present, else the `DT_SYMTAB`–`DT_STRTAB` gap. @@ -335,7 +329,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 +386,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 +409,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 +424,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 +447,24 @@ 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 = 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), @@ -514,21 +495,24 @@ pub fn spawn( }); } - let exe_tls_template = match layout.tls.filter(|t| t.memsz > 0) { + let exe_tls_template = match layout.tls().and_then(TlsSegment::occupied) { 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 +531,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, &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 { @@ -559,20 +547,21 @@ 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()); }; - 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 +593,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(), @@ -757,7 +747,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); } @@ -795,21 +785,22 @@ fn map_libs( /// because its pages do not exist yet. 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 +809,15 @@ 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| 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); } - 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); - } -} - -/// 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. -#[allow(clippy::too_many_arguments)] -fn exe_tpoff( - exe: &ExeTables, - backing: &dyn crate::file_backing::FileBacking, - r_sym: u32, - r_addend: i64, - 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 - } + for &(r_offset, r) in &exe.relas.tpoff32 { + reloc_index.add_i32(r_offset, elf::tpoff32_value(exe_tpoff(r)?)?); } + Ok(()) } /// 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..b91085263dd 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -1,13 +1,16 @@ -//! 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; +use toyos_elf::{Layout, TlsSegment}; /// This machine's TLS layout. pub const VARIANT: Variant = Variant::of(crate::arch::ELF_MACHINE); @@ -22,30 +25,49 @@ 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| { + 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) } + })?; + 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 +87,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 +100,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,58 +125,101 @@ 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); } } } } -/// 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, - 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)? - }; +/// `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}; - 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); + 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()); } - Some((crate::process::MappedPages::new(vaddr, alloc), 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,25 +232,27 @@ 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().and_then(TlsSegment::occupied) { 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 - .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()?.occupied()?))) + }; + 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/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..b89fa88ae70 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,49 @@ 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. +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 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, 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 +488,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 +508,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 +873,22 @@ 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); + 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); - 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 +927,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 0564783f726..43a646cbb62 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -262,11 +262,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 { @@ -287,25 +286,36 @@ 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().and_then(toyos_elf::TlsSegment::occupied); let data_arc = process::process_data(); let init_info = { 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_tls.and_then(|_| crate::elf::apply_dtpoff_relocs(&lib, &tls_info).err()) + }); + 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 @@ -336,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 { @@ -346,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, @@ -424,8 +434,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; @@ -474,11 +483,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, @@ -496,10 +505,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/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index 88f16bd3331..5e7b540b16e 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; @@ -539,9 +540,20 @@ 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(); + // 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(); @@ -668,6 +680,119 @@ 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), + ); + 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) }; @@ -681,8 +806,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"); @@ -703,24 +828,125 @@ fn f13_cross_module_addend_is_kept() { drop(lib_defs); } -/// Defines `xtls` (`STT_TLS`, offset 8) for a cross-module `TPOFF64`. -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: 0x20, 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() } -/// Two `TPOFF64` relocations against the undefined `xtls`, addends 0 and +/// 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, the module's own symbol: `apply_tpoff_relocs` refuses, and the + // mapping guard takes the library back down. + dlopen_refused("tls_apply_refs.so", &so_tls_ref_past_segment()); + + // spawn, the executable's own symbol: `apply_tls_relocs` refuses. + spawn_refused("tls_apply_spawn", &exe_tls_ref_past_segment()); + + // 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); + + // `S + A` (8 + `i64::MAX`) inside a `PT_TLS` declared past 2^63 bytes: only + // `S + A - tp` leaves an `i64`. + const PAST_I64: u64 = 0x8000_0000_0000_0010; + let dep = "tpoff_overflow_dep.so"; + write_file(dep, &tls_defs_so(b"wtls", PAST_I64)); + // `.dynstr` at 0x1800 holds "wtls\0\0". + let exe = exe_with(&[(DT_NEEDED, 6)], &[(0x2000, (1u64 << 32) | R_X86_64_TPOFF64, i64::MAX)], None) + .sym(0x1418, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) + .poke(0x1801, b"wtls\0") + .poke(0x1806, dep.as_bytes()); + 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 +/// 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)); + // 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, (469u64 << 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 +/// `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 `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)) @@ -730,11 +956,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 new file mode 100644 index 00000000000..dfda028af3e --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/tls_dtv_race.rs @@ -0,0 +1,138 @@ +//! 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. +//! +//! 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. +//! +//! 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. + +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; +/// 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 stores into. +static TARGET: AtomicU64 = AtomicU64::new(0); +/// The current worker has 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); +/// Rounds in which the sibling found the block mapped and stored into it. +static ENGAGED: AtomicU64 = AtomicU64::new(0); + +thread_local! { + /// A datum in this program's static TLS, which lies in the thread's block. + static ANCHOR: u8 = const { 0 }; +} + +/// 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 `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 { + // 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) { + 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 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) { + // 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) }; + } + } + } +} + +fn main() { + let sib = std::thread::Builder::new() + .name("dtv-sibling".into()) + .spawn(sibling) + .expect("spawn sibling"); + + 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; + + for round in 0..ROUNDS { + READY.store(false, SeqCst); + GO_EXIT.store(false, SeqCst); + 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 tid = unsafe { syscall::thread_spawn(entry, top, arg, base) }; + assert!(syscall::SyscallError::from_u64(tid).is_none(), "thread_spawn failed: {tid}"); + 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); + until("the sibling standing down", || PAUSED_ACK.load(SeqCst)); + GO_EXIT.store(true, SeqCst); + assert_eq!(syscall::thread_join(tid), 0, "round {round}: join"); + } + + STOP.store(true, SeqCst); + sib.join().expect("join sibling"); + 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 1e413c4835a..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", @@ -1535,6 +1538,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 +1658,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"]), @@ -3492,12 +3499,51 @@ 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, } } +/// `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 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 { + use toyos_elf::RelocError; + let mut ok = check_rust_result(result); + let log = format!("{}{}", result.before, result.serial); + 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 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 {reason:?}{}", + kernel_account(result) + ); + ok = false; + } + } + 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"; @@ -11609,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. 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..c51bab1660e 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,100 @@ 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. 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; + } + 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 +129,112 @@ 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. -#[derive(Clone, Copy, Debug)] +/// `align` is zero or a power of two no larger than [`MAX_TLS_ALIGN`], and the +/// file-backed template lies inside the image. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] 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 + } + + /// 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. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DynamicSegment { + file_offset: u64, + 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. @@ -118,33 +268,27 @@ 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, } impl Layout { @@ -160,13 +304,6 @@ 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 segment_count = 0usize; let mut vaddr_min = u64::MAX; let mut vaddr_max = 0u64; @@ -174,7 +311,10 @@ impl Layout { let mut dynamic = None; let mut eh_frame_hdr = None; - for i in 0..ehdr.phnum as usize { + // 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); }; @@ -190,107 +330,138 @@ impl Layout { if phdr.offset.checked_add(phdr.filesz).is_none() { return Err(Error::FileExtentOverflows); } - if segment_count == MAX_LOAD_SEGMENTS { + 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); - segments[segment_count] = Segment { - vaddr: phdr.vaddr, - memsz: phdr.memsz, - filesz: phdr.filesz, - file_offset: phdr.offset, - flags: SegmentFlags(phdr.flags), - }; - segment_count += 1; } // 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]; + 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(phdr.vaddr, phdr.memsz).ok_or(Error::SegmentExtentOverflows)?; + segments[placed] = Segment { + image, + filesz: phdr.filesz, + file_offset: phdr.offset, + flags: SegmentFlags(phdr.flags), + }; + placed = placed.wrapping_add(1); } - 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() + } + + /// `e_entry`: inside the image, with at least one byte after it. + pub const fn entry(&self) -> ImageOffset { + self.entry + } + + pub const fn tls(&self) -> Option { + self.tls + } + + pub const fn dynamic(&self) -> Option { + self.dynamic } - /// 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) + pub const fn section_headers(&self) -> Option { + self.section_headers } - /// The writable window `[lo, hi)` in image-relative coordinates, or `None` - /// when no segment is writable. + /// `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 +472,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 +490,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 +509,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 +545,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 +566,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..dd1fd07f54e 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, TlsSegment}; use crate::read; +use crate::sym::{SymIndex, SymTab}; +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,149 @@ 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), + /// `Some` for a chunked writer (the exe), which drops a write crossing a + /// fill page and so has it refused; `None` for a contiguous one. + pub fill: Option, + /// The module's own `PT_TLS`, which a TLS relocation with `r_sym == 0` + /// offsets into; `None` for a module with none. + pub tls: Option, +} + +/// 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 + } + + /// 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. +/// +/// 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, symbols: SymTab<'_>) -> 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, symbols.count()).ok_or(RelocError::SymbolPastTable); + let tls = || -> Result { + if rela.sym != 0 { + return Ok(TlsRef::Symbol(TlsSymRef { sym: sym()?, addend: rela.addend })); + } + rules + .tls + .and_then(|segment| TlsOffset::of(0, rela.addend, segment)) + .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 +421,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 +440,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 +463,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..ede60cbfeb6 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -1,12 +1,14 @@ //! `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`]). +use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; +use crate::tls::TlsOffset; +use crate::Error; /// Bytes in one `Elf64_Sym`. pub const ENTRY_SIZE: usize = 24; @@ -16,13 +18,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 +65,26 @@ 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 `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) + } } /// `.dynsym` (or `.symtab`) paired with the string table its names live in. @@ -87,14 +128,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 +149,39 @@ 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 + /// 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: Option) -> Result<(), Error> { + for (_, sym) in self.defined() { + if sym.kind() == STT_TLS { + tls.and_then(|segment| sym.tls_offset(0, segment)) + .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 +225,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 +233,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 +250,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..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)] @@ -126,14 +127,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 +148,27 @@ impl Static { } } +/// `S + A` inside one module's TLS segment, `0..=p_memsz`: made only by +/// [`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 `segment`. + pub(crate) const fn of(value: u64, addend: i64, segment: TlsSegment) -> Option { + match value.checked_add_signed(addend) { + Some(at) if at <= segment.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/common/mod.rs b/toyos-elf/tests/common/mod.rs index 6f3817659c5..7013e085802 100644 --- a/toyos-elf/tests/common/mod.rs +++ b/toyos-elf/tests/common/mod.rs @@ -184,6 +184,30 @@ 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() +} + +/// 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/crafted.rs b/toyos-elf/tests/crafted.rs index f34f95ec422..baff9336062 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,18 +240,21 @@ 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}"); } } -/// 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(); - 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()); +} + +#[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 ───────────────────────── @@ -260,13 +263,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 +368,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..871ba3973b1 --- /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. 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, TlsSegment}; + +/// 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(bytes: &[u8], range: ImageRange) -> &[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 = 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 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()); + } + } + + 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, + 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, symbols).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 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(|_| ())?; + 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) => tls.and_then(|t| d.tls_offset(s.addend(), t)).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..6da7bdaa5a9 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..de9c183c1e5 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -15,11 +15,30 @@ 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; +/// 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(0, u64::MAX), + window, + fill, + tls: Some(tls_segment(u64::MAX)), + }; + 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 /// from. const FUNC: u8 = (1 << 4) | 2; @@ -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,36 @@ 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(())); +} + +/// 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] @@ -364,7 +420,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, 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); } @@ -691,5 +748,13 @@ 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(0, 0x100), + window: (0, 0x100), + fill: None, + tls: None, + }; + let parsed = rela::parse(entry, &rules, SymTab::empty()).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..566f76f2f64 100644 --- a/toyos-elf/tests/tls.rs +++ b/toyos-elf/tests/tls.rs @@ -5,12 +5,32 @@ //! 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}; +#[allow(dead_code)] +mod common; + +use common::tls_segment; +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 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, tls_segment(memsz)) +} + +/// A datum `off` bytes into the largest segment a file can declare. +fn datum(off: u64) -> TlsOffset { + tls_offset(off, 0, u64::MAX).unwrap() +} + #[test] fn the_dtv_is_never_overwritten_by_tls_data() { let sizes = [ @@ -105,16 +125,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) = tls_offset(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!(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 = 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!(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 /// 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 +177,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 +194,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 +224,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); }