diff --git a/issues/a-refused-syscall-writes-a-log-record-per-call-and-nothing-bounds-the-caller.md b/issues/a-refused-syscall-writes-a-log-record-per-call-and-nothing-bounds-the-caller.md index 2e65ed3aa2..641aa6ecba 100644 --- a/issues/a-refused-syscall-writes-a-log-record-per-call-and-nothing-bounds-the-caller.md +++ b/issues/a-refused-syscall-writes-a-log-record-per-call-and-nothing-bounds-the-caller.md @@ -9,16 +9,13 @@ opened: 2026-10-08 A refusal the kernel answers with an error is also a kernel log record, once per call, at every one of these sites, and the caller chooses how often: -- `sys_dlopen` (`kernel/src/syscall/vm.rs`): a path that does not open - (`dlopen: : `), a cached image whose file changed, an image - `elf::load_shared_lib` refuses, no virtual address space left, and a TLS - reference that leaves its module's segment; -- `elf::cache_loaded_lib` (`kernel/src/elf/cache.rs`): a load that would take - the shared-object cache past its budget; +- `sys_dlopen` (`kernel/src/syscall/vm.rs`): an image + `elf::load_shared_lib` refuses (`dlopen: : `), no virtual + address space left, and a TLS reference that leaves its module's segment; - a refused spawn (`kernel/src/loader/mod.rs`): every `spawn: : …` record, one for each reason a file can be refused. -`dlopen` of a missing path in a loop, or a spawn of a file that is not an +`dlopen` of a refused image in a loop, or a spawn of a file that is not an executable, is a record per syscall from any program, into a log every program shares: `logkeeper` keeps sixteen megabytes of a boot (`issues/a-t14-boot-that-outlogs-its-retention-loses-its-middle-and-the-rows-whose-lines-sat-there.md`), diff --git a/issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md b/issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md index 274e8ce8eb..e262d69009 100644 --- a/issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md +++ b/issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md @@ -11,13 +11,11 @@ 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. +`write_at` — so a peer thread of the same process can be reading 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 +The writes go to the module's one freshly allocated `PageAlloc`, which the +peer has no address of yet, so the race is benign in practice. 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. diff --git a/issues/the-capability-end-state-is-twelve-answers.md b/issues/the-capability-end-state-is-twelve-answers.md index 2891267dfb..99b4c3ee73 100644 --- a/issues/the-capability-end-state-is-twelve-answers.md +++ b/issues/the-capability-end-state-is-twelve-answers.md @@ -156,8 +156,10 @@ table** can still reach: `SYS_SHM_CREATE`, `SYS_INBOX_SETUP`; - **and four that reach past the process** — the whole filesystem by path (`SYS_OPEN`, `SYS_READDIR`, `SYS_DELETE`, `SYS_MKDIR`, `SYS_RMDIR`, - `SYS_RENAME`, `SYS_SYMLINK`, `SYS_READLINK`), `SYS_DLOPEN`/`SYS_DLSYM` over - the same paths, `SYS_SPAWN`, and `SYS_SHUTDOWN` — plus `SYS_SYSINFO`'s census. + `SYS_RENAME`, `SYS_SYMLINK`, `SYS_READLINK`), `SYS_SPAWN`, and + `SYS_SHUTDOWN` — plus `SYS_SYSINFO`'s census. `SYS_DLOPEN` was among them + and is not now: it loads an image the caller read through its own + namespace, and opens nothing by path. **Smallest decision:** the last group. **Recommendation:** keep the filesystem, `SYS_DLOPEN` and `SYS_SPAWN` ambient under question 2's ruling; make diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 573a4c9a11..b5642a446a 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -118,12 +118,14 @@ Beside that order, each waiting on something outside it: are deleted. 5. **Spawn and dlopen from an image handle.** **Exit**: a program in `/apps` runs having been read by userland, its faults served from the - image object and not the VFS. **Partly done** (#536): spawn - (`SpawnArgs::image`). **Left**: dlopen. `SYS_DLOPEN` opens a path the - kernel serves, so no library on a file server loads - (`issues/a-package-cannot-ship-its-own-libraries.md`). Its exit: a - library in `/apps` loads having been read by userland, its faults - served from the image object, and `SYS_DLOPEN` takes no path. + image object and not the VFS; and `SYS_DLOPEN` takes no path. + **Done**: spawn (#536, `SpawnArgs::image`), and dlopen: libc's and the + SDK's `dlopen` read the library through `toyos::files::read_image` + wherever it is served, `SYS_DLOPEN` loads it from the image handle + (`DlopenArgs`) under a name it opens nothing by, and the kernel's + cross-process library cache is deleted, each process holding its own + library pages (orchestrator, under the memory design, whose later + stages bring sharing back through memory objects). 6. **fileserver for DATA over diskserver.** **Done** (#536). #660 cut its guest tests and #890 keeps them cut. `apps_and_home_are_one_filesystem`'s claim, `/apps` and `/home` one diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index ca12bda0d2..f95d7df37b 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -46,7 +46,7 @@ artifacts this tree builds. **It has a deadline.** Nothing shipped is dynamically linked today, so the move is pure deletion. The day an image ships a hosted rustc, a very large shared object is -dlopened into a kernel whose cache never evicts, and every one of those bounds +dlopened by the kernel's loader, and every one of those bounds becomes load-bearing at once. Do it after the completion architecture lands and before that day. Independent of everything else; may run as soon as a slot frees. diff --git a/issues/the-so-caches-refusals-are-narrower-than-its-reach.md b/issues/the-so-caches-refusals-are-narrower-than-its-reach.md deleted file mode 100644 index cb111115ab..0000000000 --- a/issues/the-so-caches-refusals-are-narrower-than-its-reach.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-03 ---- - -# The shared-object cache's two refusals hold over less than the cache reaches - -`kernel/src/elf/cache.rs` now refuses a path whose file changed and a load past -its byte budget, which closed the record that tracked the cache's lack of both. -Three things that record carried are not covered by either refusal, and they -went out of the tracker with it. They are one file because they are one -residual: what the refusals do **not** reach. - -## The identity cannot see a same-size rewrite on an undated machine - -`vfs::BackingId` is size plus the mount's mtime, and `/tmp` is the one writable -mount a kernel `dlopen` reaches. On a machine whose RTC never answered every -write stamps 0 (`kernel/src/clock.rs`'s `mtime_now`), so there a same-size -rewrite of a library on `/tmp` carries the identity it had. - -*Exit condition:* an identity that does not rest on a clock — a content hash, a -per-file generation the mount bumps on every write, or a `FileId` plus a write -counter — with a test that rewrites a library at the same size with every -write stamped 0 and loads the second image. - -## The budget is a machine-wide, boot-permanent denial - -256 MiB over every cached image, and nothing is ever evicted, so the bytes an -unprivileged process spends on distinct `dlopen` paths are gone for the boot and -the cache is closed to every later loader — including `/system/bin/init`'s. It is -bounded, unlike `no-physical-memory-fairness.md`'s unbounded case, and that is -the difference: the memory comes back at reboot and not before. - -This was the ruling's choice, not an oversight — eviction of an image that is -mapped into every process that loaded it needs a lifetime rule nothing here has. - -*Exit condition:* the Ring-3 loader move deletes this cache, or the lifetime -track gives a mapped image a rule that lets one be taken back. - -## The key is the caller's spelling, so an alias walks past the refusal - -`Vfs::resolve_absolute` (`kernel/src/vfs.rs`) normalises `.` and `..` and -resolves no symlink, so one file under two spellings is two entries and two -physical images. The base had the same key, so this is not a regression — but -the refusal is now **per spelling**, which is worse than the key alone: a -rewritten library refused under one name loads fresh under an alias, and the -double-map the refusal exists to prevent happens anyway to a caller that knows -the second name. - -*Exit condition:* the cache is keyed by what the mount identifies the file as -(`FileId`) rather than by the string the caller wrote. diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index 1deddb2998..16b1142aa4 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -509,7 +509,7 @@ impl AddressSpace { detached } - /// Discharges only in this address space — a caller whose mapping may be live elsewhere (e.g. `sys_dlopen`) must shoot down itself. + /// Discharges only in this address space — a caller whose mapping may be live elsewhere must shoot down itself. pub fn remap(&mut self, vaddr: UserAddr, phys: u64, prot: Prot) { let va = vaddr.raw(); assert!( diff --git a/kernel/src/elf/cache.rs b/kernel/src/elf/cache.rs deleted file mode 100644 index 2f9b5bc520..0000000000 --- a/kernel/src/elf/cache.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! The shared-object cache: one image in memory, one private writable window -//! per process. -//! -//! A cached module's read-only pages are mapped into every process that loads -//! it and its base address never moves, so its `R_X86_64_RELATIVE` relocations -//! need no rework. Only the writable window is copied. -//! -//! **Nothing is ever removed, and both refusals follow from that**, as does -//! `clone_from_cache`'s SAFETY: no address space is reachable from here, so -//! neither a changed file nor a full budget can be answered by taking an entry -//! back, and both are refused instead. - -use alloc::string::String; -use alloc::vec::Vec; -use toyos_abi::syscall::SyscallError; - -use super::{LibMemory, LoadedLib}; -use crate::mm::{KernelSlice, MAX_HEAP_ALLOC}; -use crate::process::PageAlloc; -use crate::sync::Lock; -use crate::vfs::BackingId; -use crate::UserAddr; -use toyos_elf::dynamic::InitArray; -use toyos_elf::rela::Rules; -use toyos_elf::{ImageRange, Op, ProgramHeaderTable, RelaCounts, RelocKind, SymIndex, TlsRef}; - -/// 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`. - 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, 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.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]; - if counts.max_of(&kept).checked_mul(widest).is_none_or(|b| b > MAX_HEAP_ALLOC) { - return None; - } - // Capacities are reserved exactly from `counts`; growing them could allocate past the bound just checked. - let mut relocs = CachedRelocs { - bind: Vec::with_capacity(counts.bind), - tpoff64: Vec::with_capacity(counts.tpoff64), - tpoff32: Vec::with_capacity(counts.tpoff32), - dtpmod64: Vec::with_capacity(counts.dtpmod64), - dtpoff64: Vec::with_capacity(counts.dtpoff64), - }; - for r in lib.relocations() { - 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) -} - -// Fields identical between the cache entry and every clone: only memory ownership, user base and relocations differ. -#[derive(Clone, Copy)] -struct Snapshot { - image: KernelSlice, - dynsym: Option, - dynstr: Option, - tls_template: Option, - rela: Option, - jmprel: Option, - gnu_hash: Option, - rules: Rules, - eh_frame_hdr: Option, - phdrs: ProgramHeaderTable, - init_array: Option, - span: u64, - rw_lo: u64, - rw_hi: u64, -} - -impl Snapshot { - fn of(lib: &LoadedLib) -> Snapshot { - Snapshot { - image: lib.image, - dynsym: lib.dynsym, - dynstr: lib.dynstr, - tls_template: lib.tls_template, - rela: lib.rela, - jmprel: lib.jmprel, - gnu_hash: lib.gnu_hash, - rules: lib.rules, - eh_frame_hdr: lib.eh_frame_hdr, - phdrs: lib.phdrs, - init_array: lib.init_array, - span: lib.span, - rw_lo: lib.rw_lo, - rw_hi: lib.rw_hi, - } - } - - fn into_lib( - self, - memory: LibMemory, - user_base: UserAddr, - cached_relocs: Option, - ) -> LoadedLib { - LoadedLib { - memory, - user_base, - image: self.image, - dynsym: self.dynsym, - dynstr: self.dynstr, - tls_template: self.tls_template, - rela: self.rela, - jmprel: self.jmprel, - gnu_hash: self.gnu_hash, - cached_relocs, - rules: self.rules, - eh_frame_hdr: self.eh_frame_hdr, - phdrs: self.phdrs, - init_array: self.init_array, - span: self.span, - rw_lo: self.rw_lo, - rw_hi: self.rw_hi, - } - } -} - -/// An immortal image, used as the template every later load clones from. -struct CachedLib { - alloc: PageAlloc, - snapshot: Snapshot, - rw_offset: usize, - rw_size: usize, - relocs: CachedRelocs, - /// The file this image was built from, as the mount described it at insert. - id: BackingId, -} - -static SO_CACHE: Lock> = Lock::new(Vec::new()); - -/// The most physical memory every cached image may hold between them. **A policy -/// number**: nothing derives it. For scale, the largest shared object this tree -/// builds loads a span of 144,760,832 bytes — a 146,800,640-byte allocation, so -/// this admits one of those and refuses a second. -const BUDGET_BYTES: usize = 256 * 1024 * 1024; - -/// Every cached image's allocation, summed. The caller holds the lock. -fn held_bytes(cache: &[(String, CachedLib)]) -> usize { - cache.iter().map(|(_, c)| c.alloc.size()).sum() -} - -/// What the cache holds for `path`, judged against the file `id` came from. -/// **The lookup and the publish-time recheck both ask here**, so the two cannot -/// disagree about what matches: a recheck comparing less than `id` would hand a -/// loader whose open straddled a rewrite an image of a file it never opened. -fn entry_for<'a>( - cache: &'a [(String, CachedLib)], - path: &str, - id: BackingId, -) -> Result, SyscallError> { - let Some(idx) = cache.iter().position(|(p, _)| p == path) else { - return Ok(None); - }; - let entry = &cache[idx].1; - if entry.id != id { - return Err(SyscallError::NotSupported); - } - Ok(Some(entry)) -} - -/// Takes ownership of `lib` and returns a clone in `Shared` mode with a private writable window; returns it unchanged if it cannot be cached. -/// `Err` is the budget alone: an image that merely cannot be cached still works. -pub fn cache_loaded_lib( - path: &str, - id: BackingId, - lib: LoadedLib, - rw_offset: usize, - rw_size: usize, -) -> Result { - if !matches!(lib.memory, LibMemory::Owned(_)) { - return Ok(lib); - } - let snapshot = Snapshot::of(&lib); - let user_base = lib.user_base; - // Must scan before `lib.memory` moves out: the scan reads the tables through `lib`. - let scanned = prescan_relocs(&lib); - let LibMemory::Owned(alloc) = lib.memory else { - unreachable!("the check above established this") - }; - - // A lib without prescanned relocs keeps the scan-every-table path: the cache always stores what `cached_relocs` describes. - let owned = |alloc| snapshot.into_lib(LibMemory::Owned(alloc), user_base, None); - let Some(relocs) = scanned else { - return Ok(owned(alloc)); - }; - let Some(rw_alloc) = PageAlloc::new(rw_size) else { - return Ok(owned(alloc)); - }; - let alloc_ptr = alloc.ptr(); - // SAFETY: `rw_offset`/`rw_size` are `load_shared_lib`'s validated window, so `alloc_ptr.add(rw_offset)` stays inside `alloc`; `rw_alloc` is a fresh, distinct allocation, so the ranges cannot overlap. - unsafe { - core::ptr::copy_nonoverlapping(alloc_ptr.add(rw_offset), rw_alloc.ptr(), rw_size); - } - let rw_delta = rw_alloc.ptr() as i64 - (alloc_ptr as i64 + rw_offset as i64); - - let mut cache = SO_CACHE.lock(); - // Asked again under the lock that publishes: `try_clone_cached` released it - // before the load. Entries are never removed, so a second one for a name would - // strand a whole library forever — the loser clones the winner's instead, and - // is refused when the winner's file is not the one this caller opened. - let published = match entry_for(&cache, path, id) { - Err(e) => Some(Err(e)), - Ok(Some(entry)) => Some(Ok(clone_from_cache(entry))), - Ok(None) => None, - }; - if let Some(outcome) = published { - drop(cache); - return outcome.map(|cloned| cloned.unwrap_or_else(|| owned(alloc))); - } - // Under the lock that publishes: two concurrent loads must not both find room. - let (held, entries) = (held_bytes(&cache), cache.len()); - if held.checked_add(alloc.size()).is_none_or(|after| after > BUDGET_BYTES) { - drop(cache); - // Both allocations drop here, so the refusal gives back what the load took. - log!( - "dlopen: {} would take the shared-object cache to {} bytes over {} entries, past its \ - {}-byte budget; refused, and nothing is evicted for it", - path, held.saturating_add(alloc.size()), entries + 1, BUDGET_BYTES - ); - return Err(SyscallError::ResourceExhausted); - } - cache.push(( - String::from(path), - CachedLib { alloc, snapshot, rw_offset, rw_size, relocs: relocs.clone(), id }, - )); - drop(cache); - - Ok(snapshot.into_lib( - LibMemory::Shared { - rw_alloc, - cached_image: snapshot.image, - rw_offset, - rw_delta, - }, - user_base, - Some(relocs), - )) -} - -/// Clone what is cached under `path`, if `id` still describes the file it came -/// from. `Ok(None)` is nothing usable — no entry, or a clone that found no -/// memory — and the caller's own load path answers it. `Err` is the refusal: -/// the file changed under an image no address space here can take back, so a -/// reload would map the library twice. -pub fn try_clone_cached( - path: &str, - id: BackingId, -) -> Result, SyscallError> { - let cache = SO_CACHE.lock(); - Ok(entry_for(&cache, path, id)?.and_then(clone_from_cache)) -} - -// Base address stays the cache's: `RELATIVE` relocations need no fixup until spawn/dlopen assigns a user address. -fn clone_from_cache(cached: &CachedLib) -> Option { - - let rw_alloc = PageAlloc::new(cached.rw_size)?; - // SAFETY: `rw_offset + rw_size` was validated inside `cached.alloc` when this `CachedLib` was built; `CachedLib` is immortal once cached, so `cached.alloc` is still live. - let src = unsafe { cached.alloc.ptr().add(cached.rw_offset) }; - // SAFETY: `src` is valid for `cached.rw_size` bytes per the `SAFETY` above; `rw_alloc` is a fresh, distinct allocation, so the ranges cannot overlap. - unsafe { - core::ptr::copy_nonoverlapping(src, rw_alloc.ptr(), cached.rw_size); - } - - let rw_delta = rw_alloc.ptr() as i64 - (cached.alloc.ptr() as i64 + cached.rw_offset as i64); - let image = cached.snapshot.image; - let phys_base = image.phys(); - - Some(cached.snapshot.into_lib( - LibMemory::Shared { - rw_alloc, - cached_image: image, - rw_offset: cached.rw_offset, - rw_delta, - }, - UserAddr::new(phys_base), - Some(cached.relocs.clone()), - )) -} diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index b74935f96a..9e86403e8e 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -10,10 +10,8 @@ // this `warn` is what turns a missing one into a CI failure. #![warn(clippy::undocumented_unsafe_blocks)] -mod cache; mod reloc; -pub use cache::{cache_loaded_lib, try_clone_cached, CachedRelocs}; pub use reloc::{ apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, rebase_relative_relocs, resolve_dlopen_relocs, @@ -46,21 +44,6 @@ pub fn parse_layout(data: &[u8]) -> Result { Ok(layout) } -/// Ownership model for a loaded shared library's memory. -pub enum LibMemory { - /// Fresh load: one allocation owns everything. - Owned(PageAlloc), - /// Cloned from cache: read-only pages are shared, writable pages are private. - Shared { - rw_alloc: PageAlloc, - cached_image: KernelSlice, - /// 2 MiB-aligned offset of the private writable region in the cached image. - rw_offset: usize, - /// Added to a cached address to reach the private writable copy. - rw_delta: i64, - }, -} - /// One TLS module's placement within a thread's combined TLS block. #[derive(Clone)] pub struct TlsModule { @@ -83,7 +66,8 @@ pub struct TlsModuleInfo<'a> { } pub struct LoadedLib { - pub memory: LibMemory, + /// The one allocation the module's image is loaded into, this process's own. + pub memory: PageAlloc, pub user_base: UserAddr, /// Bounds-checked view of the whole loaded image. pub image: KernelSlice, @@ -94,7 +78,6 @@ pub struct LoadedLib { rela: Option, jmprel: Option, gnu_hash: Option, - cached_relocs: Option, /// 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, @@ -107,8 +90,8 @@ pub struct LoadedLib { /// Bytes between the image's lowest and highest virtual address. pub span: u64, /// Exact (unrounded) writable range within the image; `(span, span)` if none. - /// Rounding this outward, unlike `rw_offset`/`rw_size`, would make a page of - /// some other module's `.text` writable. + /// Rounding this outward would make a page of some other module's `.text` + /// writable. pub rw_lo: u64, pub rw_hi: u64, } @@ -130,21 +113,11 @@ impl LoadedLib { /// Give this module a virtual address in `pt` and map its pages there. /// Maps each window once, at its final protection — mapping the whole - /// image writable first would leave `.text` writable in every process. - /// A `Shared` module's split window holds a shared tail of `.text` plus - /// the private copy, byte-identical there, so `ReadExec` is safe over either. + /// image writable first would leave `.text` writable. pub fn map_into(&self, pt: &crate::process::PageTables) -> Option { use crate::mm::policy::WindowProt; - let (image_phys, image_size) = match &self.memory { - LibMemory::Owned(alloc) => ( - crate::DirectMap::phys_of(alloc.ptr()), - alloc.size() as u64, - ), - LibMemory::Shared { cached_image, .. } => { - (cached_image.phys(), cached_image.size() as u64) - } - }; + let (image_phys, image_size) = (crate::DirectMap::phys_of(self.memory.ptr()), self.memory.size() as u64); let base = pt .lock() @@ -152,18 +125,7 @@ impl LoadedLib { let mut offset = 0; while offset < image_size { - // The cache's shared image, except inside the private writable copy. - let phys = match &self.memory { - LibMemory::Owned(_) => image_phys + offset, - LibMemory::Shared { rw_alloc, rw_offset, .. } => { - let lo = *rw_offset as u64; - if (lo..lo + rw_alloc.size() as u64).contains(&offset) { - crate::DirectMap::phys_of(rw_alloc.ptr()) + (offset - lo) - } else { - image_phys + offset - } - } - }; + let phys = image_phys + offset; let mut prot = WindowProt::uniform(crate::mm::policy::Prot::Read); let mut page = 0; while page < PAGE_2M { @@ -340,11 +302,8 @@ pub(crate) fn read_backing_into( } /// Load a shared object into one contiguous allocation and apply its -/// `R_X86_64_RELATIVE` relocations, returning the module plus its 2 MiB-aligned -/// writable window (must match what `rw_alloc` later covers). -pub fn load_shared_lib( - backing: &dyn crate::file_backing::FileBacking, -) -> Result<(LoadedLib, usize, usize), &'static str> { +/// `R_X86_64_RELATIVE` relocations. +pub fn load_shared_lib(backing: &dyn crate::file_backing::FileBacking) -> Result { let header_size = 4096.min(backing.file_size() as usize); let header_data = crate::loader::read_file_range(backing, 0, header_size); let layout = parse_layout(&header_data)?; @@ -362,14 +321,9 @@ pub fn load_shared_lib( let (rw_lo, rw_hi) = layout.writable_window().unwrap_or((layout.span(), layout.span())); // `span` is file-chosen; an unchecked round-up could wrap to a too-small allocation. - let (Some(load_size), Some(rw_end_aligned)) = ( - align_2m_checked(layout.span()).and_then(|v| usize::try_from(v).ok()), - align_2m_checked(rw_hi).and_then(|v| usize::try_from(v).ok()), - ) else { + let Some(load_size) = align_2m_checked(layout.span()).and_then(|v| usize::try_from(v).ok()) else { return Err("ELF: image span does not fit an allocation"); }; - let rw_offset = rw_lo as usize & !(PAGE_2M as usize - 1); - let rw_size = rw_end_aligned - rw_offset; let alloc = PageAlloc::new(load_size).ok_or("dlopen: allocation failed")?; @@ -447,9 +401,9 @@ pub fn load_shared_lib( None => None, }; - // 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`. + // The exact writable extent, not a 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 span_of = |slice: &KernelSlice| { let at = (slice.base() as usize - image.base() as usize) as u64; @@ -481,9 +435,8 @@ pub fn load_shared_lib( let tls_template = layout.tls().map(|tls| module.at(tls.template())); - Ok(( - LoadedLib { - memory: LibMemory::Owned(alloc), + Ok(LoadedLib { + memory: alloc, user_base: UserAddr::new(base_phys), image, dynsym, @@ -492,7 +445,6 @@ pub fn load_shared_lib( rela, jmprel, gnu_hash, - cached_relocs: None, rules, eh_frame_hdr: layout.eh_frame_hdr(), phdrs, @@ -500,10 +452,7 @@ pub fn load_shared_lib( span: layout.span(), rw_lo, rw_hi, - }, - rw_offset, - rw_size, - )) + }) } /// `.dynsym`'s entry count as the section header table declares it, or `None` diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs index 9081645f5f..8876b3871b 100644 --- a/kernel/src/elf/reloc.rs +++ b/kernel/src/elf/reloc.rs @@ -13,7 +13,7 @@ //! none is a record of its own, since a file chooses how many there are. A resolved TLS //! reference whose `S + A` leaves the defining module's segment is refused. -use super::{relocated_symbol, CachedRelocs, LibMemory, LoadedLib, TlsModule, TlsModuleInfo}; +use super::{relocated_symbol, LoadedLib, TlsModule, TlsModuleInfo}; use toyos_elf::sym::{Sym, SymTab}; use toyos_elf::{ImageOffset, Op, RelocError, SymIndex, TlsOffset, TlsRef, TlsSegment}; @@ -21,56 +21,31 @@ impl LoadedLib { /// Write a value at a byte offset within this module's kernel mapping. /// /// # Safety - /// Caller must be the sole writer of this module's image (or `rw_alloc`) for the duration of the call. + /// Caller must be the sole writer of this module's image for the duration of the call. pub(super) unsafe fn write_at(&self, offset: u64, value: T) { let end = (offset as usize) .checked_add(core::mem::size_of::()) .expect("LoadedLib::write_at: r_offset + width overflows"); - // Each arm asserts the bound protecting its own destination. - match &self.memory { - LibMemory::Owned(_) => { - assert!( - end <= self.image.size(), - "LoadedLib::write_at: r_offset {:#x} outside image of {:#x}", - offset, - self.image.size() - ); - self.image.write(offset as usize, value) - } - LibMemory::Shared { rw_alloc, rw_offset, rw_delta, .. } => { - assert!( - offset as usize >= *rw_offset && end <= *rw_offset + rw_alloc.size(), - "LoadedLib::write_at: r_offset {:#x} outside the writable window [{:#x}, {:#x})", - offset, - rw_offset, - rw_offset + rw_alloc.size() - ); - let ptr = (self.image.base().add(offset as usize) as i64 + rw_delta) as *mut T; - ptr.write_unaligned(value); - } - } + assert!( + end <= self.image.size(), + "LoadedLib::write_at: r_offset {:#x} outside image of {:#x}", + offset, + self.image.size() + ); + self.image.write(offset as usize, value) } - /// 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_map(move |r| Some((r.offset(), of(r.op())?))) - }); - cached.into_iter().flatten().chain(scanned.into_iter().flatten()) + /// Every slot of one kind and what it names, scanned off the image. + fn entries<'a, T: Copy + 'a>(&'a self, of: fn(Op) -> Option) -> impl Iterator + 'a { + self.relocations().filter_map(move |r| Some((r.offset(), of(r.op())?))) } fn bind_entries(&self) -> impl Iterator + '_ { - self.entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }, |c| &c.bind) + self.entries(|op| match op { Op::Bind(sym) => Some(sym), _ => None }) } fn dtpmod_entries(&self) -> impl Iterator)> + '_ { - self.entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }, |c| &c.dtpmod64) + self.entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }) } /// Every `RELATIVE` slot and the position in the image it points at. @@ -131,21 +106,21 @@ pub fn apply_tpoff_relocs( _ => 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) { + for (_, r) in lib.entries(tpoff64) { tpoff(r)?; } - for (_, r) in lib.entries(tpoff32, |c| &c.tpoff32) { + for (_, r) in lib.entries(tpoff32) { tpoff32_value(tpoff(r)?.unwrap_or(0))?; } let mut unresolved = 0; - for (offset, r) in lib.entries(tpoff64, |c| &c.tpoff64) { + for (offset, r) in lib.entries(tpoff64) { let value = tpoff(r)?; unresolved += u64::from(value.is_none()); // SAFETY: see write_at's `# Safety`. unsafe { lib.write_at::(offset, value.unwrap_or(0)) }; } - for (offset, r) in lib.entries(tpoff32, |c| &c.tpoff32) { + for (offset, r) in lib.entries(tpoff32) { let value = tpoff(r)?; unresolved += u64::from(value.is_none()); // SAFETY: see write_at's `# Safety`. @@ -184,11 +159,11 @@ pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result< _ => None, }; let resolve = |r| resolve_tls_ref(r, 0, lib.tls(), lib.symbols(), tls_info); - for (_, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { + for (_, r) in lib.entries(dtpoff) { resolve(r)?; } let mut unresolved = 0; - for (offset, r) in lib.entries(dtpoff, |c| &c.dtpoff64) { + for (offset, r) in lib.entries(dtpoff) { let value = resolve(r)?; unresolved += u64::from(value.is_none()); // SAFETY: see write_at's `# Safety`. diff --git a/kernel/src/invalidation.rs b/kernel/src/invalidation.rs index 13bc01b8ab..8cc5106da4 100644 --- a/kernel/src/invalidation.rs +++ b/kernel/src/invalidation.rs @@ -3,7 +3,7 @@ //! of the address space it answers for. /// Which path issued a shootdown, so the census names who pays: `Dlopen` (a -/// `Shared` window or rollback unmap), `Pcid` (pool reclaim), `Mmio`, `Unmap` +/// rollback unmap), `Pcid` (pool reclaim), `Mmio`, `Unmap` /// (`Unmapped::drop`), `Pipe`, `Staged` (a test actuator's), `Bench` /// (`arch::tlb::bench`'s own, so a measured shootdown is never counted as one /// a path in this kernel needed), `Stop` (the power-off's, for the answer from diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 5d8fdaa963..22037ac572 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -290,18 +290,23 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> sys_rmdir(&path) } SYS_DLOPEN => { - let path = match ctx.user_str(UserAddr::new(a1), a2) { Ok(s) => s, Err(e) => return e.to_u64() }; + let Ok(args) = ctx.copy_in::(UserAddr::new(a1)) else { return bad_addr }; + let name = match ctx.user_str(UserAddr::new(args.name_ptr), args.name_len) { Ok(s) => s, Err(e) => return e.to_u64() }; // Refused here rather than at the write, so a bad address never leaves a // library the caller was never told about. - let init_out = match a3 { + let init_out = match args.init_out { 0 => None, raw => match UserAddr::checked(raw) { Some(addr) => Some(addr), None => return bad_addr, }, }; + let image = match shared_image(args.image, args.image_len) { + Ok(image) => image, + Err(refused) => return refused, + }; // ctx carries the copy-out: sys_dlopen writes init_out only once the load succeeds. - sys_dlopen(&ctx, &path, init_out) + sys_dlopen(&ctx, name, &image, init_out) } SYS_DLSYM => { let name = match ctx.user_str(UserAddr::new(a2), a3) { Ok(s) => s, Err(e) => return e.to_u64() }; diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index 89d62cd3da..0512ac6884 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -291,10 +291,7 @@ pub(super) fn sys_sysinfo(syscap: RawHandle, out: &mut UserBytesMut) -> u64 { let demand = data.demand_pages.iter().map(|p| p.size() as u64).sum::(); let mmap = data.mmap_regions.iter().filter_map(|r| r._pages.as_ref()).map(|p| p.size() as u64).sum::(); let tls = data.elf.dynamic_tls_blocks.values().map(|p| p.size() as u64).sum::(); - let libs: u64 = data.elf.loaded_libs.iter().map(|l| match &l.memory { - crate::elf::LibMemory::Owned(alloc) => alloc.size() as u64, - crate::elf::LibMemory::Shared { rw_alloc, .. } => rw_alloc.size() as u64, - }).sum(); + let libs: u64 = data.elf.loaded_libs.iter().map(|l| l.memory.size() as u64).sum(); demand + mmap + tls + libs } else { 0 diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index a5886822de..c3bea6404f 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -18,7 +18,7 @@ use crate::mm::policy::{CachePolicy, Prot}; use crate::vma::Occupancy; use crate::user_ptr::UserBytesMut; use crate::UserAddr; -use crate::{process, vfs}; +use crate::process; use toyos_abi::syscall::*; @@ -232,10 +232,15 @@ pub(super) fn shared_image(handle: u64, len: u64) -> Result) -> u64 { - let cwd = process::with_process_data(|d| d.cwd.clone()); - let resolved = vfs::lock().resolve_absolute(&cwd, path); - +/// Load the library `image` holds into this process as `name` (`DlopenArgs`). +/// The image is the caller's, read by the load and kept by nothing: the module +/// is this process's own copy. +pub(super) fn sys_dlopen( + ctx: &crate::user_ptr::SyscallContext, + resolved: alloc::string::String, + image: &crate::file_backing::SharedImage, + init_out: Option, +) -> u64 { // A repeat load of a name this process holds shares that module and maps // nothing again — a `dlopen` loop used to grow the address space unbounded. // POSIX-shaped: the shared module's initializers do not re-run, so empty init. @@ -250,37 +255,11 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init return idx as u64; } - // Opened before the cache is consulted: the answer depends on what the path holds *now*, and the fast path above is what keeps a `dlopen` loop from paying. - let (backing, id) = match vfs::lock().open_backing_identified(&resolved) { - Ok(pair) => pair, - Err(e) => { - log!("dlopen: {}: {e}", resolved); - return e.to_u64(); - } - }; - let mut lib = match crate::elf::try_clone_cached(&resolved, id) { - Ok(Some(lib)) => lib, - Err(e) => { - log!( - "dlopen: {}: the cached image is stale — the file behind this path changed since \ - it was loaded, and the image cannot be replaced while a process has it mapped", - resolved - ); - return e.to_u64(); - } - Ok(None) => { - let (lib, rw_offset, rw_size) = match crate::elf::load_shared_lib(backing.as_ref()) { - Ok(result) => result, - Err(msg) => { - log!("dlopen: {}", msg); - return SyscallError::Unknown.to_u64(); - } - }; - - match crate::elf::cache_loaded_lib(&resolved, id, lib, rw_offset, rw_size) { - Ok(lib) => lib, - Err(e) => return e.to_u64(), - } + let mut lib = match crate::elf::load_shared_lib(image) { + Ok(lib) => lib, + Err(msg) => { + log!("dlopen: {}: {}", resolved, msg); + return SyscallError::InvalidArgument.to_u64(); } }; @@ -290,12 +269,6 @@ pub(super) fn sys_dlopen(ctx: &crate::user_ptr::SyscallContext, path: &str, init // and which executable; mapping the whole image writable would make // it executable too. let vaddr = lib.map_into(&pt).ok_or(SyscallError::ResourceExhausted)?; - // A `Shared` module's windows may reuse a range already handed out; - // `map_window`'s shootdown reached only this CPU, so the rest of the - // machine is told here. - if matches!(lib.memory, crate::elf::LibMemory::Shared { .. }) { - crate::arch::tlb::shootdown(crate::invalidation::Origin::Dlopen, crate::invalidation::Half::User); - } if vaddr != lib.user_base { lib.user_base = vaddr; crate::elf::rebase_relative_relocs(&lib); diff --git a/kernel/src/vfs.rs b/kernel/src/vfs.rs index b8a4d0a4fb..6d0e24fa00 100644 --- a/kernel/src/vfs.rs +++ b/kernel/src/vfs.rs @@ -670,30 +670,8 @@ impl Vfs { } pub fn open_backing(&mut self, path: &str) -> Result, SyscallError> { - self.open_backing_identified(path).map(|(backing, _)| backing) - } - - /// [`open_backing`](Self::open_backing) plus what the mount says about the - /// file now, for a caller that keeps something derived from the bytes. - pub fn open_backing_identified( - &mut self, - path: &str, - ) -> Result<(alloc::sync::Arc, BackingId), SyscallError> { let target = self.resolve_for_open(path, ResolveIntent::KernelOrRead)?; let (fs, fs_path, _) = self.fs_for_target(&target)?; - let mtime = fs.file_mtime(&fs_path)?; - let backing = fs.open_backing(&fs_path)?; - let id = BackingId { size: backing.file_size(), mtime }; - Ok((backing, id)) + fs.open_backing(&fs_path) } } - -/// What a mount said about a file when its backing was opened: metadata the open -/// had already reached, so it reads no page of the file. **It refuses a rewrite; -/// it does not identify a file** — two files can carry the same size and mtime, -/// and a mount whose mtime does not move under a same-size write hides one. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct BackingId { - pub size: u64, - pub mtime: u64, -} diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index db5e0e32c4..282a3d0fdc 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -319,9 +319,8 @@ crate::user_safe! { /// [`Rights::DUP`]: crate::handle::Rights::DUP #[derive(Clone, Copy)] pub struct SpawnArgs { - /// The program: the file the kernel opens and pages the child from — its - /// libraries are found beside it — or, with an `image`, the path the caller - /// read that from. **Never `argv[0]`**, which is the child's first argument + /// The program: the file the kernel opens and pages the child from, or, + /// with an `image`, the path the caller read that from. **Never `argv[0]`**, which is the child's first argument /// and the name it goes by, and which nothing opens. pub path_ptr: u64, pub path_len: u64, @@ -351,7 +350,7 @@ crate::user_safe! { /// hold, or an object that is no memory the kernel allocated. The object /// stays the caller's: what the kernel reads of it, and when, is /// `kernel/src/file_backing.rs`'s (`SharedImage`). `path` is then opened by - /// nobody, and the libraries are found in `/system/lib` alone. + /// nobody. pub image: u64, pub image_len: u64, /// The process the child is placed under, whose end takes it down: a @@ -367,6 +366,26 @@ crate::user_safe! { } } +crate::user_safe! { + /// Arguments for the `SYS_DLOPEN` syscall, passed as a single pointer. + #[derive(Clone, Copy)] + pub struct DlopenArgs { + /// The name the module goes by: what `SYS_QUERY_MODULES` answers for + /// it, and what a second load is known by, which answers the module + /// already loaded and runs no initialiser again. Nothing opens it. + pub name_ptr: u64, + pub name_len: u64, + /// The library's bytes, in a shared memory object the caller read them + /// into, as [`SpawnArgs::image`] names a program's: a handle carrying + /// `MAP`, and how many of its first bytes the library is. + pub image: u64, + pub image_len: u64, + /// Where the kernel writes the module's `[.init_array address, count]`, + /// or 0 for nowhere. + pub init_out: u64, + } +} + /// One `(label, handle)` pair of a process's endowment table. /// /// `label_off`/`label_len` index the label blob that travels beside the @@ -2118,11 +2137,18 @@ pub fn readlink(path: &[u8], buf: &mut [u8]) -> Result { /// name it does not hold past it is `ResourceExhausted`. pub const MAX_LIBRARIES: usize = 1024; -/// Load a shared library (.so) into the current process. -/// Runs .init_array constructors after loading. -pub fn dl_open(path: &[u8]) -> Result { +/// Load the shared library `image`'s first `image_len` bytes hold into this +/// process as `name` ([`DlopenArgs`]), and run its `.init_array`. +pub fn dl_open(name: &[u8], image: RawHandle, image_len: u64) -> Result { let mut init_info: [u64; 2] = [0; 2]; - let handle = check(syscall(SYS_DLOPEN, path.as_ptr() as u64, path.len() as u64, init_info.as_mut_ptr() as u64, 0))?; + let args = DlopenArgs { + name_ptr: name.as_ptr() as u64, + name_len: name.len() as u64, + image: u64::from(image.0), + image_len, + init_out: init_info.as_mut_ptr() as u64, + }; + let handle = dl_open_args(&args)?; // Run .init_array constructors (e.g. EH frame finder registration in cdylib std) let init_array_ptr = init_info[0]; let init_count = init_info[1]; @@ -2145,6 +2171,12 @@ pub fn dl_open(path: &[u8]) -> Result { Ok(handle) } +/// `SYS_DLOPEN` as `args` say, running nothing it loads: [`dl_open`]'s call, +/// for a caller that names `init_out` itself. +pub fn dl_open_args(args: &DlopenArgs) -> Result { + check(syscall(SYS_DLOPEN, args as *const DlopenArgs as u64, 0, 0, 0)) +} + /// Look up a symbol in a loaded shared library. Returns the address. /// /// # Safety diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index f7f42bbe90..5908099547 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -9,6 +9,7 @@ pub(crate) const ENOENT: i32 = 2; pub(crate) const ESRCH: i32 = 3; pub(crate) const EIO: i32 = 5; pub(crate) const E2BIG: i32 = 7; +pub(crate) const ENOEXEC: i32 = 8; pub(crate) const EBADF: i32 = 9; pub(crate) const ECHILD: i32 = 10; pub(crate) const EAGAIN: i32 = 11; @@ -71,6 +72,17 @@ pub(crate) fn of(e: SyscallError) -> i32 { } } +/// Why a program or a library was not read into an image, as POSIX words it. +pub(crate) fn of_image(refused: toyos::process::ImageRefused) -> i32 { + use toyos::process::ImageRefused; + match refused { + ImageRefused::Empty => ENOEXEC, + ImageRefused::Shrank => EIO, + ImageRefused::Memory(e) => of(e), + ImageRefused::Read(e) => of_files(e), + } +} + /// The errno a refusal of `toyos::files` is answered with. pub(crate) fn of_files(e: toyos::files::Error) -> i32 { use toyos::files::Error; diff --git a/sdk/libc/src/misc.rs b/sdk/libc/src/misc.rs index 1d9385e0b1..4081d42dac 100644 --- a/sdk/libc/src/misc.rs +++ b/sdk/libc/src/misc.rs @@ -445,9 +445,15 @@ fn module_index(handle: *mut u8) -> Option { pub unsafe extern "C" fn dlopen(path: *const u8, _flags: i32) -> *mut u8 { if path.is_null() { return ptr::null_mut(); } let path_bytes = super::posix_io::c_str_to_bytes(path); - match syscall::dl_open(path_bytes) { + match toyos::files::dl_open(path_bytes) { Ok(index) => (index + 1) as *mut u8, - Err(_) => ptr::null_mut(), + Err(refused) => { + crate::errno::set(match refused { + toyos::files::DlOpenRefused::Image(refused) => crate::errcode::of_image(refused), + toyos::files::DlOpenRefused::Load(e) => crate::errcode::of(e), + }); + ptr::null_mut() + } } } diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 296af6c5f6..5bc36797c6 100644 --- a/sdk/libc/src/spawn.rs +++ b/sdk/libc/src/spawn.rs @@ -330,7 +330,13 @@ fn start(path: &str, argv: &[u8], env: &[u8], fds: &[(u32, u32)]) -> Result Some(toyos::files::read_image(path.as_bytes()).map_err(crate::errcode::of_image)?), + false => None, + }; + request.direct(&direct, image.as_ref()).map_err(errno_of) } fn errno_of(refused: Refused) -> i32 { diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index ea2d6e7638..ffecb3da6c 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -522,11 +522,7 @@ pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> { /// The whole of the file at `path` as a program image: what a spawn of a /// program the kernel cannot open itself hands the kernel. pub fn read_image(path: &Path) -> io::Result { - let mut opts = OpenOptions::new(); - opts.read(true); - let file = File::open(path, &opts)?; - let len = file.file_attr()?.size; - toyos::process::Image::read(len, |buf| file.read(buf)).map_err(|refused| match refused { + files::read_image(bytes(path)).map_err(|refused| match refused { toyos::process::ImageRefused::Empty => { io::const_error!(io::ErrorKind::InvalidData, "the program is an empty file") } @@ -534,7 +530,7 @@ pub fn read_image(path: &Path) -> io::Result { toyos::process::ImageRefused::Shrank => { io::const_error!(io::ErrorKind::UnexpectedEof, "the program shrank while it was read") } - toyos::process::ImageRefused::Read(e) => e, + toyos::process::ImageRefused::Read(e) => files_error(e), }) } diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index c40ada2279..cc6d0e5dd4 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -1,7 +1,8 @@ //! Files by path, and the descriptors that name open ones, wherever they are //! served: the kernel serves `/system` and `/tmp`, and every other directory a //! process may reach is a capability in its namespace, `fs:`, -//! served by a file server ([`crate::fs`]). +//! served by a file server ([`crate::fs`]). A program or a library is read +//! here into an image the kernel pages it from ([`read_image`]). //! //! **A path goes to the capability with the longest matching prefix**, at //! most [`MAX_CAPABILITY_DEPTH`] components deep, relative to it; a path under @@ -23,6 +24,7 @@ use toyos_abi::syscall::{SeekFrom, Stat, SyscallError}; use crate::fd::{self, Fd}; use crate::fs; +use crate::process::{Image, ImageRefused}; use crate::OwnedHandle; /// The deepest directory a capability names, in components. @@ -248,6 +250,42 @@ pub fn read_dir(path: &[u8]) -> Result, Error> { } } +/// The whole of the file at `path`, read into a memory object of this +/// process's own, wherever it is served: what a spawn names as its program +/// (`SpawnArgs::image`) and [`dl_open`] loads, since the kernel opens neither. +pub fn read_image(path: &[u8]) -> Result> { + let fd = open(path, Open { read: true, cloexec: true, ..Open::default() }).map_err(ImageRefused::Read)?; + let image = match fstat(fd) { + Ok(stat) => Image::read(stat.size, |buf| read(fd, buf)), + Err(e) => Err(ImageRefused::Read(e)), + }; + // A refused close leaves nothing this read depends on. + let _ = fd::close(fd); + image +} + +/// Why [`dl_open`] loaded nothing. +#[derive(Debug)] +pub enum DlOpenRefused { + /// The library was not read into an image. + Image(ImageRefused), + /// The kernel refused to load the image it was handed. + Load(SyscallError), +} + +/// Load the shared library at `path` into this process, under its absolute +/// path as the name it goes by, and run its initialisers: the module's handle. +/// A second load of one name answers the module the first loaded. +pub fn dl_open(path: &[u8]) -> Result { + let text = core::str::from_utf8(path).map_err(|_| DlOpenRefused::Image(ImageRefused::Read(Error::NotUtf8)))?; + let mut cwd = alloc::vec![0u8; toyos_abi::syscall::MAX_PATH]; + let n = toyos_abi::syscall::getcwd(&mut cwd); + let name = absolute(&String::from_utf8_lossy(&cwd[..n.min(cwd.len())]), text); + let image = read_image(name.as_bytes()).map_err(DlOpenRefused::Image)?; + let (handle, len) = image.spawn_words(); + toyos_abi::syscall::dl_open(name.as_bytes(), toyos_abi::RawHandle(handle as u32), len).map_err(DlOpenRefused::Load) +} + /// Whether `path` is on a file server rather than the kernel. pub fn is_served(path: &[u8]) -> bool { // SAFETY: `path` is live for the call. diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 38cc254793..40e7b2c090 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -394,9 +394,10 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" version = "0.8.9" -source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#bad8d494d4faaa8dacbe8963ca27c1c538894953" +source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#da187608c2662c6e251386192ee1297c83d3ebd8" dependencies = [ "cfg-if", + "toyos", "toyos-abi", "windows-link", ] diff --git a/tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs b/tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs index 424464239d..ad82ade770 100644 --- a/tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs +++ b/tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs @@ -2,6 +2,7 @@ //! name it does not hold is refused by name having registered nothing, and a //! name it holds still answers. +use toyos::files::{self, DlOpenRefused}; use toyos_abi::syscall::{self, SyscallError, MAX_LIBRARIES}; const DIR: &str = "/tmp/dlopen-ledger"; @@ -10,9 +11,8 @@ const DIR: &str = "/tmp/dlopen-ledger"; /// heap allocation. const ATTEMPTS: usize = 4 * MAX_LIBRARIES; -/// One `PT_LOAD`, read-only and exactly 2 MiB long, so the image has no -/// writable window and the shared-object cache keeps none of it: every load is -/// its own, and nothing of this test outlives it. +/// One `PT_LOAD`, read-only and exactly 2 MiB long: every load is its own, +/// and nothing of this test outlives it. fn image() -> Vec { const LEN: usize = 0x1000; const SPAN: u64 = 2 * 1024 * 1024; @@ -55,7 +55,7 @@ fn main() { // A name of its own for one file: the ledger is keyed by name. let name = format!("{DIR}/{i}.so"); syscall::symlink(lib.as_bytes(), name.as_bytes()).unwrap_or_else(|e| panic!("link {name}: {e:?}")); - match syscall::dl_open(name.as_bytes()) { + match files::dl_open(name.as_bytes()) { Ok(handle) => { first.get_or_insert((name, handle)); loaded += 1; @@ -69,16 +69,16 @@ fn main() { let refusal = refusal.unwrap_or_else(|| { panic!("{loaded} names of one library all loaded: the library count is unbounded") }); - assert_eq!( - refusal, - SyscallError::ResourceExhausted, - "the load after {loaded} was refused for another reason", + assert!( + matches!(refusal, DlOpenRefused::Load(SyscallError::ResourceExhausted)), + "the load after {loaded} was refused for another reason: {refusal:?}", ); assert_eq!(held + loaded, MAX_LIBRARIES, "refused after {loaded} loads beside the {held} it started with"); assert_eq!(libraries(), MAX_LIBRARIES, "the refused load is registered"); let (name, handle) = first.expect("a library was loaded"); - assert_eq!(syscall::dl_open(name.as_bytes()), Ok(handle), "a name already held was refused at the bound"); + let again = files::dl_open(name.as_bytes()).unwrap_or_else(|e| panic!("a name already held was refused at the bound: {e:?}")); + assert_eq!(again, handle, "a name already held answered another module"); println!("{loaded} libraries loaded and the next name refused; a name held still answers"); } diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs index ce5088bd53..d89821bd6c 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs @@ -245,10 +245,9 @@ fn main() { ("dyn_rela_far", &[(DT_RELA, 0x7000_0000), (DT_RELASZ, 24)][..]), ("dyn_reloc_oob", &[(DT_RELA, 0x3800), (DT_RELASZ, 24)][..]), // Every written relocation type, not just RELATIVE, has to be bounds - // checked at load: once the module is cached the write lands in a - // *smaller* private allocation than the image, so the image's bounds do - // not cover it. DTPOFF64 with `r_sym == 0` writes `r_addend` verbatim, - // which makes the value file-chosen too. + // checked at load against the module's allocation. DTPOFF64 with + // `r_sym == 0` writes `r_addend` verbatim, which makes the value + // file-chosen too. ("dyn_dtpoff_oob", &[(DT_RELA, 0x3800), (DT_RELASZ, 24)][..]), ("dyn_globdat_oob", &[(DT_RELA, 0x3800), (DT_RELASZ, 24)][..]), // An in-range r_offset whose r_sym indexes past .dynsym: the symbol diff --git a/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs b/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs index 50fa0caca5..44438d25eb 100644 --- a/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs +++ b/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs @@ -3,7 +3,7 @@ //! shallow-copies the kernel's PML4 half, so a kernel address resolved to a //! present, writable 2 MiB leaf of the direct map. //! -//! `SYS_DLOPEN`'s third argument was the one the syscall layer never validated +//! `SYS_DLOPEN`'s `init_out` was the one the syscall layer never validated //! at all — sixteen bytes of arbitrary kernel memory, written by any process //! that can call `dlopen`. `SYS_FUTEX_WAIT`'s word is the other: it was //! guarded, but by an expression at the dispatch arm whose value was thrown @@ -18,7 +18,7 @@ //! (`SYS_DEBUG` actions 10 and 11): where they are, and whether they still say //! what it put there. -use toyos_abi::syscall::{self, MmapFlags, MmapProt, SyscallError, SYS_DLOPEN}; +use toyos_abi::syscall::{self, DlopenArgs, MmapFlags, MmapProt, SyscallError}; use toyos_abi::syscall::debug_action::{CANARY_ADDR, CANARY_CHANGED}; @@ -29,24 +29,14 @@ const LIB: &[u8] = b"/system/lib/libtls_dlopen_lib.so"; const PAGE_2M: u64 = 2 * 1024 * 1024; /// `dl_open` builds its own `init_info` on the stack, so the typed wrapper -/// cannot express the argument under test. Everything else about the call is -/// the ABI's own: number in rdi, arguments in rsi/rdx/r8/r9. +/// cannot express the argument under test: `SYS_DLOPEN` of [`LIB`], read once +/// into an image, with `init_out` as given. fn dlopen_raw(path: &[u8], init_out: u64) -> u64 { - let ret: u64; - unsafe { - core::arch::asm!( - "syscall", - in("rdi") SYS_DLOPEN, - in("rsi") path.as_ptr() as u64, - in("rdx") path.len() as u64, - in("r8") init_out, - in("r9") 0u64, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - ret + static IMAGE: std::sync::OnceLock = std::sync::OnceLock::new(); + let image = IMAGE.get_or_init(|| toyos::files::read_image(path).unwrap_or_else(|e| panic!("read the library: {e:?}"))); + let (handle, image_len) = image.spawn_words(); + let args = DlopenArgs { name_ptr: path.as_ptr() as u64, name_len: path.len() as u64, image: handle, image_len, init_out }; + syscall::dl_open_args(&args).unwrap_or_else(SyscallError::to_u64) } fn err(ret: u64) -> Option { diff --git a/tests/guest/rust-tests/src/bin/dlopen_dedup.rs b/tests/guest/rust-tests/src/bin/dlopen_dedup.rs index 04a0bc9865..9416a950d7 100644 --- a/tests/guest/rust-tests/src/bin/dlopen_dedup.rs +++ b/tests/guest/rust-tests/src/bin/dlopen_dedup.rs @@ -3,8 +3,9 @@ //! two ways: handle identity (64 loads of one path return one handle, not //! 0, 1, 2, …), and the ELF/POSIX same-object guarantee (a TLS global written //! through the first handle's symbol is read back through the last's — one -//! module, one TLS block; a fresh second module would read zero). Raw `dl_*` -//! syscalls, not `libloading`, so the handle integer is observable. +//! module, one TLS block; a fresh second module would read zero). The SDK's +//! `dl_open` and raw `dl_sym`, not `libloading`, so the handle integer is +//! observable. use std::sync::{Arc, Barrier}; @@ -25,7 +26,7 @@ fn main() { /// The mechanism: repeated loads of one name are one handle. fn handle_identity() -> Vec { let handles: Vec = (0..LOADS) - .map(|i| syscall::dl_open(LIB).unwrap_or_else(|e| panic!("dlopen #{i}: {e:?}"))) + .map(|i| toyos::files::dl_open(LIB).unwrap_or_else(|e| panic!("dlopen #{i}: {e:?}"))) .collect(); assert!( @@ -80,7 +81,7 @@ fn one_name_under_contention() { let line = Arc::clone(&line); std::thread::spawn(move || { line.wait(); - syscall::dl_open(OTHER_LIB).unwrap_or_else(|e| panic!("racer {i}: {e:?}")) + toyos::files::dl_open(OTHER_LIB).unwrap_or_else(|e| panic!("racer {i}: {e:?}")) }) }) .collect(); diff --git a/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs b/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs index fa97e03ef2..0b4f3f17ac 100644 --- a/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs +++ b/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs @@ -109,7 +109,7 @@ fn two_names_at_once_are_two_modules() { let path = core::str::from_utf8(name).expect("a library path"); std::fs::read(path).unwrap_or_else(|e| panic!("warm {path}: {e}")); line.wait(); - syscall::dl_open(name).unwrap_or_else(|e| panic!("dlopen {path}: {e:?}")) + toyos::files::dl_open(name).unwrap_or_else(|e| panic!("dlopen {path}: {e:?}")) }) }) .collect();