From d89b8bf63d20f581edc0343831a209096a46c78a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 11:14:42 +0100 Subject: [PATCH 01/13] The loader is the program's: TLS and dlopen leave the kernel Stage 2 of Move 1 of issues/the-kernel-still-parses-what-userland-writes.md. A program lays out its own thread-local storage (toyos::tls): one process-wide module list, the executable first, which every thread's DTV names so every copy of the toyos crate in a process finds it; each thread's area (DTV, the executable's block, the TCB) below its stack, released with it; a dlopen'd module's block made on first use by __tls_get_addr's slow path, in the process's own memory; a DTV that grows. The kernel's only part is SYS_SET_THREAD_POINTER (171), which points the caller's FS base or TPIDR_EL0 at a user-half address and answers its tid; a spawned thread starts with no thread pointer and names its own first. FSGSBASE stays forbidden, so x86-64 needs the call. libc's loader (sdk/libc/src/dl.rs over dlimage.rs) reads a library wherever it is served, lays it out in private memory, relocates it with toyos-elf against every published module (the executable first), gives each segment its protection and PT_GNU_RELRO read-only, publishes it and runs its constructors. dlopen, dlsym (RTLD_DEFAULT included), dlerror, dladdr and dl_iterate_phdr read the list; std's unwinder and the fork's backtrace crate read it too. An executable naming DT_NEEDED is no longer refused by toyos::relocate: it applies its RELATIVE entries, and its start loads the libraries and binds the rest before main (__toyos_load_needed). A C program always links the loader; std reaches it through a weak symbol, since libtoyos_c is built from std, and a Rust program naming a library links libtoyos_c itself or is refused at start by name, as std_tls (restored) and the track record. Retired: SYS_DLOPEN, SYS_DLSYM, SYS_DLCLOSE, SYS_TLS_ALLOC_BLOCK and SYS_QUERY_MODULES, with DlopenArgs, ModuleInfo, MAX_LIBRARIES and the ABI's TCB items (current_tid moves to toyos::tls). The kernel loses kernel/src/elf/, loader/tls.rs, ElfInfo, a thread's TLS mapping, the dynamic TLS blocks, KernelSlice, Rollback and OwnedAlloc's views: kernel/src goes from 223 files and 56,800 lines to 218 and 55,286. toyos-elf loses the static-TLS arithmetic only the kernel used. A program may protect its own memory read and execute (vm_prot), which dlopen already effectively allowed through any bytes it was handed; no protection allows a write and an execution at once, held by kernel/pure/vm.rs's no_protection_allows_a_write_and_an_execution, and an object's mapping still never becomes executable. libc's mmap and mprotect follow: read+exec answered, write+exec ENOTSUP. Closes the DTV-capacity defect (the DTV grows in the process's memory), the twice-zeroed TLS block, the contiguous dlopen run, dlopen's write_at contract, std's query-modules miscount, RTLD_DEFAULT's panic (207 holds it) and KernelSlice's lifetime. The crafted-ELF guest cases keep what the kernel still parses, the program headers; a library's are the program's loader's, held by toyos-libc-copies' dlimage tests. Co-Authored-By: Claude Opus 5.5 --- .../a-dlopen-image-is-one-contiguous-run.md | 20 - ...-never-binds-to-the-executables-exports.md | 15 +- ...a-package-cannot-ship-its-own-libraries.md | 12 +- ...-total-that-reads-zero-under-contention.md | 6 +- ...-per-call-and-nothing-bounds-the-caller.md | 15 +- ...ock-is-zeroed-twice-with-preemption-off.md | 21 - issues/an-executables-relro-stays-writable.md | 4 +- ...-not-the-sole-writer-of-a-mapped-module.md | 26 - issues/dtv-capacity-is-a-workload-bound.md | 50 -- issues/kernelslice-outlives-its-allocation.md | 37 -- ...dl-iterate-phdr-allocates-on-every-walk.md | 19 +- ...ym-rtld-default-panics-as-unimplemented.md | 20 - ...bc-refuses-what-toyos-cannot-yet-answer.md | 9 +- ...b-pages-still-copies-each-programs-text.md | 4 +- ...oyos-up-to-four-times-linuxs-on-the-t14.md | 5 +- ...ry-modules-byte-count-as-a-module-count.md | 32 -- ...-capability-end-state-is-twelve-answers.md | 12 +- ...-small-interrupts-post-and-threads-wait.md | 13 +- ...ernel-still-parses-what-userland-writes.md | 43 +- issues/toyos-runs-on-arm64.md | 7 +- issues/what-is-owed-on-file-size.md | 3 +- kernel/pure/vm.rs | 16 + kernel/src/arch/aarch64/cpu.rs | 2 +- kernel/src/arch/x86_64/cpu.rs | 8 + kernel/src/elf/mod.rs | 513 ------------------ kernel/src/elf/reloc.rs | 248 --------- kernel/src/loader/mod.rs | 99 +--- kernel/src/loader/tls.rs | 158 ------ kernel/src/main.rs | 2 - kernel/src/mm/mod.rs | 2 - kernel/src/mm/region.rs | 75 --- kernel/src/mm/space.rs | 5 - kernel/src/object/shm.rs | 2 +- kernel/src/process.rs | 149 +---- kernel/src/rollback.rs | 27 - kernel/src/syscall/dispatch.rs | 36 +- kernel/src/syscall/proc.rs | 15 + kernel/src/syscall/vm.rs | 323 +---------- lib/elf/src/header.rs | 5 +- lib/elf/src/lib.rs | 10 +- lib/elf/src/tls.rs | 166 +----- lib/elf/tests/fuzz.rs | 64 +-- lib/elf/tests/tls.rs | 194 ------- rust | 2 +- sdk/abi/src/arch/aarch64.rs | 19 - sdk/abi/src/arch/mod.rs | 9 +- sdk/abi/src/arch/x86_64.rs | 26 - sdk/abi/src/lib.rs | 26 +- sdk/abi/src/syscall.rs | 263 +-------- sdk/libc/arch/src/dlimage_relocations.rs | 222 ++++++++ sdk/libc/arch/src/lib.rs | 7 + sdk/libc/arch/src/listing_reader.rs | 35 +- sdk/libc/arch/src/memory_refusals.rs | 10 +- sdk/libc/arch/src/prototypes.rs | 1 + sdk/libc/src/dl.rs | 351 ++++++++++++ sdk/libc/src/dlimage.rs | 350 ++++++++++++ sdk/libc/src/lib.rs | 13 + sdk/libc/src/link.rs | 88 +-- sdk/libc/src/listing.rs | 23 +- sdk/libc/src/memreq.rs | 21 +- sdk/libc/src/misc.rs | 50 -- sdk/libc/src/posix_io.rs | 5 +- sdk/libc/src/pthread.rs | 44 +- sdk/libc/src/signal.rs | 2 +- sdk/std/sys/pal/mod.rs | 122 ++--- sdk/std/sys/pal/tls.rs | 83 +-- sdk/std/sys/thread.rs | 49 +- sdk/toyos/src/arch/aarch64.rs | 28 + sdk/toyos/src/arch/mod.rs | 4 +- sdk/toyos/src/arch/x86_64.rs | 37 ++ sdk/toyos/src/files.rs | 26 +- sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/log/stdio.rs | 4 +- sdk/toyos/src/relocate/mod.rs | 66 ++- sdk/toyos/src/relocate/tests.rs | 36 +- sdk/toyos/src/tls/layout.rs | 126 +++++ sdk/toyos/src/tls/mod.rs | 460 ++++++++++++++++ tests/corpus/tinycc/206_libc_refusals.c | 6 +- tests/corpus/tinycc/206_libc_refusals.expect | 4 +- tests/corpus/tinycc/207_libc_names.c | 6 + tests/corpus/tinycc/207_libc_names.expect | 1 + tests/guest/osbench/src/sys/toyos.rs | 2 +- tests/guest/rust-tests/Cargo.lock | 7 +- .../rust-tests/src/bin/abuse_dlopen_ledger.rs | 84 --- .../rust-tests/src/bin/abuse_elf_loader.rs | 404 ++------------ .../rust-tests/src/bin/abuse_elf_segments.rs | 139 +---- .../rust-tests/src/bin/abuse_kernel_addr.rs | 109 +--- .../rust-tests/src/bin/abuse_tls_alloc.rs | 140 ----- .../guest/rust-tests/src/bin/affinity_pins.rs | 2 +- .../guest/rust-tests/src/bin/dlopen_dedup.rs | 58 +- tests/guest/rust-tests/src/bin/mmap_stress.rs | 8 +- .../rust-tests/src/bin/query_modules_size.rs | 67 --- tests/guest/rust-tests/src/bin/std_threads.rs | 2 +- tests/guest/rust-tests/src/bin/std_tls.rs | 75 +++ .../rust-tests/src/bin/std_tls_multi_crate.rs | 50 +- .../rust-tests/src/bin/timing_protection.rs | 2 +- .../rust-tests/src/bin/wake_onto_busy.rs | 4 +- tests/guest/rust-tests/tls-lib/Cargo.toml | 2 +- tests/toyos.rs | 4 +- 99 files changed, 2259 insertions(+), 4018 deletions(-) delete mode 100644 issues/a-dlopen-image-is-one-contiguous-run.md delete mode 100644 issues/a-tls-block-is-zeroed-twice-with-preemption-off.md delete mode 100644 issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md delete mode 100644 issues/dtv-capacity-is-a-workload-bound.md delete mode 100644 issues/kernelslice-outlives-its-allocation.md delete mode 100644 issues/libc-dlsym-rtld-default-panics-as-unimplemented.md delete mode 100644 issues/std-reads-a-query-modules-byte-count-as-a-module-count.md delete mode 100644 kernel/src/elf/mod.rs delete mode 100644 kernel/src/elf/reloc.rs delete mode 100644 kernel/src/loader/tls.rs delete mode 100644 kernel/src/mm/region.rs delete mode 100644 kernel/src/rollback.rs delete mode 100644 lib/elf/tests/tls.rs create mode 100644 sdk/libc/arch/src/dlimage_relocations.rs create mode 100644 sdk/libc/src/dl.rs create mode 100644 sdk/libc/src/dlimage.rs create mode 100644 sdk/toyos/src/tls/layout.rs create mode 100644 sdk/toyos/src/tls/mod.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_tls_alloc.rs delete mode 100644 tests/guest/rust-tests/src/bin/query_modules_size.rs create mode 100644 tests/guest/rust-tests/src/bin/std_tls.rs diff --git a/issues/a-dlopen-image-is-one-contiguous-run.md b/issues/a-dlopen-image-is-one-contiguous-run.md deleted file mode 100644 index 02eee08eb95..00000000000 --- a/issues/a-dlopen-image-is-one-contiguous-run.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-22 ---- - -# A `dlopen` image is one physically contiguous run - -Every other user memory is 4 KiB frames in any order, but a library `dlopen` -loads is copied into one object over a physical run of 2 MiB pages -(`kernel/src/elf/mod.rs`, `Run::new`, over `SharedMemObject::create_run`): -the kernel lays the image out as one slice it relocates in place. A large -library on a machine whose memory is fragmented can be refused for want of a -run that long, though the frames exist, as a large program was before its -image object became scattered frames. - -Owner: the memory track -(`issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md`). -Exit: a library image is laid out and relocated through scattered frames, and -`create_run` serves only memory a device is lent. diff --git a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md index 3f56a76d034..48ffb00810d 100644 --- a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md +++ b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md @@ -6,12 +6,11 @@ opened: 2026-09-29 # A dlopen'ed library never binds to the executable's exports -`resolve_dlopen_relocs` (`kernel/src/elf/reloc.rs`) resolves a `dlopen`ed -module's `GLOB_DAT`/`JUMP_SLOT` slots against the other loaded libraries only. -glibc's `dlopen` searches the global scope, which starts with the -executable, so on ToyOS a plugin that calls back into a symbol its host -exports is left with an unresolved slot and faults when it uses it. +libc's loader (`sdk/libc/src/dl.rs`, `Published`) binds a library's +`GLOB_DAT`/`JUMP_SLOT` slots against every published module, the executable +first, as glibc's `dlopen` searches the global scope. Nothing shows it: no +guest case loads a plugin that calls back into a symbol its host exports, and +no ToyOS executable is linked to export one. -**Exit**: `resolve_dlopen_relocs` binds a slot against the executable's dynamic -exports before the other loaded libraries, and a guest case `dlopen`s a library that calls a function its executable -exports, and the call lands. +**Exit**: a guest case `dlopen`s a library that calls a function its +executable exports, and the call lands. diff --git a/issues/a-package-cannot-ship-its-own-libraries.md b/issues/a-package-cannot-ship-its-own-libraries.md index 1b379fdf6ec..135b3658be5 100644 --- a/issues/a-package-cannot-ship-its-own-libraries.md +++ b/issues/a-package-cannot-ship-its-own-libraries.md @@ -6,13 +6,11 @@ opened: 2026-09-27 # A package cannot ship its own libraries -A program on a file server is spawned from a memory image its parent read -(`SpawnArgs::image`), and nothing loads a `DT_NEEDED` library for it: the -kernel loads none at spawn, and an executable that names one is refused at its -own start (`toyos::relocate`) until the userland loader of -`issues/the-kernel-still-parses-what-userland-writes.md` loads them. A package -under `/apps//` whose binary needs a library it carries beside it fails -to start. +A program's own start loads the `DT_NEEDED` libraries it names (libc's +`__toyos_load_needed`), but a name without a `/` is looked for in +`/system/lib` alone (`sdk/libc/src/dl.rs`, `LIBRARY_DIR`). A package under +`/apps//` whose binary needs a library it carries beside it fails to +start. ## Exit condition diff --git a/issues/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md b/issues/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md index 55515880590..b2e8342f666 100644 --- a/issues/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md +++ b/issues/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md @@ -33,9 +33,9 @@ part of one and another can take several, so exporting the other count yields no number that returns to where it was. `peak_memory` is a high-water mark, which means a *fix* is invisible in it: a leak that stops leaking leaves the peak where it was. -`tests/guest/rust-tests/src/bin/abuse_tls_alloc.rs` has the harness shape. It -runs raw `thread_spawn`/`thread_join` over one explicitly `mmap`ed stack, so -the address space holds still while it measures. What it does not do is read a +`tests/guest/rust-tests/src/bin/abuse_thread_table.rs` has the harness shape. It +runs raw `thread_spawn`/`thread_join` over stacks it maps itself, so the +address space holds still while it measures. What it does not do is read a number. **So what is missing is narrow and worth building.** A test-only per-process 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 af7d37baa73..63013ffbd3e 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 @@ -8,17 +8,12 @@ opened: 2026-10-08 # A refused syscall writes a log record per call, and nothing bounds the caller 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: +per call, and the caller chooses how often: a refused spawn +(`kernel/src/loader/mod.rs`) writes a `spawn: : …` record, one for each +reason a file can be refused. -- `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 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 +A spawn of a file that is not an executable, in a loop, 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`), and what a storm of these pushes out is the middle of everybody else's. diff --git a/issues/a-tls-block-is-zeroed-twice-with-preemption-off.md b/issues/a-tls-block-is-zeroed-twice-with-preemption-off.md deleted file mode 100644 index 0cee3d5c08d..00000000000 --- a/issues/a-tls-block-is-zeroed-twice-with-preemption-off.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-03 ---- - -# A TLS block is zeroed twice with preemption off - -`kernel/src/loader/tls.rs` lays a block out in a zeroed heap buffer, then -copies it into an object whose frames `pmm::alloc_frame` zeroed -(`SharedMemObject::create`): every byte of the block is written three times. -Every `SYS_THREAD_SPAWN` (`process::spawn_thread`) and every `SYS_SPAWN` -(`loader`) builds one, and a syscall runs with preemption off from entry to -exit (`issues/syscall-preemption-is-incidental.md`). The block is whole pages -of its layout, a page for most programs, not the 2 MiB it once was. - -By reading, not measured. - -Owner: `issues/toyos-beats-linuxs-latency-on-the-t14.md`. - -**Exit**: `build_combined` zeroes no byte the PMM already zeroed. diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md index 9dcdb200fe1..c138cd05e86 100644 --- a/issues/an-executables-relro-stays-writable.md +++ b/issues/an-executables-relro-stays-writable.md @@ -12,7 +12,9 @@ of function pointers. The kernel maps it as the writable `PT_LOAD` that holds it, reads no `PT_GNU_RELRO` (`rg -i relro kernel/src lib/elf/src` matches nothing), and gives a program no way to take the write away once `toyos::relocate` has run. A stray or hostile write in a running program can -rewrite a vtable. +rewrite a vtable. A library's is read-only once libc's loader has relocated +it (`sdk/libc/src/dl.rs`, `protect`); an executable's is not, and the same +`SYS_VM_PROTECT` is what its start would call. Owner: the userland loader of `issues/the-kernel-still-parses-what-userland-writes.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 deleted file mode 100644 index e262d69009c..00000000000 --- a/issues/dlopen-write-at-is-not-the-sole-writer-of-a-mapped-module.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 those pages -while the relocation writes land. - -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. - -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/issues/dtv-capacity-is-a-workload-bound.md b/issues/dtv-capacity-is-a-workload-bound.md deleted file mode 100644 index 6afdf43f886..00000000000 --- a/issues/dtv-capacity-is-a-workload-bound.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-17 ---- - -# The DTV's capacity is a fixed bound over a quantity the workload sets, and its refusal is unrecoverable - -`kernel/src/loader/tls.rs` fixes `DTV_INITIAL_CAPACITY = 64`, and -`tls_alloc_block` (`kernel/src/syscall/vm.rs`) refuses a `module_id` above it -with `ResourceExhausted`, deliberately: the DTV is a fixed-capacity array the -kernel wrote, so there is nowhere to record the answer. `abuse_tls_alloc` gates -the refusal. - -The quantity bounded is *the number of TLS-carrying modules a process loads*, -which is set by the workload and by nothing else. This tree has just spent three -scheduler designs establishing that a bound over a workload-set quantity is a -defect rather than a policy, and this is one. - -**The refusal has no recoverable form.** The only caller is std's -`__tls_get_addr_slow` (`sdk/std/sys/pal/tls.rs`), and its own -comment says why it cannot pass the error on: *"`__tls_get_addr`'s ABI is an -address and there is nobody to return an error to: a refusal added to `offset` is -a pointer near the top of the address space that the caller would then -dereference."* It answers `rtabort!("no TLS block for a dlopen'd module")`. So -the 65th TLS-carrying module in a process is a process abort at an arbitrary -point in its execution, not an error a `dlopen` caller could handle. - -**How close the tree already is.** Nothing shipped reaches it: measured with -`toyos-elf`, all 20 programs in `system.toml` have zero `DT_NEEDED` and zero TLS -relocations, so every shipped process has exactly one TLS module. The pressure -comes from the hosted rustc, which loads proc-macro dylibs with `dlopen`: -`ls rust/build/x86_64-unknown-toyos/stage2/lib/*.so | wc -l` → 18, and each -proc-macro one measured (`libserde_derive`, `libdarling_macro`) carries 165 -`DTPMOD64` + 165 `DTPOFF64` relocations — i.e. it is a TLS module. A crate graph -with more than ~62 proc-macro dependencies aborts rustc. - -Not fixed here. A growable DTV is a change to a structure `__tls_get_addr`'s -naked fast path indexes directly, and the honest fix may be that the kernel -should not own the DTV at all: moving the loader out to Ring 3 puts the DTV in -the process's own address space, where that address space is the bound. This -came out of the 2026-08-17 scoping of exactly that move. - -**Promoted to `defect` 2026-08-25** (finding-lifecycle ruling). The body already -argues it: a fixed bound over a quantity the workload sets is a defect rather -than a policy by this tree's own rule, and this one's refusal has no recoverable -form — the 65th TLS-carrying module is an `rtabort!` at an arbitrary point, not -an error a `dlopen` caller can handle. -Owed by the Ring-3 loader move, which is where the DTV stops being the kernel's -to size. diff --git a/issues/kernelslice-outlives-its-allocation.md b/issues/kernelslice-outlives-its-allocation.md deleted file mode 100644 index 818e714df4d..00000000000 --- a/issues/kernelslice-outlives-its-allocation.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-22 ---- - -# A `KernelSlice` can outlive the allocation it was built from - -`kernel/src/mm/region.rs`. `KernelSlice::whole` is now the type's only -constructor and it reads the base and the size off an `mm::Allocation`, so the -size a bound is checked against is the allocation's — that half is closed. The -half that is not: `KernelSlice` is `Copy` and carries no lifetime, so a copy -taken while the `PageAlloc`/`OwnedAlloc` is alive stays nameable after it has -been dropped and its pages reissued. - -The shape that answers it is a view that borrows what it came out of, with -one named way to reach `'static` — as the deleted `mm::Dma<'pool>` did with -`DmaPool::leak`: `KernelSlice<'alloc>`. - -What makes it more than a `git sed`: `elf::cache` holds a `cached_image: -KernelSlice` over pages the cache owns and hands it to every process that -clones the library, and `LoadedLib`, `TlsModule` and `UserStack` each store one -whose allocation lives in a different structure from the slice. Each of those is -a real lifetime that has to be written down rather than inferred, and getting -one wrong is a compile error rather than a silent regression — which is the -argument for doing it, and the reason it is not a small change. - -No known live bug: every current holder outlives its slice today, checked by -hand when `from_raw` was deleted (2026-08-22). This is the residual, not a -reproduction. - -**Promoted to `defect` 2026-08-25** (finding-lifecycle ruling; unsure resolves -to promote, and a memory-safety residual is the worst thing to lose to a wrong -fold). `KernelSlice` being `Copy` with no lifetime means nothing but hand-checking -stops a copy naming pages that have been reissued — and the hand-check was done -once, in 2026-08-22, against holders that have moved since. Owed by whoever writes the four holders' lifetimes -down: `elf::cache`'s `cached_image`, `LoadedLib`, `TlsModule` and `UserStack`. diff --git a/issues/libc-dl-iterate-phdr-allocates-on-every-walk.md b/issues/libc-dl-iterate-phdr-allocates-on-every-walk.md index 9cd1b171f3a..7b6198b5307 100644 --- a/issues/libc-dl-iterate-phdr-allocates-on-every-walk.md +++ b/issues/libc-dl-iterate-phdr-allocates-on-every-walk.md @@ -4,16 +4,13 @@ kind: defect opened: 2026-09-29 --- -# libc's `dl_iterate_phdr` allocates on every walk +# Nothing holds that libc's `dl_iterate_phdr` walks without allocating -`sdk/libc/src/link.rs` sizes and reads a whole `SYS_QUERY_MODULES` -answer into a heap `Vec`, and builds each module's NUL-terminated name in a -second one: every walk allocates twice and makes at least two syscalls. -libunwind calls `dl_iterate_phdr` to find a frame's unwind tables, so a C++ -`throw std::bad_alloc` with the heap exhausted dies in libc's allocator -instead of reaching its handler. The answer's size grows with every library -and path, so no fixed buffer holds it: an allocation-free walk needs a query whose -answer a bounded buffer holds, which is an ABI change. +libunwind calls `dl_iterate_phdr` to find a frame's unwind tables, so a walk +that allocates lets a C++ `throw std::bad_alloc` with the heap exhausted die +in libc's allocator instead of reaching its handler. `sdk/libc/src/link.rs` +walks the process's own module list (`toyos::tls`), whose names are kept +NUL-terminated, and allocates nothing, but no case shows it. -**Exit**: `dl_iterate_phdr` makes no heap allocation, and a guest C case walks -every module with `malloc` exhausted and visits each. +**Exit**: a guest C case walks every module with `malloc` exhausted and +visits each. diff --git a/issues/libc-dlsym-rtld-default-panics-as-unimplemented.md b/issues/libc-dlsym-rtld-default-panics-as-unimplemented.md deleted file mode 100644 index 03c75ed1f80..00000000000 --- a/issues/libc-dlsym-rtld-default-panics-as-unimplemented.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-29 ---- - -# libc's `dlsym(RTLD_DEFAULT, …)` panics as unimplemented - -`dlfcn.h` defines `RTLD_DEFAULT` as the null handle, and libc's `dlsym` -(`sdk/libc/src/misc.rs`) panics on it: a handle is a module index plus -one, and `SYS_DLSYM` answers from one loaded library by index. Nothing searches -the global scope — the executable's exports, then every library in load order — -which is what glibc's `dlsym(RTLD_DEFAULT, …)` answers from. A C program that -probes the process for an optional symbol dies. The kernel-side sibling is -`issues/a-dlopened-library-never-binds-to-the-executables-exports.md`: -both want the executable inside the global scope. - -**Exit**: `dlsym(RTLD_DEFAULT, name)` answers the first definition of `name` in -the executable and then every loaded library in load order, and NULL when there -is none, and a guest C case asserts both. diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index dc2951c8495..fa1dc31aabf 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -18,8 +18,7 @@ These answer failure in their POSIX form and do nothing the failure and reads the `rlimit` it did not get. - `gethostname`, `ENOSYS`: no host name is published to a process. - `msync`, `ENOSYS`: no mapping is of a file, so there is nothing to write back. -- `mmap` of a file, `ENODEV`, and `mmap` or `mprotect` of executable memory, - `ENOTSUP`: the kernel maps neither. +- `mmap` of a file, `ENODEV`: the kernel maps no file. - `fcntl` (`sdk/libc/src/fdreq.rs`): a record lock, `EINVAL`, POSIX's answer for a file that supports no locking; `F_GETFL`, `F_SETFL`, `F_GETOWN` and `F_SETOWN`, `ENOSYS`. @@ -28,6 +27,12 @@ Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no call replacing a process's image; `setsid` and `getsid`, no POSIX session; `fchown`, no owner; `getpwnam_r` and `getpwuid_r`, no user database. +Refused by design, and owed nothing: `mmap` or `mprotect` of memory writable +and executable at once, `ENOTSUP`. A program writes its code and then +protects it read and execute, as libc's loader does; no page is both +(orchestrator, with the userland loader, which made read and execute a +protection a program may ask for). + **Exit**: each above either does what POSIX says, asserted by a guest C case that reads its effect back, or is ruled out by the owner and moved to the list above. diff --git a/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md b/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md index ed5e8e71bb0..213c78e807a 100644 --- a/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md +++ b/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md @@ -23,8 +23,8 @@ holds is spawned from a copy in one memory object, which `SYS_SHM_CREATE` now makes of 4 KiB frames in any order (`kernel/src/object/shm.rs`), so the hosted clang's 148 MB image no longer needs one physical run; `hosted_clang_hello`, which was refused `ENOMEM` linking, is the check. What is still one physical -run is a device-lendable object (`SHM_DEVICE`, `SharedMemObject::create_run`) -and each `dlopen` image (`issues/a-dlopen-image-is-one-contiguous-run.md`). +run is a device-lendable object (`SHM_DEVICE`, `SharedMemObject::create_run`); +a library is laid out in the process's own private memory by libc's loader. **The constraint that makes this a track, measured on the T14** (run 29 readback, `PMM: 100/16038MB used` at 11 s with four user processes and three diff --git a/issues/spawning-a-process-costs-toyos-up-to-four-times-linuxs-on-the-t14.md b/issues/spawning-a-process-costs-toyos-up-to-four-times-linuxs-on-the-t14.md index 40f2d220f3a..4abd9a85fb8 100644 --- a/issues/spawning-a-process-costs-toyos-up-to-four-times-linuxs-on-the-t14.md +++ b/issues/spawning-a-process-costs-toyos-up-to-four-times-linuxs-on-the-t14.md @@ -70,6 +70,5 @@ under deep idle. current-state comparison, or each remaining difference measured to its parts and justified. -**Owner**: process creation (`kernel/src/process.rs`, `kernel/src/elf/`, the -SDK's spawn and `sdk/libc/src/spawn.rs`) and, for the parallel build, the -scheduler (`kernel/src/sched/`). +**Owner**: process creation (`kernel/src/process.rs`, the SDK's spawn and +`sdk/libc/src/spawn.rs`) and, for the parallel build, the scheduler (`kernel/src/sched/`). diff --git a/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md b/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md deleted file mode 100644 index cae37e3ea35..00000000000 --- a/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-29 ---- - -# std reads a `SYS_QUERY_MODULES` byte count as a module count - -Two readers, one in std's ToyOS backend and one in the fork's backtrace crate, -take `query_modules`'s `Ok(n)` as a record count -and read `size_of::()`-byte records from offset 0 until one would -pass the end of their 4096-byte buffer: - -- the unwinder, `sdk/std/sys/pal/mod.rs`, - `eh_frame::load_modules`, reads each as `base`/`text_end`/`eh_frame_hdr`; -- the backtrace crate, `rust/library/backtrace/src/symbolize/gimli/libs_toyos.rs`, - `native_libraries`, makes a `Library` of each whose one segment is - `text_end - base` bytes long, a subtraction that can underflow on them. - -`n` counts **bytes** (`toyos_abi::syscall::query_modules`'s doc), so every -"record" after the real ones is the packed path strings and then the zeroed -tail of the buffer. Both also take `n > buf.len()`, which means nothing was -written, as an answer and read the zeroes: the call answers a short buffer -with `Ok`, so neither ever reaches the `Err` arm that grows its buffer. - -No wrong frame has been seen: a path's ASCII read as a `u64` lies far above the -user half, so no user PC falls in the range it makes, and a zeroed record's -range is empty. That is luck in the bytes, not a check. - -**Exit**: `load_modules` and `native_libraries` each decode the answer with -`toyos_abi::syscall::modules` over the `n` bytes the call reported, and grow -the buffer to `n` when `n` exceeds it. diff --git a/issues/the-capability-end-state-is-twelve-answers.md b/issues/the-capability-end-state-is-twelve-answers.md index 0b6834ec9f7..4ccfd294e70 100644 --- a/issues/the-capability-end-state-is-twelve-answers.md +++ b/issues/the-capability-end-state-is-twelve-answers.md @@ -141,11 +141,9 @@ table** can still reach: - **its own execution** — `SYS_EXIT`, `SYS_THREAD_EXIT`, `SYS_THREAD_SPAWN`, `SYS_THREAD_JOIN`, `SYS_FUTEX_WAIT`/`WAKE`, `SYS_NANOSLEEP`, - `SYS_SET_THREAD_NAME`; -- **its own address space** — `SYS_VM_*`, `SYS_TLS_ALLOC_BLOCK`, - `SYS_STACK_INFO`; -- **its own record** — `SYS_GETPID`, `SYS_GET_ENV`, `SYS_ENDOWMENTS`, - `SYS_QUERY_MODULES`, `SYS_GETCWD`, `SYS_CHDIR`; + `SYS_SET_THREAD_NAME`, `SYS_SET_THREAD_POINTER`; +- **its own address space** — `SYS_VM_*`, `SYS_STACK_INFO`; +- **its own record** — `SYS_GETPID`, `SYS_GET_ENV`, `SYS_ENDOWMENTS`; - **machine facts** — `SYS_CLOCK_EPOCH`, `SYS_CPU_COUNT`, `SYS_RANDOM`, and `SYS_SYSINFO`'s header (total and used memory, the CPU count, the live-thread count, the uptime and the two CPU-time accumulators) but not the roster after @@ -158,8 +156,8 @@ table** can still reach: (`SYS_OPEN`, `SYS_READDIR`, `SYS_DELETE`, `SYS_MKDIR`, `SYS_RMDIR`, `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. + and is gone: a program loads its own libraries (libc's loader), reading + each through its own namespace. **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 5bb38420b7d..395c4b6c7cd 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 @@ -121,13 +121,12 @@ Beside that order, each waiting on something outside it: 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; 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). + **Done**: spawn (#536, `SpawnArgs::image`), and dlopen: libc's loader + reads the library through `toyos::files::read_image` wherever it is + served and loads it itself, each process holding its own library pages + (orchestrator, under the memory design, whose later stages bring sharing + back through memory objects); `SYS_DLOPEN` is gone with the kernel's + loader (`issues/the-kernel-still-parses-what-userland-writes.md`). 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 b67e0425433..9de298846a5 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -25,18 +25,30 @@ parses most of it. **Stage 1 is built: an executable relocates itself** (`toyos::relocate`, called first by std's and libc's `_start`), and the kernel reads no -executable's dynamic section, symbol tables or relocations, and loads no -library at spawn. Left in Ring 0: `dlopen` and its four sibling syscalls, the -TLS block, and the program-header parse. **The interval it opens, owed by the -stage that brings the userland loader:** an executable with a `DT_NEEDED` -does not start (its own `_start` refuses it: exit 127 and a line on slot 2, -or, holding no slot 2, the kernel's handle-fault exit), so `std_tls`, the -guest test of a startup library's TLS beside the executable's, is deleted at -the commit that lands stage 1 and comes back with that stage. Nothing shipped -links a library at load time. The kernel's static TLS block holds the -executable's module alone, at offset 0, so `TlsModule::base_offset` and -`Static::tpoff`'s offset are 0 at every caller; that stage places modules in -userland and deletes them with the kernel's TLS build. +executable's dynamic section, symbol tables or relocations. + +**Stage 2 is built: the loader is the program's.** A program lays out its own +thread-local storage (`toyos::tls`): its module list, every thread's area and +DTV, which grows in the process's own memory, and `__tls_get_addr`'s blocks. +The kernel's only part is the thread pointer a thread names +(`SYS_SET_THREAD_POINTER`). libc's loader (`sdk/libc/src/dl.rs`) lays a +library out in private memory, relocates it, protects its pages, its RELRO +read-only, and publishes it in the module list; `dlopen`, `dlsym`, `dladdr` and +`dl_iterate_phdr` read that list, and so do std's unwinder and the backtrace +crate. `SYS_DLOPEN`, `SYS_DLSYM`, `SYS_DLCLOSE`, `SYS_TLS_ALLOC_BLOCK` and +`SYS_QUERY_MODULES` are deleted, and a program may protect its own memory read +and execute, never write and execute at once. An executable's start loads the +libraries it names (`DT_NEEDED`) through the same loader before `main` +(`__toyos_load_needed`), and `std_tls` holds it. What is left in Ring 0 is the +program-header parse, which the end state keeps. + +**The interval it leaves.** libtoyos_c is built from std, so std cannot link it: +std reaches the loader through a weak symbol, and a Rust executable that names +a library links libtoyos_c itself (`std_tls` does), or its start refuses it by +name and exits 127. A C program always has it. The exit: every Rust executable +links the loader as a C program does, through the target's link of its C +runtime. AArch64 shared objects reach their thread-locals through TLS +descriptors, which the loader refuses. The evidence that it is worth doing: nine dated commits over eleven days fixed userland-reachable kernel defects in this code, six of them machine-wide panics; @@ -44,11 +56,8 @@ seven of the loader's twelve bounds are over quantities a workload sets and two have no bound at all; and two of those ceilings are *already* exceeded by 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 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. +Its deadline, the day an image ships a hosted rustc whose shared objects the +kernel would have loaded, is met: those bounds are the process's own now. **Move 2 — filesystem daemons**, sequenced after the completion architecture. A crafted image attacks the kernel rather than a sandboxed daemon. The FS daemon diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index f1250d7cdb3..794d21e2581 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -159,9 +159,10 @@ bound). Heaviest: `arch::cpu` (57 references), `arch::percpu` (28), 63, one root); `mm/paging.rs:1-7` states invalidation is INVPCID/INVLPG. ARM splits kernel (TTBR1) from user (TTBR0), carries AttrIndx→MAIR, AP[2:1], UXN/PXN, and ASIDs in the TTBR. -- **TLS.** The kernel builds x86 variant II (`loader/tls.rs:1`); std's - `__tls_get_addr` uses `fs:`. AArch64 is variant I; LLVM emits TLSDESC by - default. +- **TLS.** Each program lays its own out (`sdk/toyos/src/tls`), variant II + on x86-64 and variant I on AArch64; std's `__tls_get_addr` uses `fs:`. + LLVM emits TLSDESC by default on AArch64, which libc's loader refuses, so + no AArch64 shared object reaches its thread-locals yet. - **Calling conventions.** `extern "sysv64"` at `main.rs:205`, `deadline.rs:194`, `sched/driver.rs:892`, `i8042/mod.rs:490`, `loader/src/main.rs:785`. diff --git a/issues/what-is-owed-on-file-size.md b/issues/what-is-owed-on-file-size.md index 843fca33951..1cecea7d741 100644 --- a/issues/what-is-owed-on-file-size.md +++ b/issues/what-is-owed-on-file-size.md @@ -13,7 +13,8 @@ what survives it is a different question. **Answered:** - `elf.rs` → `lib/elf/` (host-tested, crafted-input corpus), `e2c6a06`; the - mapping half stayed as `kernel/src/elf/`, `42b29c9`. + mapping half stayed as `kernel/src/elf/`, `42b29c9`, until libc's loader + took a library's loading out of the kernel. - `compositor/main.rs` → `toyos-desktop/` (pure, since moved to `system/compositor/desktop/`), `763712b`; the effects shell is `system/compositor/` over five files with a 71-line `main.rs`, `72705d9`. diff --git a/kernel/pure/vm.rs b/kernel/pure/vm.rs index 79fa1b181f7..d98bab73c37 100644 --- a/kernel/pure/vm.rs +++ b/kernel/pure/vm.rs @@ -516,6 +516,22 @@ mod tests { assert!(!Prot::ReadExec.within(Prot::ReadWrite) && Prot::None.within(Prot::None)); } + /// A program writes its code and then protects it executable, never both at + /// once: no protection a range can hold allows a write and an execution, and + /// an object's mapping, which another may write through, never becomes + /// executable, since a protect is bounded by the mapping's own. + #[test] + fn no_protection_allows_a_write_and_an_execution() { + for prot in [Prot::None, Prot::Read, Prot::ReadWrite, Prot::ReadExec] { + // Every protection: one more is a compile error here until it is listed. + match prot { + Prot::None | Prot::Read | Prot::ReadWrite | Prot::ReadExec => {} + } + assert!(!(prot.allows(Access::Write) && prot.allows(Access::Exec)), "{prot:?}"); + } + assert!(!Prot::ReadExec.within(Prot::Read) && !Prot::ReadExec.within(Prot::ReadWrite)); + } + /// The inventory against a page map of the same space, through a /// deterministic mix of every change: what is held, what is free and what /// the free index can place always agree. diff --git a/kernel/src/arch/aarch64/cpu.rs b/kernel/src/arch/aarch64/cpu.rs index e79989281c3..dd22c6b1f5f 100644 --- a/kernel/src/arch/aarch64/cpu.rs +++ b/kernel/src/arch/aarch64/cpu.rs @@ -69,7 +69,7 @@ pub fn thread_pointer() -> u64 { /// Install the thread pointer the next return to EL0 runs with. /// # Safety -/// `tp` is the thread's own, which the kernel computed for it. +/// `tp` is the running thread's. pub unsafe fn write_thread_pointer(tp: u64) { // SAFETY: `TPIDR_EL0` is EL0's register; the kernel never reads through it. unsafe { asm!("msr tpidr_el0, {}", in(reg) tp, options(nomem, nostack, preserves_flags)) }; diff --git a/kernel/src/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index 3c3de77c434..0c13cb70c85 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -539,6 +539,14 @@ pub fn thread_pointer() -> u64 { read_fs_base() } +/// Install the thread pointer this thread returns to Ring 3 with. +/// # Safety +/// `tp` is canonical, and the running thread's. +pub unsafe fn write_thread_pointer(tp: u64) { + // SAFETY: the caller's. + unsafe { write_fs_base(tp) }; +} + /// Leave the current stack for good and run `func` on the one ending at `top`, /// with a zeroed frame chain so a panic there backtraces instead of walking off /// the top. diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs deleted file mode 100644 index 5026380fea6..00000000000 --- a/kernel/src/elf/mod.rs +++ /dev/null @@ -1,513 +0,0 @@ -//! Loading a shared object: the effects half of ELF. -//! -//! Decoding lives in `toyos-elf`, pure and host-tested; this module owns -//! everything that touches memory — allocation, segment reads, the private -//! writable window, and the kernel's own ceilings. An ELF is untrusted input: -//! nothing here panics on a malformed one, and refusals are `&'static str` so -//! callers can log them beside the path. - -// Every unsafe block in this module tree must carry a `SAFETY:` comment; -// this `warn` is what turns a missing one into a CI failure. -#![warn(clippy::undocumented_unsafe_blocks)] - -mod reloc; - -pub use reloc::{ - apply_dtpmod_relocs, apply_dtpoff_relocs, apply_tpoff_relocs, rebase_relative_relocs, - resolve_dlopen_relocs, -}; - -use crate::mm::{align_2m_checked, KernelSlice, MAX_HEAP_ALLOC, PAGE_2M, PAGE_BYTES}; -use crate::UserAddr; -use toyos_elf::dynamic::{Dynamic, InitArray}; -use toyos_elf::section::{SectionTable, SHT_DYNSYM}; -use toyos_elf::sym::{Sym, SymTab}; -use toyos_elf::{ - rela, Extent, GnuHash, ImageRange, Layout, ProgramHeaderTable, 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); - -/// [`Layout::parse`] plus the kernel's ceiling on section header table size, -/// checked once here since not every caller can refuse a malformed file. -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() - .is_some_and(|s| s.byte_len() > MAX_HEAP_ALLOC) - { - return Err("ELF: section header table larger than one kernel allocation"); - } - Ok(layout) -} - -/// A module's memory: one run of 2 MiB pages the kernel lays an image out in, -/// addressed as one slice, and mapped as the object it is. -pub struct Run(alloc::sync::Arc); - -impl Run { - /// `size` bytes of zeroed memory in one run. - pub fn new(size: usize) -> Option { - crate::object::shm::SharedMemObject::create_run(&crate::mm::charge::current(), size as u64).ok().map(Self) - } - - pub fn ptr(&self) -> *mut u8 { - crate::DirectMap::from_phys(self.phys()).as_mut_ptr() - } - - pub fn phys(&self) -> u64 { - self.0.run().expect("elf: a module's memory is a run").0 - } - - pub fn size(&self) -> usize { - self.0.size() as usize - } - - /// This allocation as a bounds-checked window; a `&mut [u8]` here would carry `noalias`/`dereferenceable` into LLVM over pages that become a user mapping. - pub fn window(&self) -> KernelSlice { - KernelSlice::whole(self) - } - - pub fn object(&self) -> &alloc::sync::Arc { - &self.0 - } -} - -// SAFETY: `ptr()`/`size()` are the run the object owns for as long as `self` holds it. -unsafe impl crate::mm::Allocation for Run { - fn ptr(&self) -> *mut u8 { - Run::ptr(self) - } - fn size(&self) -> usize { - Run::size(self) - } -} - -/// One TLS module's placement within a thread's combined TLS block. -#[derive(Clone)] -pub struct TlsModule { - pub template: Option, - /// Total TLS size including `.tbss`, which is zeroed rather than copied. - pub memsz: usize, - /// Byte offset of this module within the combined block (static modules only). - pub base_offset: usize, - /// DTV module ID, 1-based; `__tls_get_addr` indexes the DTV with it. - pub module_id: u64, - /// True for modules present at process startup; a `dlopen`ed module's - /// block is allocated on demand through `SYS_TLS_ALLOC_BLOCK`. - pub is_static: bool, -} - -/// Everything a cross-module TLS relocation has to resolve against. -pub struct TlsModuleInfo<'a> { - pub libs: &'a [LoadedLib], - pub modules: &'a [TlsModule], -} - -pub struct LoadedLib { - /// The one run the module's image is loaded into, this process's own. - pub memory: Run, - pub user_base: UserAddr, - /// Bounds-checked view of the whole loaded image. - pub image: KernelSlice, - /// `.dynsym`, clamped to the entries it actually holds. - dynsym: Option, - dynstr: Option, - pub tls_template: Option, - rela: Option, - jmprel: Option, - gnu_hash: 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, - /// `PT_GNU_EH_FRAME`, for DWARF unwinding. - pub eh_frame_hdr: Option, - /// The program header table, which `load_shared_lib` refuses a module without. - pub phdrs: ProgramHeaderTable, - /// `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. - /// Rounding this outward would make a page of some other module's `.text` - /// writable. - pub rw_lo: u64, - pub rw_hi: u64, -} - -impl LoadedLib { - /// Protection for the page at `offset`: exec below the writable window, - /// write inside it, read-only above — over-permissive, never under, for - /// an unusual segment layout. - fn page_prot(&self, offset: u64) -> kernel::vm::Prot { - use kernel::vm::Prot; - if offset < self.rw_lo { - Prot::ReadExec - } else if offset < self.rw_hi { - Prot::ReadWrite - } else { - Prot::Read - } - } - - /// Give this module a virtual address in `space` and map its pages there, - /// each run of pages at its final protection — mapping the whole image - /// writable first would leave `.text` writable. `None` where the space has - /// no room. - pub fn map_into(&self, space: &crate::process::PageTables) -> Option { - use crate::mm::space::At; - let (image, size) = (self.memory.object(), self.memory.size() as u64); - let mut space = space.lock(); - let base = space.reserve(size, At::ANYWHERE, kernel::vm::Prot::None).ok()?; - let ceil = |x: u64| x.next_multiple_of(crate::mm::PAGE_SIZE).min(size); - let cuts = [0, ceil(self.rw_lo), ceil(self.rw_hi), size]; - for cut in cuts.windows(2) { - let (from, to) = (cut[0], cut[1]); - if from < to { - space.map(image.clone(), from, to - from, self.page_prot(from), At::Fixed(base + from)).ok()?; - } - } - Some(base) - } - - /// This module's symbol table, or an empty one when it declares none. - pub fn symbols(&self) -> SymTab<'_> { - let Some(syms) = &self.dynsym else { - return SymTab::empty(); - }; - // A missing `DT_STRTAB` still leaves the symbol count intact; only names are lost. - let strs: &[u8] = match &self.dynstr { - // SAFETY: `dynstr` came from `ModuleImage::slice`'s bounds check, - // outlives this borrow via `LoadedLib`, and was refused by - // `rela::tables_outside_window` if it met the relocation window. - Some(strs) => unsafe { strs.as_slice() }, - None => &[], - }; - // SAFETY: same bounds argument as `dynstr` above; `syms` is additionally - // clamped to the declared symbol count. - unsafe { SymTab::new(syms.as_slice(), strs) } - } - - fn gnu_hash(&self) -> Option> { - // SAFETY: same bounds argument as `symbols` above. - GnuHash::parse(unsafe { self.gnu_hash.as_ref()?.as_slice() }) - } - - /// 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 - /// `user_base`. - pub fn user_end(&self) -> u64 { - self.user_base.raw() + self.span - } - - /// The address this module gives a symbol it defines. - pub fn resolve(&self, name: &str) -> Option { - let symbols = self.symbols(); - let idx = self.gnu_hash()?.lookup(name, &symbols)?; - self.address_of(&symbols.get(idx)?) - } - - /// 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).and_then(|(_, sym)| lib.address_of(&sym)) -} - -/// 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, - extent: Extent, -} - -impl ModuleImage { - fn slice(&self, vaddr: u64, size: u64) -> Result { - 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.extent.max().saturating_sub(vaddr)) - } - - fn optional(&self, vaddr: Option, size: u64) -> Result, &'static str> { - match vaddr { - Some(v) => Ok(Some(self.slice(v, size)?)), - None => Ok(None), - } - } -} - -/// Read `dst.size()` bytes from a file backing straight into `dst`. -/// -/// `dst` is a `KernelSlice`, so the read is bounded by its own allocation. -pub(crate) fn read_backing_into( - backing: &crate::file_backing::SharedImage, - offset: u64, - dst: KernelSlice, -) { - let mut remaining = dst.size(); - let mut file_off = offset; - let mut buf_off = 0usize; - let mut page_buf = [0u8; PAGE_BYTES]; - while remaining > 0 { - let off_in_block = (file_off % 4096) as usize; - let chunk = (4096 - off_in_block).min(remaining); - backing.read_page(file_off - off_in_block as u64, &mut page_buf); - // SAFETY: `copy_from` asserts `buf_off + chunk <= dst.size()`; `page_buf` - // and `dst` cannot overlap, and nothing else can see `dst` yet. - unsafe { dst.copy_from(buf_off, &page_buf[off_in_block..off_in_block + chunk]) }; - file_off += chunk as u64; - buf_off += chunk; - remaining -= chunk; - } -} - -/// Load a shared object into one contiguous allocation and apply its -/// `R_X86_64_RELATIVE` relocations. -pub fn load_shared_lib(backing: &crate::file_backing::SharedImage) -> 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)?; - - 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 extent.min() != 0 { - return Err("ELF: a shared object must begin at vaddr 0"); - } - // `SYS_QUERY_MODULES` answers with the mapped table. - let phdrs = layout.program_headers().ok_or("ELF: no PT_LOAD maps the program header table")?; - // No writable segment yields an empty window; no relocation can target it. - 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) = 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 alloc = - Run::new(load_size).ok_or("dlopen: allocation failed")?; - // Every offset below is bounded against what the PMM actually returned, not `load_size`. - let image = alloc.window(); - - // SAFETY: `image` is freshly built from the exclusively-owned `alloc`; nothing - // else holds a reference yet. - unsafe { - image.zero(); - } - - 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 = module.at(seg.image()).subslice(0, seg.filesz() as usize); - read_backing_into(backing, seg.file_offset(), dst); - } - - 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() }) - } - None => Dynamic::default(), - }; - - let gnu_hash = match dyn_info.gnu_hash { - Some(vaddr) => Some(module.slice_to_end(vaddr)?), - None => None, - }; - - // Prefer the section header's count: `.gnu.hash` indexes only exports, so a - // count derived from it is short whenever the module imports anything. - let declared = dynsym_count_from_sections(backing, &layout) - .filter(|&n| n > 0) - .or_else(|| { - // SAFETY: same bounds argument as above; still within the - // exclusive-construction phase. - let table = GnuHash::parse(unsafe { gnu_hash.as_ref()?.as_slice() })?; - table.sym_count() - }) - .unwrap_or(0); - - // Clamped to `declared`, so `r_sym` is bounded by the image and by the table, whichever is smaller. - let dynsym = match dyn_info.symtab { - Some(vaddr) => { - let whole = module.slice_to_end(vaddr)?; - let count = declared.min(whole.size() / toyos_elf::sym::ENTRY_SIZE); - Some(whole.subslice(0, count * toyos_elf::sym::ENTRY_SIZE)) - } - None => None, - }; - 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)?), - None => None, - }; - let jmprel = match dyn_info.jmprel { - Some(t) => Some(module.slice(t.vaddr, t.size)?), - None => None, - }; - - // 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; - (at, at + slice.size() as u64) - }; - let tables = rela::ReadTables { - 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, PAGE_BYTES as u64)?; - let rules = rela::Rules { - extent, - window, - 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(); - 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()) }; - } - } - - let tls_template = layout.tls().map(|tls| module.at(tls.template())); - - Ok(LoadedLib { - memory: alloc, - user_base: UserAddr::new(base_phys), - image, - dynsym, - dynstr, - tls_template, - rela, - jmprel, - gnu_hash, - rules, - eh_frame_hdr: layout.eh_frame_hdr(), - phdrs, - init_array, - span: layout.span(), - rw_lo, - rw_hi, - }) -} - -/// `.dynsym`'s entry count as the section header table declares it, or `None` -/// when the file does not say. -fn dynsym_count_from_sections( - backing: &crate::file_backing::SharedImage, - layout: &Layout, -) -> Option { - 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); - Some((dynsym.size / entry_size) as usize) -} - -/// The entries of one optional relocation table. -/// -/// A free function, not a closure: the iterator borrows the kernel pages the -/// table lives in, not the caller's frame. -fn table_entries(table: &Option) -> impl Iterator + '_ { - // SAFETY: every `KernelSlice` here came from `ModuleImage::slice`'s bounds - // check, and `rela::tables_outside_window` refused any image whose tables - // meet the range the writes are bounded to. - table - .iter() - .flat_map(|slice| RelaTable::new(unsafe { slice.as_slice() }, crate::arch::ELF_MACHINE).iter()) -} diff --git a/kernel/src/elf/reloc.rs b/kernel/src/elf/reloc.rs deleted file mode 100644 index 8876b3871b1..00000000000 --- a/kernel/src/elf/reloc.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Applying relocations to a loaded module. -//! -//! Every write goes through [`LoadedLib::write_at`]; every offset given to 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 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. Each function answers how many -//! it left, and the spawn or `dlopen` it serves says the sum in its one record: -//! 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, LoadedLib, TlsModule, TlsModuleInfo}; -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. - /// - /// # Safety - /// 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"); - 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, 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 }) - } - - fn dtpmod_entries(&self) -> impl Iterator)> + '_ { - self.entries(|op| match op { Op::DtpMod64(sym) => Some(sym), _ => None }) - } - - /// 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, - }) - } -} - -/// Point every `R_X86_64_RELATIVE` slot into the image at `lib.user_base`. -/// -/// 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()) }; - } -} - -/// Bind a `dlopen`ed module's `GLOB_DAT`/`JUMP_SLOT` slots to symbols the -/// process already has; answers how many it left unresolved. -pub fn resolve_dlopen_relocs(lib: &LoadedLib, other_libs: &[LoadedLib]) -> u64 { - let symbols = lib.symbols(); - let mut unresolved = 0; - for (offset, sym) in lib.bind_entries() { - let name = relocated_symbol(symbols, sym).name_in(symbols.strings()); - match other_libs.iter().find_map(|other| other.resolve(name)) { - // SAFETY: rela::tables_outside_window refused any image whose tables meet the window these writes land in. - Some(addr) => unsafe { lib.write_at::(offset, addr.raw()) }, - None => unresolved += 1, - } - } - unresolved -} - -/// Apply `R_X86_64_TPOFF64` and `R_X86_64_TPOFF32`: the initial-exec TLS -/// model, a fixed offset from the thread pointer. Answers how many references -/// name a symbol no module defines; each is written as zero. -/// -/// 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 { - 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) { - tpoff(r)?; - } - for (_, r) in lib.entries(tpoff32) { - tpoff32_value(tpoff(r)?.unwrap_or(0))?; - } - - let mut unresolved = 0; - 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) { - let value = tpoff(r)?; - unresolved += u64::from(value.is_none()); - // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, tpoff32_value(value.unwrap_or(0))?) }; - } - Ok(unresolved) -} - -/// A `TPOFF32`'s field is 32 bits the instruction sign-extends; a value outside -/// them names some other address than the one resolved. -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. -/// Answers how many name a symbol no module defines; each is given this -/// module's own id. -pub fn apply_dtpmod_relocs(lib: &LoadedLib, module_id: u64, tls_info: &TlsModuleInfo) -> u64 { - let mut unresolved = 0; - for (offset, sym) in lib.dtpmod_entries() { - let mid = resolve_dtpmod(lib, sym, module_id, tls_info); - unresolved += u64::from(mid.is_none()); - // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, mid.unwrap_or(module_id)) }; - } - unresolved -} - -/// 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. Answers how many name -/// a symbol no module defines; each is written as zero. -pub fn apply_dtpoff_relocs(lib: &LoadedLib, tls_info: &TlsModuleInfo) -> Result { - 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) { - resolve(r)?; - } - let mut unresolved = 0; - for (offset, r) in lib.entries(dtpoff) { - let value = resolve(r)?; - unresolved += u64::from(value.is_none()); - // SAFETY: see write_at's `# Safety`. - unsafe { lib.write_at::(offset, value.map_or(0, |(_, at)| at.get())) }; - } - Ok(unresolved) -} - -/// 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 { - let Some(segment) = lib.tls().and_then(TlsSegment::occupied) else { - continue; - }; - 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, segment, sym)); - } - } - None -} - -/// The module id one `DTPMOD64` names, or `None` for a symbol no module defines. -fn resolve_dtpmod(lib: &LoadedLib, sym: Option, self_module_id: u64, tls_info: &TlsModuleInfo) -> Option { - let Some(sym) = sym else { return Some(self_module_id) }; - let symbols = lib.symbols(); - let named = relocated_symbol(symbols, sym); - if named.is_defined() { - return Some(self_module_id); - } - let name = named.name_in(symbols.strings()); - defining_module(name, tls_info).map(|(module, _, _)| module.module_id) -} - -/// `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; 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 = sym.name_in(symbols.strings()); - let Some((module, segment, defined)) = defining_module(name, tls_info) else { - return Ok(None); - }; - let at = defined.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment)?; - Ok(Some((module.base_offset, at))) -} - -/// `S + A - tp` for one initial-exec reference, or `None` for a symbol no module defines. -fn compute_tpoff( - r: TlsRef, - own_base_offset: usize, - own_tls: Option, - symbols: SymTab<'_>, - tls: toyos_elf::tls::Static, - tls_info: &TlsModuleInfo, -) -> Result, RelocError> { - resolve_tls_ref(r, own_base_offset, own_tls, symbols, tls_info)? - .map(|(base_offset, at)| tls.tpoff(base_offset, at).ok_or(RelocError::TpoffOverflows)) - .transpose() -} diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 6d539e831cf..3f52b8e833d 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -1,46 +1,42 @@ //! Building a process out of an ELF file. //! -//! The kernel reads an executable's program headers, its `PT_TLS` template -//! and its build-id note, copies its `PT_LOAD`s into memory of the new -//! process's own and jumps to its entry; -//! it reads no dynamic section, applies no relocation and loads no library at -//! spawn. The program relocates itself (`toyos::relocate`). +//! The kernel reads an executable's program headers and its build-id note, +//! copies its `PT_LOAD`s into memory of the new process's own and jumps to its +//! entry. Everything else is the program's: it relocates itself +//! (`toyos::relocate`), lays out its own thread-local storage (`toyos::tls`) +//! and loads its own libraries. //! //! Every number the file names is untrusted: a refusal is //! `SyscallError::{InvalidArgument, ResourceExhausted}`, never a panic. //! //! A spawn that lands writes one record, `spawn: pid=N (…ms)`, once //! the process is in the table and placed; a spawn that is refused writes -//! one, naming why. Nothing is said on the way, by this file or by -//! `crate::elf` under it (`crate::process`'s header). +//! one, naming why. Nothing is said on the way (`crate::process`'s header). // `warn`, not `deny`: the rest of the kernel is not yet swept for undocumented unsafe blocks. #![warn(clippy::undocumented_unsafe_blocks)] mod start; -mod tls; pub use start::{build_child_handles, Commit, PendingHandles}; pub(crate) use start::{alloc_kernel_stack, KernelStack, Start}; -pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use crate::elf; use crate::object::{ops, HandleTable, KObjectRef}; use crate::mm::space::CopySource; use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::process::{ - Admission, ElfInfo, Endowments, OwnedAlloc, Parent, Pid, ProcessAccounting, ProcessData, + Admission, Endowments, Parent, Pid, ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserImage, PROCESS_TABLE, }; use crate::sync::Lock; use crate::{scheduler, UserAddr}; use toyos_abi::handle::Rights; use toyos_abi::syscall::SyscallError; -use toyos_elf::{Layout, TlsSegment}; +use toyos_elf::Layout; use toyos_symbols::frame::BuildId; /// The main thread's stack: committed whole, populated as it is touched. @@ -190,25 +186,25 @@ pub fn spawn( }; // Before anything is built; dropped on every way out below but the insert. let admission = Admission::ask(parent)?; - let mut charge = ProcessData::charge(&job, path, &env).map_err(SyscallError::from)?; + let charge = ProcessData::charge(&job, path, &env).map_err(SyscallError::from)?; let t0 = crate::clock::nanos_since_boot(); let header_size = 4096.min(backing.file_size() as usize); let header_data = read_file_range(backing.as_ref(), 0, header_size); - let layout = match elf::parse_layout(&header_data) { + let layout = match Layout::parse(&header_data, crate::arch::ELF_MACHINE) { Ok(l) => l, - Err(msg) => { - log!("spawn: {}: {}", path, msg); + Err(e) => { + log!("spawn: {}: {}", path, e.as_str()); return Err(SyscallError::InvalidArgument.into()); } }; - // The program reads the mapped table to relocate itself, and - // `SYS_QUERY_MODULES` answers with it: an image without one has neither. - let Some(phdrs) = layout.program_headers() else { + // The program reads the mapped table to relocate itself and lay out its + // thread-local storage. + if layout.program_headers().is_none() { log!("spawn: {}: no PT_LOAD maps the program header table", path); return Err(SyscallError::InvalidArgument.into()); - }; + } let source = backing.source(); @@ -253,44 +249,6 @@ pub fn spawn( (image_start, base, stack_base, sp) }; - let exe_tls_template = match layout.tls().and_then(TlsSegment::occupied) { - Some(tls) => { - let Some(tls_file_off) = layout.file_offset_of(tls.template()) else { - log!("spawn: {}: PT_TLS's template is in no PT_LOAD's file bytes", path); - return Err(SyscallError::InvalidArgument.into()); - }; - // Read directly into the `memsz`-sized buffer: `OwnedAlloc` zeroes - // (no second pass for `.tbss`) and refuses a size past one heap - // allocation itself. - let template = core::alloc::Layout::from_size_align(tls.memsz() as usize, 16).map_err(|_| SyscallError::InvalidArgument)?; - charge.grow(crate::mm::charge::heap(template)).map_err(SyscallError::from)?; - 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. - elf::read_backing_into( - backing.as_ref(), - tls_file_off, - tls_buf.slice(tls.template().len() as usize), - ); - Some(tls_buf) - } - None => None, - }; - - let Some((tls_modules, tls)) = tls::build_tls_layout(&layout, exe_tls_template.as_ref()) else { - log!("spawn: {}: the TLS module does not fit one block", path); - return Err(SyscallError::ResourceExhausted.into()); - }; - - let Some((tls_pages, thread_pointer, _)) = - 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()); - }; - // Inside the image, which `rebase_base` placed inside the user half. let Some(entry) = toyos_userbound::Entry::new((image_start + layout.entry().get()).raw()) else { @@ -298,7 +256,6 @@ pub fn spawn( return Err(SyscallError::InvalidArgument.into()); }; let image_end = (image_start + layout.span()).raw(); - let t_tls = crate::clock::nanos_since_boot(); // What a crash record names a frame by. The build-id is read at each // `PT_NOTE`'s own offset, a page of it at most. @@ -329,33 +286,16 @@ pub fn spawn( let proc_data = Arc::new(Lock::new(ProcessData { handles, env, - elf: ElfInfo { - elf_alloc: exe_tls_template, - tls_modules, - tls, - next_tls_module_id: tls::FIRST_DLOPEN_MODULE, - dynamic_tls_blocks: alloc::collections::BTreeMap::new(), - loaded_libs: Vec::new(), - elf_base: UserAddr::new(base), - exe_eh_frame_hdr: layout - .eh_frame_hdr() - .map_or((0, 0), |r| ((image_start + r.start().get()).raw(), r.len())), - exe_vaddr_max: image_end, - exe_phdrs: ((image_start + phdrs.image().start().get()).raw(), phdrs.count()), - lib_paths: Vec::new(), - }, space: Arc::clone(&child_pt), - exe_path: String::from(path), spawn_ns: crate::clock::nanos_since_boot(), accounting: ProcessAccounting::default(), endowments, job: job.clone(), - charge, + _charge: charge, pager, })); let thread_data = Arc::new(Lock::new(ThreadData { - tls: Some(tls_pages), user_stack_base: stack_base, user_stack_size: USER_STACK_SIZE as u64, })); @@ -383,7 +323,7 @@ pub fn spawn( kernel_stack: ks_alloc, entry_sp: ks_sp, address_space: child_pt.clone(), - thread_pointer, + thread_pointer: 0, job, image, // The spawning thread's, as a new thread's is. @@ -403,8 +343,7 @@ pub fn spawn( crate::process::debug_hold_marked_spawn(parent, &object); let t3 = crate::clock::nanos_since_boot(); - log!("spawn: {} pid={} (layout={}ms tls={}ms total={}ms)", - path, pid, (t1 - t0) / 1_000_000, (t_tls - t1) / 1_000_000, (t3 - t0) / 1_000_000); + log!("spawn: {} pid={} (layout={}ms total={}ms)", path, pid, (t1 - t0) / 1_000_000, (t3 - t0) / 1_000_000); Ok((pid, held)) } diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs deleted file mode 100644 index 8a8620c6b98..00000000000 --- a/kernel/src/loader/tls.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! 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 block's bytes, the template copies, the TCB and the -//! DTV. -//! -//! A block is laid out on the kernel's heap with every pointer in it an offset from its start, and -//! moved to the address it is given inside the one hold of the space 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 alloc::vec::Vec; - -use crate::elf::TlsModule; -use crate::mm::space::At; -use crate::object::shm::SharedMemObject; -use crate::process::{OwnedAlloc, PageTables, KernelMapping}; -use toyos_elf::tls::{Static, Variant}; -use toyos_elf::{Layout, TlsSegment}; - -/// This machine's TLS layout. -pub const VARIANT: Variant = Variant::of(crate::arch::ELF_MACHINE); - -/// Variant II's TCB; variant I's is the gap `toyos_elf::tls` leaves below the data. -const TCB_SIZE: usize = 64; -/// Module entries a thread's DTV can hold; `SYS_TLS_ALLOC_BLOCK` refuses a module id above it: there is nowhere to record the answer. -pub const DTV_INITIAL_CAPACITY: usize = 64; -/// Generation word, then length word. -const DTV_HEADER_SIZE: usize = 16; -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 thread's TLS block, built and not yet mapped. -pub struct TlsBlock { - bytes: Vec, - /// Where the thread pointer goes, from the block's first byte. - tp_offset: usize, - /// What its start is aligned to. - align: 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.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 `space`, counted to its job, answering its mapping, the thread pointer, - /// and where that pointer lies in the block. `None` when `space` has no - /// room or no memory is left for it. - pub fn publish(mut self, pt: &PageTables) -> Option<(KernelMapping, u64, usize)> { - let job = alloc::sync::Arc::clone(pt.lock().job()); - let object = SharedMemObject::create(&job, self.bytes.len() as u64).ok()?; - let mut space = pt.lock(); - let at = space.reserve(object.size(), At::Anywhere(self.align), kernel::vm::Prot::None).ok()?; - rebase(&mut self.bytes, self.tp_offset, at.raw()); - object.write(0, &self.bytes); - let len = object.size(); - space.map(object.clone(), 0, len, kernel::vm::Prot::ReadWrite, At::Fixed(at)).ok()?; - let thread_pointer = at.raw() + self.tp_offset as u64; - Some((KernelMapping { object, at }, thread_pointer, self.tp_offset)) - } -} - -/// Every static module's template copied in, and the TCB and DTV pointing at -/// offsets from the block's start, which [`rebase`] moves once it has an address. -fn build_combined(modules: &[TlsModule], tls: Static) -> Option { - let plan = tls.plan(TCB_SIZE, DTV_BYTES, crate::mm::PAGE_SIZE as usize)?; - if plan.alloc_size > crate::mm::MAX_HEAP_ALLOC { - return None; - } - let mut block = alloc::vec![0u8; plan.alloc_size]; - for module in modules.iter().filter(|m| m.is_static) { - if let Some(template) = &module.template { - let at = plan.tls_start + module.base_offset; - // SAFETY: a bounds-checked view of the template, which its module keeps alive. - let bytes = unsafe { template.as_slice() }; - block[at..at + bytes.len()].copy_from_slice(bytes); - } - } - let mut put = |at: usize, value: u64| block[at..at + 8].copy_from_slice(&value.to_ne_bytes()); - - // The thread's id (`toyos_abi::TCB_TID`) is TP+16 on variant II and TP+8 on - // variant I, zero here — a process's first thread is tid 0 — and written by - // `process::spawn_thread` for every other. - match VARIANT { - // TP+0 the psABI self-pointer, TP+8 the DTV pointer. - Variant::II => { - put(plan.tp_offset, plan.tp_offset as u64); - put(plan.tp_offset + 8, 0); - } - // TP+0 the DTV pointer, TP+8 the implementation's word, the tid (zeroed above). - Variant::I => put(plan.tp_offset, 0), - } - - put(0, 1); - put(8, DTV_INITIAL_CAPACITY as u64); - for i in 0..DTV_INITIAL_CAPACITY { - put(16 + i * 8, DTV_UNALLOCATED); - } - // A `dlopen`ed module's slot stays unallocated until `__tls_get_addr` asks for it. - for module in modules.iter().filter(|m| m.is_static) { - let idx = module.module_id as usize; - if idx > 0 && idx <= DTV_INITIAL_CAPACITY { - put(16 + (idx - 1) * 8, (plan.tls_start + module.base_offset) as u64); - } - } - - let align = (tls.align() as u64).max(crate::mm::PAGE_SIZE); - Some(TlsBlock { bytes: block, tp_offset: plan.tp_offset, align }) -} - -/// Move the block's self-referential pointers from offsets to addresses at `at`. -/// -/// 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. -fn rebase(block: &mut [u8], tp_offset: usize, at: u64) { - let word = |block: &[u8], offset: usize| u64::from_ne_bytes(block[offset..offset + 8].try_into().expect("eight bytes")); - let mut offsets: Vec = match VARIANT { - Variant::II => alloc::vec![tp_offset, tp_offset + 8], - Variant::I => alloc::vec![tp_offset], - }; - offsets.extend((0..DTV_INITIAL_CAPACITY).map(|i| 16 + i * 8).filter(|&offset| word(block, offset) != DTV_UNALLOCATED)); - for offset in offsets { - let moved = word(block, offset) + at; - block[offset..offset + 8].copy_from_slice(&moved.to_ne_bytes()); - } -} - -/// The id the first `dlopen`ed module with TLS takes: the executable's is 1. -pub const FIRST_DLOPEN_MODULE: u64 = 2; - -/// The static block's one module, the executable's, and its layout; no module for an executable -/// without TLS, and `None` when it does not fit. The module goes where its linker resolved its own -/// accesses: ending at the thread pointer in variant II, starting at it in variant I. -pub fn build_tls_layout( - layout: &Layout, - exe_tls_template: Option<&OwnedAlloc>, -) -> Option<(alloc::vec::Vec, Static)> { - let Some(tls) = layout.tls().and_then(TlsSegment::occupied) else { - return Some((alloc::vec::Vec::new(), Static::new(VARIANT, 0, 1)?)); - }; - let (memsz, align) = (tls.memsz() as usize, tls.align() as usize); - // Variant II's executable ends at the thread pointer at its extent, not its `memsz`: - // its linker fixed every local-exec offset against the rounded size. - let placed = match VARIANT { - Variant::II => toyos_elf::tls::exe_extent(memsz, align)?, - Variant::I => memsz, - }; - let template = exe_tls_template.map(|buf| buf.slice(tls.template().len() as usize)); - let module = TlsModule { template, memsz, base_offset: 0, module_id: 1, is_static: true }; - Some((alloc::vec![module], Static::new(VARIANT, placed, align)?)) -} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 860bd33c8c4..f594df4085e 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -44,10 +44,8 @@ mod power; mod inventory; mod loaded; -mod rollback; mod bootimage; mod file_backing; -mod elf; mod symbols; mod process; mod loader; diff --git a/kernel/src/mm/mod.rs b/kernel/src/mm/mod.rs index 35350a7cad9..5e688b0704a 100644 --- a/kernel/src/mm/mod.rs +++ b/kernel/src/mm/mod.rs @@ -10,7 +10,6 @@ pub mod charge; mod image; mod mmio; pub mod policy; -mod region; pub mod space; /// Behind the same feature as its only caller, `sched::driver`'s ladder. @@ -24,7 +23,6 @@ pub use alloc::sweep as sweep_heap_bands; pub use alloc::hold_lock as hold_heap_lock; pub use image::Image; pub use mmio::Mmio; -pub use region::{Allocation, KernelSlice}; use crate::MemoryMapEntry; pub use pmm::Region; diff --git a/kernel/src/mm/region.rs b/kernel/src/mm/region.rs deleted file mode 100644 index 6e169147bdf..00000000000 --- a/kernel/src/mm/region.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! `KernelSlice` is `Copy`, carries no lifetime, and can outlive its `Allocation`; tracked at `issues/kernelslice-outlives-its-allocation.md`. - -/// A kernel allocation that can vouch for its own extent. -/// # Safety -/// `size()` must be no more than what `ptr()` really owns, for as long as `self` is alive. -pub unsafe trait Allocation { - /// First byte of the allocation, addressable by the kernel. - fn ptr(&self) -> *mut u8; - /// How many bytes were allocated. - fn size(&self) -> usize; -} - -/// Bounds-checked view into a contiguous kernel memory region. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct KernelSlice { - base: *mut u8, - size: usize, -} - -// SAFETY: every access through `base` is its own `unsafe fn`, so sharing `(base, size)` across threads claims nothing about aliasing. -unsafe impl Send for KernelSlice {} -// SAFETY: see the `Send` impl above — same reasoning. -unsafe impl Sync for KernelSlice {} - -impl KernelSlice { - /// The whole of an allocation, sized by the allocation — the only constructor. - pub fn whole(alloc: &impl Allocation) -> Self { - Self { base: alloc.ptr(), size: alloc.size() } - } - - pub fn size(&self) -> usize { self.size } - pub fn base(&self) -> *mut u8 { self.base } - - /// Physical address of the base via the direct map. - pub fn phys(&self) -> u64 { - super::DirectMap::phys_of(self.base) - } - - pub fn subslice(&self, offset: usize, size: usize) -> KernelSlice { - assert!(offset + size <= self.size, - "KernelSlice OOB: offset={:#x} size={:#x} total={:#x}", offset, size, self.size); - KernelSlice { - // SAFETY: `offset + size <= self.size` was just asserted, so the result stays inside the allocation `self` covers. - base: unsafe { self.base.add(offset) }, - size, - } - } - - fn check(&self, offset: usize, len: usize) { - assert!(offset + len <= self.size, - "KernelSlice OOB: offset={:#x} len={} size={:#x}", offset, len, self.size); - } - - /// # 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); - } - - /// # Safety: the returned slice must not alias a live `&mut` for as long as it is held. - pub unsafe fn as_slice(&self) -> &[u8] { - core::slice::from_raw_parts(self.base, self.size) - } - - /// # Safety: same as `write`, for `src.len()` bytes at `offset`. - pub unsafe fn copy_from(&self, offset: usize, src: &[u8]) { - self.check(offset, src.len()); - core::ptr::copy_nonoverlapping(src.as_ptr(), self.base.add(offset), src.len()); - } - - /// # Safety: same as `write`, for the whole range. - pub unsafe fn zero(&self) { - core::ptr::write_bytes(self.base, 0, self.size); - } -} diff --git a/kernel/src/mm/space.rs b/kernel/src/mm/space.rs index d7792a78452..5028e659fbd 100644 --- a/kernel/src/mm/space.rs +++ b/kernel/src/mm/space.rs @@ -237,11 +237,6 @@ impl AddressSpace { self.tables.root() } - /// The job its memory is counted to. - pub fn job(&self) -> &Arc { - &self.job - } - /// Where a range of `len` bytes goes, changing nothing: a `Fixed` one is /// put in place of what is there by [`put`](Self::put). `len` is whole /// pages. diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index c501dbcd187..6744f3b4f52 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -1,5 +1,5 @@ //! A memory object: bytes more than one address space can map — a region -//! `SYS_SHM_CREATE` makes, a device's grant, a program's TLS block, an inbox's +//! `SYS_SHM_CREATE` makes, a device's grant, an inbox's //! ring, the clock page, a boot image the loader handed over, part of another //! object's pages — or an aperture over memory the kernel does not own: a //! device's registers, firmware's framebuffer. diff --git a/kernel/src/process.rs b/kernel/src/process.rs index aa9db74d283..e3c89ef2caa 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -37,15 +37,15 @@ use crate::sched::driver; use crate::sched::payload::ThreadSched; use kernel::sched::cpu::{homes, Near}; use crate::time::{Deadline, Duration}; -use crate::{elf, pipe, scheduler}; +use crate::{pipe, scheduler}; use crate::object::shm::SharedMemObject; use crate::UserAddr; -use crate::loader::{alloc_kernel_stack, Start, TlsBlock}; +use crate::loader::{alloc_kernel_stack, Start}; pub use toyos_abi::{Pid, Tid}; pub use crate::scheduler::TaskId; use toyos_abi::end::{End, Fault}; -use toyos_abi::syscall::{EndowEntry, SyscallError, MAX_LIBRARIES}; +use toyos_abi::syscall::{EndowEntry, SyscallError}; /// The lifecycle's decisions; this file only performs them. pub use toyos_proclife::{ThreadLocation, Watch}; @@ -67,7 +67,7 @@ pub struct OwnedAlloc { } impl OwnedAlloc { - /// `None` above `mm::MAX_HEAP_ALLOC` — not `PAGE_2M`, which dlmalloc's own overhead can still overrun for a `PT_TLS` block near that size. + /// `None` above `mm::MAX_HEAP_ALLOC`. pub fn new(size: usize, align: usize) -> Option { if size > crate::mm::MAX_HEAP_ALLOC { return None; } let layout = Layout::from_size_align(size, align).ok()?; @@ -77,18 +77,6 @@ impl OwnedAlloc { } pub fn ptr(&self) -> *mut u8 { self.ptr.as_ptr() } - pub fn size(&self) -> usize { self.layout.size() } - - /// A bounds-checked view of the first `len` bytes; `len` is checked against the allocation, not trusted. - pub fn slice(&self, len: usize) -> crate::mm::KernelSlice { - crate::mm::KernelSlice::whole(self).subslice(0, len) - } -} - -// SAFETY: `ptr()`/`size()` read the same `Layout` `alloc_zeroed` used; the type is move-only and frees once in `Drop`. -unsafe impl crate::mm::Allocation for OwnedAlloc { - fn ptr(&self) -> *mut u8 { OwnedAlloc::ptr(self) } - fn size(&self) -> usize { OwnedAlloc::size(self) } } impl Drop for OwnedAlloc { @@ -102,9 +90,8 @@ impl Drop for OwnedAlloc { unsafe impl Send for OwnedAlloc {} -/// An object the kernel mapped into a process for it and takes back itself: a -/// thread's TLS block, a `dlopen`ed module's block for one thread, an inbox's -/// rings. The kernel writes what it owes the process into the object. +/// An object the kernel mapped into a process for it and takes back itself: an +/// inbox's rings. The kernel writes what it owes the process into the object. pub struct KernelMapping { pub object: Arc, pub at: UserAddr, @@ -265,33 +252,6 @@ impl Processes for ProcessTable { } -/// ELF loading artifacts and TLS state. -pub struct ElfInfo { - pub elf_alloc: Option, - pub tls_modules: Vec, - /// Every static module's TLS, as a thread's block is laid out from it. - pub tls: toyos_elf::tls::Static, - /// Next module ID to assign on dlopen (1-based, exe=1). - pub next_tls_module_id: u64, - /// Dynamically allocated TLS blocks for dlopen'd modules, keyed by - /// (Tid, module_id), each with the charge of its place here. - pub dynamic_tls_blocks: alloc::collections::BTreeMap<(Tid, u64), (KernelMapping, Charge)>, - /// Dynamically loaded shared libraries (indexed by dlopen handle). - pub loaded_libs: Vec, - pub elf_base: UserAddr, - /// 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, - /// The executable's program header table as (address, count), `(0, 0)` with no executable. - pub exe_phdrs: (u64, u16), - /// Paths of dlopen'd libraries (parallel to loaded_libs). - pub lib_paths: Vec, -} - -// The ledger stops at `MAX_LIBRARIES`, and a doubling from below it stays inside one heap allocation. -const _: () = assert!(2 * MAX_LIBRARIES * core::mem::size_of::() <= crate::mm::MAX_HEAP_ALLOC); - /// Process-level data shared across all threads via `Arc>`. pub struct ProcessData { /// Every kernel object this process can name; stdio is handles `0`, `1`, `2`. @@ -299,12 +259,8 @@ pub struct ProcessData { /// Inherited environment variables (KEY=VALUE\0KEY2=VALUE2\0...) pub env: Vec, - pub elf: ElfInfo, - /// The space every thread of it runs in. pub space: PageTables, - /// Executable path (for SYS_QUERY_MODULES). - pub exe_path: String, pub spawn_ns: u64, pub accounting: ProcessAccounting, @@ -313,8 +269,8 @@ pub struct ProcessData { /// The job its threads, and its children unless a spawn names another, /// run in. pub job: Arc, - /// What [`ProcessData::charge`] charged, and its executable's TLS beside. - pub charge: Charge, + /// What [`ProcessData::charge`] charged. + pub _charge: Charge, /// Whether it was started as a pager, which no call changes, and whether /// it made its one pager object. pub pager: PagerRole, @@ -353,7 +309,7 @@ impl ProcessData { + entry + threads + 2 * core::mem::size_of::(); - let strings = 2 * charge::array::(path.len()) + let strings = charge::array::(path.len()) + charge::array::(toyos_abi::syscall::MAX_PATH) + charge::array::(env.len()); let endowments = charge::array::(toyos_abi::syscall::MAX_ENDOWMENTS) @@ -406,7 +362,6 @@ pub struct ProcessAccounting { /// Per-thread data, unique to each thread via `Arc>`. pub struct ThreadData { - pub tls: Option, pub user_stack_base: UserAddr, pub user_stack_size: u64, } @@ -647,44 +602,19 @@ pub fn spawn_thread( .expect("spawn_thread: the spawning thread runs in an address space"); (addr_space, Arc::clone(&proc.process_data)) }; - let (tls_modules, tls, job) = { - let data = process_data_arc.lock(); - (data.elf.tls_modules.clone(), data.elf.tls, data.job.clone()) - }; + let job = process_data_arc.lock().job.clone(); - // 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)?; - // Placement or memory refused is a resource failure the process caused, - // not a kernel bug. - let (tls_alloc, thread_pointer, tp_offset) = block.publish(&parent_addr_space)?; - - let (ks_alloc, ks_sp) = match alloc_kernel_stack(&job, Start::Thread { entry, sp: stack_ptr, arg }) { - Some(ks) => ks, - None => { - tls_alloc.release(&parent_addr_space); - return None; - } - }; + // Phase 2: the kernel stack, outside any lock. + let (ks_alloc, ks_sp) = alloc_kernel_stack(&job, Start::Thread { entry, sp: stack_ptr, arg })?; // Phase 3: insert into table (brief table lock); threads share the parent's ProcessData Arc. let mut guard = PROCESS_TABLE.lock(); let table = guard.as_mut().unwrap(); // Re-checked under the insert lock: a thread refused here would otherwise be invisible to a retire sweep already under way. if !proclife_spawn::admit_thread_insert(table, parent_process).is_yes() { - // The unmap cannot run under the table lock (its shootdown is a wait a - // sibling can be spinning against), so the mapping is - // still solely this scope's here — built into a ThreadData only past - // the admission, where the table owns its release. - drop(guard); - tls_alloc.release(&parent_addr_space); return None; } - let tcb = Arc::clone(&tls_alloc.object); let thread_data = Arc::new(Lock::new(ThreadData { - tls: Some(tls_alloc), user_stack_base: UserAddr::new(stack_base), user_stack_size: if stack_base > 0 { stack_ptr - stack_base } else { 0 }, })); @@ -697,9 +627,6 @@ pub fn spawn_thread( let siblings = proc.threads.iter().rev().filter_map(|(_, thread)| thread.sched().map(|sched| sched.shared.state())); let homes = homes(siblings, mask.within(crate::smp::cpu_count() as usize)); 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`). - tcb.write((tp_offset + toyos_abi::TCB_TID) as u64, &tid.raw().to_ne_bytes()); // Enqueue while still holding the table lock: this thread is fully visible to the scheduler before any retire sweep can start. let (sched, _dst) = scheduler::enqueue_new(scheduler::NewTask { @@ -707,7 +634,8 @@ pub fn spawn_thread( kernel_stack: ks_alloc, entry_sp: ks_sp, address_space: parent_addr_space, - thread_pointer, + // The thread names its own (`SYS_SET_THREAD_POINTER`). + thread_pointer: 0, job, image, mask, @@ -749,21 +677,18 @@ impl core::fmt::Display for Consumed { } } -/// Frees an exiting process's resources (mappings, handles, ELF state), and answers what it consumed. +/// Frees an exiting process's resources (mappings, handles), and answers what it consumed. /// Says nothing of the machine: a reading of every CPU is `crate::census`'s, never one process's end. fn teardown_resources( process_data_arc: &Arc>, main_thread: &(Arc>, Option), pid: Pid, ) -> Consumed { - let (thread_data_arc, sched) = main_thread; + let (_, sched) = main_thread; let syscalls = sched.as_ref().map(|sched| sched.handle.syscalls()); let syscall_total = syscalls.map_or(0, |s| s.calls()); let syscall_total_ns = syscalls.map_or(0, |s| s.ns()); let syscall_counts = syscalls.map_or([0; toyos_abi::syscall::SYSCALL_PROFILE_BINS], |s| s.by_number()); - // Never hold ThreadData + ProcessData at once. - thread_data_arc.lock().tls.take(); - // Before the teardown below: a report spans its process up to here, and the next one this teardown. #[cfg(feature = "mask-windows")] crate::windows::report(); @@ -772,8 +697,6 @@ fn teardown_resources( ops::close_all(&mut data.handles); // Every thread has left, and none returns to Ring 3 to use them. crate::isa::process_ends(pid); - data.elf.elf_alloc.take(); - data.elf.loaded_libs.clear(); let usage = data.space.usage(); Consumed { @@ -978,7 +901,7 @@ pub enum Joined { Detached, } -/// Exit the current thread. If this is the main thread, it is the process's exit. For a child thread, frees its own mappings and leaves. +/// Exit the current thread. If this is the main thread, it is the process's exit; a child thread leaves. pub fn thread_exit(code: i32, joined: Joined) -> ! { let process_pid = current_process(); let tid = current_tid(); @@ -992,7 +915,6 @@ pub fn thread_exit(code: i32, joined: Joined) -> ! { proclife::ThreadExit::Sibling { post } => post, }; - release_thread(tid); match joined { Joined::Joinable => leave_as(Leaving::Joinable(code)), Joined::Detached => leave_as(Leaving::Detached(code)), @@ -1011,24 +933,6 @@ pub fn thread_exit(code: i32, joined: Joined) -> ! { scheduler::exit_current(); } -/// A child thread's own mappings, released before it leaves; returns rather than diverging for [`leave`]'s reason. -fn release_thread(tid: Tid) { - let addr_space = current_address_space(); - crate::mm::paging::activate_kernel(); - - let tls = current_data().lock().tls.take(); - let blocks: Vec = { - let owner_arc = process_data(); - let mut owner = owner_arc.lock(); - let keys: Vec<(Tid, u64)> = owner.elf.dynamic_tls_blocks.keys().filter(|&&(t, _)| t == tid).copied().collect(); - keys.iter().filter_map(|key| owner.elf.dynamic_tls_blocks.remove(key)).map(|(block, _charge)| block).collect() - }; - // With no process lock held: a release takes the space's. - for block in tls.iter().chain(&blocks) { - block.release(&addr_space); - } -} - /// A thread's scheduler record, cloned out of the table so wake/retire never hold the table lock while they post. pub fn thread_sched(pid: Pid, tid: Tid) -> Option { let guard = PROCESS_TABLE.lock(); @@ -1145,24 +1049,7 @@ pub fn dump_crash_diagnostics(fault_addr: u64, rip: u64) { let tp = crate::arch::cpu::thread_pointer(); if tp != 0 { log!(" Thread pointer: {:#x}", tp); - if let Some(first) = read_user(tp) { - if matches!(crate::loader::TLS_VARIANT, toyos_elf::tls::Variant::II) { - log!(" [TP] = {:#x} (expected {:#x}, variant II's self-pointer)", first, tp); - } - for i in 0..8u64 { - let addr = tp + i * 8; - let Some(val) = read_user(addr) else { break }; - log!(" TP+{:#x} = {:#018x}", i * 8, val); - } - log!(" TLS data before TP:"); - for i in 1..=4u64 { - let addr = tp - i * 8; - let Some(val) = read_user(addr) else { break }; - log!(" TP-{:#x} = {:#018x}", i * 8, val); - } - } else { - log!(" Thread pointer {:#x} NOT MAPPED!", tp); - } + dump_region("the thread pointer", tp); } } diff --git a/kernel/src/rollback.rs b/kernel/src/rollback.rs deleted file mode 100644 index 06ff1b9c0ff..00000000000 --- a/kernel/src/rollback.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! An RAII commit-guard: `undo` runs on drop unless [`Rollback::commit`] ran, -//! so an early return between acquiring a resource and committing it undoes the -//! acquire — "install, then fail, then leak" is not writable behind one. - -#[must_use = "a Rollback runs its undo when dropped; bind it and commit() on the success path"] -pub struct Rollback { - undo: Option, -} - -impl Rollback { - pub fn new(undo: F) -> Self { - Self { undo: Some(undo) } - } - - /// The success path: keep the resource, cancel the rollback. - pub fn commit(mut self) { - self.undo = None; - } -} - -impl Drop for Rollback { - fn drop(&mut self) { - if let Some(undo) = self.undo.take() { - undo(); - } - } -} diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 4b3f418ca95..a5e5f10f0ab 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -48,10 +48,10 @@ use super::machine::lower_sysinfo_bound; use super::paged::{sys_paged_create, sys_paged_holder, sys_paged_notices, sys_paged_op, sys_paged_read, sys_pager_create}; use super::proc::{ spawn_pager, spawn_place, sys_endowments, sys_exit, sys_nanosleep, sys_process_stats, - sys_account_info, sys_job_create, sys_job_floor, sys_job_own, sys_process_quit, sys_process_wait, sys_rt_enter, sys_spawn, sys_thread_exit, sys_thread_get_affinity, sys_thread_join, sys_thread_set_affinity, sys_thread_spawn, sys_wake_latency_hold, sys_yield, + sys_account_info, sys_job_create, sys_job_floor, sys_job_own, sys_process_quit, sys_process_wait, sys_rt_enter, sys_spawn, sys_thread_exit, sys_thread_get_affinity, sys_thread_join, sys_thread_set_affinity, sys_set_thread_pointer, sys_thread_spawn, sys_wake_latency_hold, sys_yield, }; use super::vm::{ - shared_image, sys_dlopen, sys_dlsym, sys_query_modules, sys_tls_alloc_block, sys_vm_decommit, sys_vm_discard, sys_vm_remap, + shared_image, sys_vm_decommit, sys_vm_discard, sys_vm_remap, sys_vm_protect, sys_vm_release, sys_vm_reserve, }; @@ -193,6 +193,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> // No right: the one caller marks both ends of a pair, so requiring one would refuse the other. SYS_MARK_TTY => with_object(RawHandle(a1 as u32), Rights::NONE, ops::mark_tty), SYS_THREAD_SPAWN => sys_thread_spawn(a1, a2, a3, a4), + SYS_SET_THREAD_POINTER => sys_set_thread_pointer(a1), SYS_THREAD_JOIN => sys_thread_join(a1), SYS_YIELD => sys_yield(), // Answers from the boot-time anchor, never the CMOS: NotSupported beats a @@ -250,32 +251,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_HANDLE_DUP => sys_handle_dup(RawHandle(a1 as u32), a2), SYS_NULL => sys_null(a1), SYS_GETPID => process::current_process().raw() as u64, - SYS_DLOPEN => { - 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 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, 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() }; - sys_dlsym(a1, &name) - } - // A no-op by contract: a dlopen'd module is shared and process-lived, and - // a reload returns the same handle (`sys_dlopen`), so nothing to reclaim. - SYS_DLCLOSE => 0, SYS_STACK_INFO => { let stack = process::with_current_data(|data| { (data.user_stack_base.raw() > 0) @@ -432,7 +407,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> let name = match ctx.user_str(UserAddr::new(a2), a3) { Ok(s) => s, Err(e) => return e.to_u64() }; sys_namespace_open(RawHandle(a1 as u32), &name) } - SYS_TLS_ALLOC_BLOCK => sys_tls_alloc_block(a1), SYS_INBOX_SETUP => { // Taken before the inbox exists: a bad address must not leave a handle the caller was never told. let len = core::mem::size_of::() as u64; @@ -442,10 +416,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_INBOX_SUBMIT => { sys_inbox_submit(RawHandle(a1 as u32), a2 as u32, a3 as u32, a4) } - SYS_QUERY_MODULES => { - let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a1), a2) else { return bad_addr }; - sys_query_modules(&mut buf) - } // Feature-gated, not capability-checked: a shipping kernel has no SYS_DEBUG // arm, so the number falls to the same default InvalidArgument as any unassigned one. #[cfg(feature = "test-actuators")] diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 9fe384dcd5b..544852cb953 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -220,6 +220,21 @@ pub(super) fn sys_endowments(out: &mut crate::user_ptr::UserBytesMut) -> u64 { needed as u64 } +/// Point the calling thread's thread pointer at `tp` (x86-64's FS base, +/// AArch64's `TPIDR_EL0`), which the thread keeps across every switch, and +/// answer its tid. The kernel never reads through it: what lies there is the +/// program's. Zero or a user-half address; anything else is `InvalidArgument`, +/// since a non-canonical FS base would fault in the kernel's ring. +pub(super) fn sys_set_thread_pointer(tp: u64) -> u64 { + if tp != 0 && UserAddr::checked(tp).is_none() { + return SyscallError::InvalidArgument.to_u64(); + } + // SAFETY: zero or a user-half address, so canonical; the register is this + // thread's, saved with it at every switch. + unsafe { crate::arch::cpu::write_thread_pointer(tp) }; + u64::from(process::current_tid().raw()) +} + /// Spawn a thread; refuses a `stack_base` above `stack_ptr` (no stack to clamp to) /// and an `entry` outside the user half, which the return to it would fault on /// in the kernel's ring. The stack pointer is the thread's own to fault on. diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index 3c166aed0b4..a6408be9696 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -1,7 +1,5 @@ //! The caller's own address space: `vm_*` reserve, commit, protect, discard, -//! resize and take away private memory, `dlopen` maps a library image, `SYS_TLS_ALLOC_BLOCK` -//! maps a thread's per-module TLS block, `SYS_QUERY_MODULES` reports what is -//! loaded. Exhausting address space is an error return, never an `.expect`. +//! resize and take away private memory. Exhausting address space is an error return, never an `.expect`. //! //! **A length from userland is refused before any arithmetic touches it**: //! rounding it to pages is the checked sum, and one no page count holds is @@ -9,9 +7,7 @@ use kernel::vm::Prot; -use crate::mm::charge::{self, Charge}; use crate::mm::space::At; -use crate::user_ptr::UserBytesMut; use crate::UserAddr; use crate::process; @@ -22,15 +18,16 @@ fn whole_pages(size: u64) -> Option { size.checked_next_multiple_of(crate::mm::PAGE_SIZE).filter(|&bytes| bytes != 0) } -/// The protection a `VmProt` word names. Executable memory is not yet one a -/// program may ask for: `ReadExec` is `NotSupported`. +/// The protection a `VmProt` word names. No word names write and execute at +/// once: a program's loader writes its code `ReadWrite` and then protects it +/// `ReadExec`, so no page of private memory is ever both. fn vm_prot(word: u64) -> Result { - match VmProt::from_u64(word).ok_or(SyscallError::InvalidArgument)? { - VmProt::None => Ok(Prot::None), - VmProt::Read => Ok(Prot::Read), - VmProt::ReadWrite => Ok(Prot::ReadWrite), - VmProt::ReadExec => Err(SyscallError::NotSupported), - } + Ok(match VmProt::from_u64(word).ok_or(SyscallError::InvalidArgument)? { + VmProt::None => Prot::None, + VmProt::Read => Prot::Read, + VmProt::ReadWrite => Prot::ReadWrite, + VmProt::ReadExec => Prot::ReadExec, + }) } /// `[addr, addr + len)` as a page-aligned start and whole pages. @@ -123,303 +120,3 @@ pub(super) fn shared_image(handle: u64, len: u64) -> Result, -) -> 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. - if let Some(idx) = - process::with_process_data(|d| d.elf.lib_paths.iter().position(|p| *p == resolved)) - { - if let Some(out) = init_out { - if ctx.copy_out(out, &[0u64, 0]).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - return idx as 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(); - } - }; - - let pt = process::current_address_space(); - // The module's own program headers decide which pages are writable and - // which executable; mapping the whole image writable would make it - // executable too. - let Some(base) = lib.map_into(&pt) else { - log!("dlopen: {}: out of virtual address space", resolved); - return SyscallError::ResourceExhausted.to_u64(); - }; - if base != lib.user_base { - lib.user_base = base; - crate::elf::rebase_relative_relocs(&lib); - } - - // The mapping is committed before the fallible copy-out below; this guard - // takes it back if the copy-out refuses, so a refused dlopen leaves no - // library mapped or registered. - let span = lib.image.size() as u64; - let mapping = crate::rollback::Rollback::new(move || pt.lock().release(base, span)); - - let lib_tls = lib.tls().and_then(toyos_elf::TlsSegment::occupied); - let data_arc = process::process_data(); - // References no module defines: a count in this load's one record, as a spawn's are. - let mut unresolved; - let init_info = { - let data = data_arc.lock(); - unresolved = crate::elf::resolve_dlopen_relocs(&lib, &data.elf.loaded_libs); - - // 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 initial_exec = if data.elf.tls.total_memsz() > 0 { - crate::elf::apply_tpoff_relocs(&lib, 0, data.elf.tls, &tls_info) - } else { - Ok(0) - }; - let general_dynamic = initial_exec.and_then(|left| match lib_tls { - Some(_) => crate::elf::apply_dtpoff_relocs(&lib, &tls_info).map(|more| left + more), - None => Ok(left), - }); - match general_dynamic { - Ok(left) => unresolved += left, - Err(refused) => { - log!("dlopen: {}: {}", resolved, refused.as_str()); - return SyscallError::InvalidArgument.to_u64(); - } - } - - // 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 - // refused copy-out registers nothing and the mapping guard rolls back. - if let Some(out) = init_out { - if ctx.copy_out(out, &init_info).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - - let mut data = data_arc.lock(); - // Asked again under the guard that registers: the lookup at the top released - // this lock across the whole load, so a sibling can have registered the name - // meanwhile. The loser commits nothing and its mapping goes down with the guard. - if let Some(idx) = data.elf.lib_paths.iter().position(|p| *p == resolved) { - drop(data); - // Empty init, the same answer the lookup at the top of the call gives. - if let Some(out) = init_out { - if ctx.copy_out(out, &[0u64, 0]).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - return idx as u64; - } - // Asked here for the same reason, so a load past the bound, or one whose - // records the process cannot be charged, is refused having registered - // nothing; its mapping goes down with the guard. - if data.elf.loaded_libs.len() >= MAX_LIBRARIES || data.charge.grow(library_heap(&resolved)).is_err() { - drop(data); - return SyscallError::ResourceExhausted.to_u64(); - } - mapping.commit(); - - let idx = data.elf.loaded_libs.len(); - // 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 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 { - libs: &data.elf.loaded_libs, - modules: &data.elf.tls_modules, - }; - unresolved += 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() as usize, - base_offset: 0, - module_id, - is_static: false, - }); - } - log!("dlopen: {} pid={} unresolved={}", resolved, process::current_process(), unresolved); - data.elf.lib_paths.push(resolved); - data.elf.loaded_libs.push(lib); - idx as u64 -} - -/// The heap a module's records take in its process, which keeps them for its -/// life: its place in the lists of libraries, paths and TLS modules, each of -/// which doubles, and its path. -fn library_heap(path: &str) -> usize { - use core::mem::size_of; - 2 * (size_of::() + size_of::() + size_of::()) - + charge::array::(path.len()) -} - -/// Allocates a TLS block for the current thread's DTV entry for `module_id`, -/// returning its virtual address. `module_id` crosses the trust boundary: -/// every rejection is an error return, never a panic. -pub(super) fn sys_tls_alloc_block(module_id: u64) -> u64 { - match tls_alloc_block(module_id) { - Ok(vaddr) => vaddr, - Err(e) => e.to_u64(), - } -} - -fn tls_alloc_block(module_id: u64) -> Result { - // The valid set is the process's own module list, which the kernel built. - if module_id == 0 { - return Err(SyscallError::InvalidArgument); - } - // Bounded by the kernel's own `DTV_INITIAL_CAPACITY`, never the DTV's own - // `len` field, which the process can rewrite. - if module_id > crate::loader::DTV_INITIAL_CAPACITY as u64 { - return Err(SyscallError::ResourceExhausted); - } - - let owner_arc = process::process_data(); - let (tls_memsz, tls_template) = { - let data = owner_arc.lock(); - let m = data.elf.tls_modules.iter().find(|m| m.module_id == module_id) - .ok_or(SyscallError::InvalidArgument)?; - (m.memsz, m.template) - }; - - // A DTV entry leaves DTV_UNALLOCATED once and never returns; serving a - // fresh block on a repeat call would leave the first mapping present, - // USER and writable, over whatever the PMM hands out next. - let tid = process::current_tid(); - let existing = process::with_process_data(|data| { - data.elf.dynamic_tls_blocks.get(&(tid, module_id)).map(|(b, _)| b.at) - }); - - let tls_vaddr = match existing { - Some(vaddr) => vaddr, - None => { - let place = Charge::current(charge::map_entry::<(process::Tid, u64), (process::KernelMapping, Charge)>())?; - let block = crate::object::shm::SharedMemObject::create(&crate::mm::charge::current(), tls_memsz.max(1) as u64)?; - if let Some(template) = &tls_template { - // SAFETY: the template is a bounds-checked view of the module's - // own image, which its `LoadedLib` keeps alive. - block.write(0, unsafe { template.as_slice() }); - } - let pt = process::current_address_space(); - let len = block.size(); - let vaddr = pt.lock().map(block.clone(), 0, len, kernel::vm::Prot::ReadWrite, crate::mm::space::At::ANYWHERE)?; - // A thread asks for its own blocks alone, so none raced this one in. - process::with_process_data(|data| { - data.elf.dynamic_tls_blocks.insert((tid, module_id), (process::KernelMapping { object: block, at: vaddr }, place)) - }); - vaddr - } - }; - - // Through the thread's own TLS object, never by chasing a pointer out of - // the FS base, which addresses user-writable memory. - let tls = process::with_current_data(|data| { - data.tls.as_ref().map(|tls| alloc::sync::Arc::clone(&tls.object)).expect("sys_tls_alloc_block: thread has no TLS block") - }); - // `module_id` is bounded non-zero and at most `DTV_INITIAL_CAPACITY` at the - // top of this function, so the slot is inside the DTV after its two - // header words, which the block's first bytes hold. - tls.write((2 + (module_id - 1)) * 8, &tls_vaddr.raw().to_ne_bytes()); - Ok(tls_vaddr.raw()) -} - -pub(super) fn sys_dlsym(handle: u64, name: &str) -> u64 { - let data_arc = process::process_data(); - let data = data_arc.lock(); - let idx = handle as usize; - if idx >= data.elf.loaded_libs.len() { - return SyscallError::NotFound.to_u64(); - } - match crate::elf::dlsym(&data.elf.loaded_libs[idx], name) { - Some(addr) => addr.raw(), - None => u64::MAX, - } -} - -/// Describes every loaded module into `buf`, returning the required byte -/// length; nothing is written unless the whole answer fits, and the length -/// never lands in `SyscallError`'s encoded range. -pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 { - use toyos_abi::syscall::ModuleInfo; - let info_size = core::mem::size_of::(); - - process::with_process_data(|data| { - let module_count = 1 + data.elf.loaded_libs.len(); - - let exe_path_bytes = data.exe_path.as_bytes(); - let total_path_bytes: usize = exe_path_bytes.len() - + data.elf.lib_paths.iter().map(|p| p.len()).sum::(); - - let required = module_count * info_size + total_path_bytes; - if out.len() < required { - return required as u64; - } - - // Record array ends at the first module's `path_offset`; paths are - // packed after it in module order. - let mut path_offset = (module_count * info_size) as u32; - - let (eh_addr, eh_size) = data.elf.exe_eh_frame_hdr; - let (phdr, phnum) = data.elf.exe_phdrs; - let exe_info = ModuleInfo { - base: data.elf.elf_base.raw(), - text_end: data.elf.exe_vaddr_max, - eh_frame_hdr: eh_addr, - eh_frame_hdr_size: eh_size, - phdr, - phnum: phnum.into(), - path_offset, - path_len: exe_path_bytes.len() as u32, - }; - out.write_at(0, toyos_abi::usersafe::bytes(&exe_info)); - out.write_at(path_offset as usize, exe_path_bytes); - path_offset += exe_path_bytes.len() as u32; - - for (i, lib) in data.elf.loaded_libs.iter().enumerate() { - let lib_path_bytes = if i < data.elf.lib_paths.len() { - data.elf.lib_paths[i].as_bytes() - } else { - b"" - }; - let lib_info = ModuleInfo { - base: lib.user_base.raw(), - text_end: lib.user_end(), - 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()), - phdr: (lib.user_base + lib.phdrs.image().start().get()).raw(), - phnum: lib.phdrs.count().into(), - path_offset, - path_len: lib_path_bytes.len() as u32, - }; - out.write_at((1 + i) * info_size, toyos_abi::usersafe::bytes(&lib_info)); - out.write_at(path_offset as usize, lib_path_bytes); - path_offset += lib_path_bytes.len() as u32; - } - - required as u64 - }) -} diff --git a/lib/elf/src/header.rs b/lib/elf/src/header.rs index 12e2123e984..ca92648eaeb 100644 --- a/lib/elf/src/header.rs +++ b/lib/elf/src/header.rs @@ -26,9 +26,8 @@ const EM_AARCH64: u16 = 183; /// The instruction set an image is built for. /// -/// Each machine names its own relocation numbers ([`crate::rela`]) and its own -/// TLS variant ([`crate::tls`]); nothing else in this crate is -/// per-architecture. +/// Each machine names its own relocation numbers ([`crate::rela`]); nothing +/// else in this crate is per-architecture. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Machine { X86_64, diff --git a/lib/elf/src/lib.rs b/lib/elf/src/lib.rs index d9c3e8acee7..31acb62c37e 100644 --- a/lib/elf/src/lib.rs +++ b/lib/elf/src/lib.rs @@ -1,10 +1,10 @@ //! ELF64 decoding, for the questions ToyOS asks a program image: **what does //! this file want mapped, where does everything it names live inside it, and -//! which of the numbers in it may the kernel act on?** +//! which of the numbers in it may a loader act on?** //! -//! An ELF is untrusted input — `spawn` opens whatever path a process names and -//! `dlopen` opens whatever path a process names — so the kernel's fail-fast -//! rule does not apply to anything decoded here. Every length, count, offset +//! An ELF is untrusted input — the kernel maps whatever image a spawn names, +//! and libc's loader whatever library a process names — so the kernel's +//! fail-fast rule does not apply to anything decoded here. Every length, count, offset //! and vaddr is checked before it is used, nothing is indexed without a bound, //! and no path panics. What cannot be satisfied is refused by name through //! [`Error`]; nothing is silently truncated to fit. @@ -19,7 +19,7 @@ //! //! It maps nothing, allocates nothing and writes nothing. Turning a //! [`Layout`] into an address space, applying a relocation and copying a TLS -//! template are the kernel's, and the split is the point: everything in here +//! template are its callers', and the split is the point: everything in here //! is a pure function of bytes and is tested on the host in milliseconds. //! //! # Scope diff --git a/lib/elf/src/tls.rs b/lib/elf/src/tls.rs index 3377319c55f..37cdabbe5f0 100644 --- a/lib/elf/src/tls.rs +++ b/lib/elf/src/tls.rs @@ -1,151 +1,7 @@ -//! Where everything goes inside a thread's TLS allocation, in the layout each -//! machine's psABI names ([`Variant`]), with the DTV the kernel writes at the -//! front of the allocation: -//! -//! ```text -//! variant II (x86-64): [DTV] [pad] [TLS data (.tdata + .tbss)] [TCB] -//! ^ tls_start ^ thread pointer -//! variant I (AArch64): [DTV] [pad] [TCB] [TLS data (.tdata + .tbss)] -//! ^ thread pointer, tls_start - gap -//! ``` -//! -//! Variant II: the linker computes the executable's `TPOFF = sym_offset - -//! round(memsz, p_align)` ([`exe_extent`]), so the executable's module, so -//! rounded, ends at the thread pointer, `tls_start + total_memsz`. Variant I: -//! the linker computes `TPOFF = align_up(16, p_align) + sym_offset` from the -//! executable's own `PT_TLS`, so the executable's block is the first one, -//! `gap` above the thread pointer. Either way `tls_start` carries the -//! executable's alignment. Every input is a sum of numbers a file -//! declared, which is why every step here is checked and the whole thing is a -//! pure function: `dtv_bytes <= tls_start` is the property, and it used to be -//! an assertion in the kernel reached from a crafted `PT_TLS`. +//! A thread-local datum's place inside its module's `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)] -pub enum Variant { - /// The thread pointer addresses a two-word TCB, `[DTV, reserved]`, and the - /// first module's data follows it. - I, - /// The thread pointer addresses a TCB, `[self, DTV]`, after the last - /// module's data. - II, -} - -impl Variant { - pub const fn of(machine: Machine) -> Variant { - match machine { - Machine::X86_64 => Variant::II, - Machine::Aarch64 => Variant::I, - } - } -} - -/// A thread's static TLS, the executable's module alone, as much of it as its -/// layout depends on. Its alignment is a power of two, or it does not exist -/// ([`Static::new`]). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Static { - variant: Variant, - /// The module's bytes: in variant II its [`exe_extent`]. - total_memsz: usize, - /// Its `p_align`, "no constraint" as 8: variant I's linker fixed the - /// executable's distance from the thread pointer from it. - align: usize, -} - -/// A planned TLS allocation, in offsets from the base of one block. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TlsBlock { - /// Bytes to allocate, a whole number of granules. - pub alloc_size: usize, - /// Where the first module's TLS data begins. Aligned to the requested - /// alignment, and never below `dtv_bytes`. - pub tls_start: usize, - /// Where the thread pointer goes. - pub tp_offset: usize, -} - -impl Static { - /// `None` for an alignment that is not a power of two: a mask that is not - /// a mask can place the data anywhere. Zero and one mean "no constraint". - pub fn new(variant: Variant, total_memsz: usize, align: usize) -> Option { - Some(Static { variant, total_memsz, align: effective(align)? }) - } - - pub fn total_memsz(self) -> usize { - self.total_memsz - } - - pub fn align(self) -> usize { - self.align - } - - /// No module at all: what a thread of a program without TLS is given. - pub const fn empty(variant: Variant) -> Static { - Static { variant, total_memsz: 0, align: 8 } - } - - /// Lay out one thread's TLS block, or `None` for a layout no allocation - /// can hold. - /// - /// `granule` is the allocation unit the block is rounded up to, - /// `dtv_bytes` the fixed header the kernel writes at offset zero, and - /// `tcb_size` variant II's TCB; variant I's is the `gap` below its data. - /// All three are kernel constants; everything else came out of a file. - pub fn plan(self, tcb_size: usize, dtv_bytes: usize, granule: usize) -> Option { - debug_assert!(granule.is_power_of_two()); - let align = self.align; - match self.variant { - Variant::II => { - let block_size = self.total_memsz.checked_add(tcb_size)?; - // The DTV goes at the start of this same allocation and the TLS - // data is placed `align`-aligned above it, so both belong in - // the size. Sizing from the block and the alignment alone left - // `tls_start` free to land inside the DTV. - let alloc_size = - align_up(block_size.checked_add(dtv_bytes)?.checked_add(align)?, granule)?; - // Rounding *down* by `align` loses less than `align`, and - // `align` was one of the addends — so this is at least - // `dtv_bytes + 1` and the DTV can never be overwritten by TLS - // data. - let tls_start = (alloc_size - block_size) & !(align - 1); - Some(TlsBlock { alloc_size, tls_start, tp_offset: tls_start + self.total_memsz }) - } - Variant::I => { - let gap = self.gap(); - // At least 16, so the thread pointer `gap` below is 16-aligned. - let tls_start = align_up(dtv_bytes.checked_add(gap)?, align.max(16))?; - let alloc_size = align_up(tls_start.checked_add(self.total_memsz)?, granule)?; - Some(TlsBlock { alloc_size, tls_start, tp_offset: tls_start - gap }) - } - } - } - - /// A static-TLS datum's initial-exec offset from the thread pointer: psABI - /// `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.checked_sub(i64::try_from(self.total_memsz).ok()?), - Variant::I => from_start.checked_add(i64::try_from(self.gap()).ok()?), - } - } - - /// Variant I's distance from the thread pointer to the module's data: - /// `align_up(16, p_align)`, the linker's own. - fn gap(self) -> usize { - self.align.max(16) - } -} - /// `S + A` inside one module's TLS segment, `0..=p_memsz`: made only by /// [`TlsOffset::of`] against a [`TlsSegment`] a parse derived. /// @@ -166,23 +22,3 @@ impl TlsOffset { self.0 } } - -/// The bytes variant II's executable module takes below the thread pointer: its -/// `p_memsz` rounded up to its `p_align`, the psABI's `tlsoffset1 = -/// round(tlssize1, align1)`, which a linker bakes into every local-exec access -/// and the loader can only honour. `None` is a size the rounding overflows. -pub fn exe_extent(memsz: usize, align: usize) -> Option { - align_up(memsz, align.max(1)) -} - -/// `align`, with "no constraint" as 8; `None` for one that is not a power of two. -fn effective(align: usize) -> Option { - if align != 0 && !align.is_power_of_two() { - return None; - } - Some(if align > 1 { align } else { 8 }) -} - -fn align_up(value: usize, granule: usize) -> Option { - value.checked_add(granule - 1).map(|v| v & !(granule - 1)) -} diff --git a/lib/elf/tests/fuzz.rs b/lib/elf/tests/fuzz.rs index 50d1d435f3d..d15571f274c 100644 --- a/lib/elf/tests/fuzz.rs +++ b/lib/elf/tests/fuzz.rs @@ -1,19 +1,18 @@ //! 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. +//! `DT_INIT_ARRAY` and the TLS segment's size and alignment. //! //! 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: +//! hostile the rest, and then run through the calls libc'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; +//! - nothing panics — the test binary runs with overflow checks on, so an +//! unchecked sum here is the 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. +//! image placed at the highest address its span fits at, and every TLS +//! offset inside its module's `PT_TLS`. //! //! The generator also counts what it got past the parse, so a regression that //! refuses everything cannot pass as one that accepts nothing wrong. @@ -25,7 +24,7 @@ use common::*; use toyos_elf::dynamic::{Dynamic, InitArray}; use toyos_elf::rela::{self, Op, ReadTables, RelaTable, Rules, TlsRef}; use toyos_elf::sym::{self, SymTab}; -use toyos_elf::tls::{self, Static, TlsOffset, Variant}; +use toyos_elf::tls::TlsOffset; use toyos_elf::{ImageRange, Layout, Machine, TlsSegment}; /// Images per run: every one of them reaches the relocation parse unless its @@ -51,12 +50,6 @@ 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; @@ -138,7 +131,7 @@ fn generate(rng: &mut Rng) -> Case { // rate per number would refuse nearly every image over its first one. rng.hostile = rng.pick(&[0, 1, 2, 5, 15]); let machine = if rng.chance(50) { Machine::X86_64 } else { Machine::Aarch64 }; - // Where the kernel loads a library: at vaddr 0. + // Where a library is loaded: at vaddr 0. let vmin = 0u64; let bss = rng.pick(&[0u64, 0x800, 0x3000]); let span = SIZE as u64 + bss; @@ -275,7 +268,7 @@ enum Placement { Highest, } -/// Everything the kernel's loader decides about one image, in its order, +/// Everything the 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<(), ()> { @@ -350,42 +343,12 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), } } - // The thread's static block: this module alone, placed as the kernel's - // `build_tls_layout` places an executable's. - let variant = Variant::of(case.machine); - let (block, base_offset, memsz) = match tls.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, - }; - (Static::new(variant, placed, align).ok_or(())?, 0, 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 memsz = tls.and_then(TlsSegment::occupied).map_or(0, |t| t.memsz()); 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 + // another module's, which the loader 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(()), @@ -414,8 +377,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), reached.bind += 1; } Op::Tpoff64(t) | Op::Tpoff32(t) => { - if let Some(off) = resolve(t)? { - thread_offset(off, matches!(r.op(), Op::Tpoff32(_)))?; + if resolve(t)?.is_some() { reached.tpoff += 1; } } diff --git a/lib/elf/tests/tls.rs b/lib/elf/tests/tls.rs deleted file mode 100644 index 98a7d71b847..00000000000 --- a/lib/elf/tests/tls.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! The TLS block's arithmetic, over the whole space a file can name. -//! -//! `total_memsz` and `align` are sums of numbers a file declared, and the -//! kernel used to assert `dtv_size < tls_start` after computing them — a -//! kernel-bug assert reached from a crafted `PT_TLS`. The property is proved -//! here instead, which is what lets the assert go. - -#[allow(dead_code)] -mod common; - -use common::tls_segment; -use toyos_elf::sym; -use toyos_elf::tls::{self, Static, TlsOffset, Variant}; - -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 = [ - 0usize, - 1, - 8, - 4095, - 4096, - DTV, - DTV + 1, - 0x1F_FF00, - GRANULE - TCB - 1, - GRANULE, - GRANULE + 1, - 16 * GRANULE, - ]; - let aligns = [0usize, 1, 2, 8, 16, 64, 4096, 65536, GRANULE]; - for &memsz in &sizes { - for &align in &aligns { - let plan = Static::new(Variant::II, memsz, align) - .and_then(|s| s.plan(TCB, DTV, GRANULE)) - .unwrap_or_else(|| panic!("no plan for memsz {memsz} align {align}")); - let effective = if align > 1 { align } else { 8 }; - assert!( - plan.tls_start >= DTV, - "memsz {memsz} align {align}: TLS data at {} overlaps a {DTV}-byte DTV", - plan.tls_start, - ); - assert_eq!(plan.tls_start % effective, 0, "memsz {memsz} align {align}"); - assert_eq!(plan.tp_offset, plan.tls_start + memsz); - assert!( - plan.tp_offset + TCB <= plan.alloc_size, - "memsz {memsz} align {align}: the TCB runs past the allocation", - ); - assert_eq!(plan.alloc_size % GRANULE, 0, "memsz {memsz} align {align}"); - } - } -} - -#[test] -fn a_size_no_allocation_can_hold_has_no_plan() { - for variant in [Variant::I, Variant::II] { - let plan = |memsz, align| Static::new(variant, memsz, align).unwrap().plan(TCB, DTV, GRANULE); - assert_eq!(plan(usize::MAX, 8), None, "{variant:?}"); - assert_eq!(plan(usize::MAX - TCB, 8), None, "{variant:?}"); - assert_eq!(plan(usize::MAX - GRANULE, GRANULE), None, "{variant:?}"); - } -} - -/// `!(align - 1)` is only a mask for a power of two, and a mask that is not a -/// mask can place the TLS data anywhere — the DTV included. -#[test] -fn an_alignment_that_is_not_a_power_of_two_has_no_plan() { - for align in [3usize, 5, 6, 100, usize::MAX] { - for variant in [Variant::I, Variant::II] { - assert_eq!(Static::new(variant, 64, align), None, "align {align}"); - } - } -} - -/// `TPOFF64`/`TPOFF32` is `S + A - tp`: the addend is in every branch's answer. -#[test] -fn tpoff_carries_the_addend() { - let total = 0x200usize; - let two = Static::new(Variant::II, total, 64).unwrap(); - let one = Static::new(Variant::I, total, 64).unwrap(); - for &module_addr in &[0u64, 8, 0x40, 0x1F0] { - assert_eq!(two.tpoff(0, datum(module_addr)), Some(module_addr as i64 - total as i64)); - assert_eq!(one.tpoff(0, datum(module_addr)), Some(module_addr as i64 + 64)); - 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(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).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).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 -/// executable's data has to start exactly that far above the thread pointer. -#[test] -fn variant_i_puts_the_module_where_its_linker_put_it() { - let sizes = [0usize, 1, 8, 0x48, 4096, DTV + 1, GRANULE - 1, GRANULE + 1]; - let aligns = [0usize, 1, 2, 8, 16, 64, 4096, GRANULE]; - for &memsz in &sizes { - for &align in &aligns { - let s = Static::new(Variant::I, memsz, align).unwrap(); - let plan = s.plan(TCB, DTV, GRANULE).unwrap(); - let gap = 16usize.max(align); - let at = format!("memsz {memsz} align {align}: {plan:?}"); - assert_eq!(plan.tls_start - plan.tp_offset, gap, "{at}"); - assert_eq!(s.tpoff(0, datum(0)), Some(gap as i64), "{at}"); - assert!(plan.tp_offset >= DTV, "{at}: the TCB overlaps the DTV"); - assert_eq!(plan.tp_offset % 16, 0, "{at}"); - assert_eq!(plan.tls_start % align.max(16), 0, "{at}"); - assert!(plan.tls_start + memsz <= plan.alloc_size, "{at}"); - assert_eq!(plan.alloc_size % GRANULE, 0, "{at}"); - } - } -} - -/// rust-lld's own answer, read off an `aarch64-unknown-toyos` executable whose -/// `PT_TLS` is 64-aligned: `add x0, x8, #0x40` for the datum at offset 0 of -/// `.tdata`, `#0x80` for the one at 0x40. -#[test] -fn variant_i_agrees_with_what_lld_linked() { - let s = Static::new(Variant::I, 0xb0, 64).unwrap(); - assert_eq!(s.tpoff(0, datum(0)), Some(0x40)); - assert_eq!(s.tpoff(0x40, datum(0)), Some(0x80)); -} - -/// The machine names the variant: x86-64's psABI is variant II, AArch64's -/// variant I. -#[test] -fn each_machine_has_its_own_variant() { - assert_eq!(Variant::of(toyos_elf::Machine::X86_64), Variant::II); - assert_eq!(Variant::of(toyos_elf::Machine::Aarch64), Variant::I); -} - -/// Variant II's executable ends at the thread pointer at its extent, not its -/// size: a local-exec access is `sym_offset - round(p_memsz, p_align)`. -#[test] -fn the_executables_extent_is_its_size_rounded_to_its_alignment() { - assert_eq!(tls::exe_extent(0xa8, 0x40), Some(0xc0)); - assert_eq!(tls::exe_extent(0xc0, 0x40), Some(0xc0)); - assert_eq!(tls::exe_extent(0x70, 8), Some(0x70)); - // No alignment is alignment one. - assert_eq!(tls::exe_extent(0x71, 0), Some(0x71)); - assert_eq!(tls::exe_extent(usize::MAX, 0x40), None); - - // The extent is what ends at the thread pointer, so `tp - base` is the - // offset the linker subtracted. - let s = Static::new(Variant::II, tls::exe_extent(0xa8, 0x40).unwrap(), 0x40).unwrap(); - assert_eq!(s.tpoff(0, datum(0)), Some(-0xc0)); -} diff --git a/rust b/rust index aa0d9be2c2b..fd00d661c36 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit aa0d9be2c2b2b04fc51ad966a4a5d8ba137c589e +Subproject commit fd00d661c36d8c1d2e2920ebe2e02d4c20c01142 diff --git a/sdk/abi/src/arch/aarch64.rs b/sdk/abi/src/arch/aarch64.rs index d20b8327fe2..cce0331846b 100644 --- a/sdk/abi/src/arch/aarch64.rs +++ b/sdk/abi/src/arch/aarch64.rs @@ -1,22 +1,3 @@ -use crate::{tcb, Tid}; - -/// Where a thread finds its own [`Tid`]: the kernel writes it into the thread -/// control block at `TP + TCB_TID` before the thread's first instruction. The -/// word is the thread's own memory, so what it says is the thread's word about -/// itself and nothing more. -pub const TCB_TID: usize = tcb::AARCH64_TID; - -/// The calling thread's id, read off its control block without a syscall. -#[inline] -pub fn current_tid() -> Tid { - let tp: u64; - // SAFETY: a read of `TPIDR_EL0`, the thread pointer, into a register. - unsafe { core::arch::asm!("mrs {tp}, tpidr_el0", tp = out(reg) tp, options(nomem, nostack)) }; - // SAFETY: `TP + TCB_TID` lies inside the psABI's TCB at TP, below the - // first TLS block (`tcb::AARCH64_BYTES`). - Tid(unsafe { core::ptr::read_volatile((tp as usize + TCB_TID) as *const u32) }) -} - /// The counter the clock page's line describes: the generic timer's /// virtual count, which EL0 may read, read after every earlier instruction and /// before any later one, as the x86-64 one is. diff --git a/sdk/abi/src/arch/mod.rs b/sdk/abi/src/arch/mod.rs index cb4ac4d86e2..4afd134d82d 100644 --- a/sdk/abi/src/arch/mod.rs +++ b/sdk/abi/src/arch/mod.rs @@ -1,7 +1,6 @@ //! What a process reads off its own CPU without a syscall, one module per -//! architecture: the counter the clock page describes, the thread's id in -//! its thread control block, `in` and `out` where there is a port space, and -//! the processor's CPUID identity. +//! architecture: the counter the clock page describes, `in` and `out` where +//! there is a port space, and the processor's CPUID identity. #[cfg(target_arch = "aarch64")] mod aarch64; @@ -9,6 +8,6 @@ mod aarch64; mod x86_64; #[cfg(target_arch = "aarch64")] -pub use aarch64::{counter, current_tid, ioport, processor_id, TCB_TID}; +pub use aarch64::{counter, ioport, processor_id}; #[cfg(target_arch = "x86_64")] -pub use x86_64::{counter, current_tid, ioport, processor_id, TCB_TID}; +pub use x86_64::{counter, ioport, processor_id}; diff --git a/sdk/abi/src/arch/x86_64.rs b/sdk/abi/src/arch/x86_64.rs index 39e7fcfbef1..caa6d44dfcf 100644 --- a/sdk/abi/src/arch/x86_64.rs +++ b/sdk/abi/src/arch/x86_64.rs @@ -1,29 +1,3 @@ -use crate::{tcb, Tid}; - -/// Where a thread finds its own [`Tid`]: the kernel writes it into the thread -/// control block at `TP + TCB_TID` before the thread's first instruction. The -/// word is the thread's own memory, so what it says is the thread's word about -/// itself and nothing more. -pub const TCB_TID: usize = tcb::X86_64_TID; - -/// The calling thread's id, read off its control block without a syscall. -#[inline] -pub fn current_tid() -> Tid { - let tid: u32; - // SAFETY: `fs` is this thread's TP, set by the kernel at the thread's - // start, and `TP + TCB_TID` lies inside the 64-byte TCB the kernel - // reserves there; a 4-byte load of it touches nothing else. - unsafe { - core::arch::asm!( - "mov {tid:e}, dword ptr fs:[{at}]", - tid = out(reg) tid, - at = const TCB_TID, - options(nostack, readonly, preserves_flags), - ); - } - Tid(tid) -} - /// The counter the clock page's line describes: the time-stamp counter, /// read after every earlier load has completed, so a stamp taken after reading /// another writer's record cannot be older than that record's, and before any diff --git a/sdk/abi/src/lib.rs b/sdk/abi/src/lib.rs index 8c7a29d0b26..17879b6c966 100644 --- a/sdk/abi/src/lib.rs +++ b/sdk/abi/src/lib.rs @@ -102,31 +102,7 @@ unsafe impl Sync for FramebufferInfo {} // pointers, no interior mutability) covers `Send`. unsafe impl Send for FramebufferInfo {} -/// Where each architecture's thread control block keeps the thread's [`Tid`], -/// as an offset from TP, inside the TCB its psABI puts there. -pub mod tcb { - /// x86-64, TLS variant II: the TCB is at and above TP, `TP + 0` the - /// psABI's self-pointer and `TP + 8` the DTV pointer, inside the - /// [`X86_64_BYTES`] the kernel reserves; TLS data is below TP. - pub const X86_64_TID: usize = 16; - pub const X86_64_BYTES: usize = 64; - /// AArch64, TLS variant I: the TCB is the [`AARCH64_BYTES`] at TP, `TP + 0` - /// the DTV pointer and `TP + 8` the word the psABI leaves to the - /// implementation, which is this; the first TLS block starts at - /// `TP + AARCH64_BYTES`, so nothing of the TCB may lie there. - pub const AARCH64_TID: usize = 8; - pub const AARCH64_BYTES: usize = 16; - - /// Each tid word lies inside its TCB and on none of the words its psABI - /// already names: past x86-64's self-pointer and DTV pointer, and past - /// AArch64's DTV pointer and wholly below its first TLS block. - const TID: usize = core::mem::size_of::(); - const _: () = assert!(X86_64_TID >= 16 && X86_64_TID + TID <= X86_64_BYTES); - const _: () = assert!(AARCH64_TID >= 8 && AARCH64_TID + TID <= AARCH64_BYTES); - const _: () = assert!(AARCH64_BYTES == 16, "AArch64's psABI TCB is two words"); -} - -pub use arch::{current_tid, ioport, processor_id, TCB_TID}; +pub use arch::{ioport, processor_id}; /// An x86 processor's CPUID identity (Intel SDM Vol. 2A, "CPUID"; AMD APM /// Vol. 3, CPUID Fn0000_0000 and Fn0000_0001_EAX). diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 10f9ba136a5..5c52726c18c 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -77,9 +77,6 @@ pub const SYS_NANOSLEEP: u64 = 49; /// [`dup`] and [`dup_narrowed`]. pub const SYS_HANDLE_DUP: u64 = 50; pub const SYS_GETPID: u64 = 51; -pub const SYS_DLOPEN: u64 = 55; -pub const SYS_DLSYM: u64 = 56; -pub const SYS_DLCLOSE: u64 = 57; pub const SYS_FUTEX_WAIT: u64 = 58; pub const SYS_FUTEX_WAKE: u64 = 59; pub const SYS_STACK_INFO: u64 = 61; @@ -95,16 +92,11 @@ pub const SYS_CLOCK_EPOCH: u64 = 75; /// /// [`Acceptor`]: crate::handle::RawHandle pub const SYS_ACCEPT: u64 = 86; -/// Allocate a TLS block for a dlopen'd module on the current thread. -/// Arg0: module_id (1-based DTV index). Returns the block's virtual address, -/// or a `SyscallError` word — see [`tls_alloc_block`]. -pub const SYS_TLS_ALLOC_BLOCK: u64 = 88; /// Create an [`inbox`](crate::inbox) and map its rings. See [`inbox_setup`]. pub const SYS_INBOX_SETUP: u64 = 89; /// Hand queued submissions to the kernel and/or wait for completions. See /// [`inbox_submit`]. pub const SYS_INBOX_SUBMIT: u64 = 90; -pub const SYS_QUERY_MODULES: u64 = 91; /// Debug syscall. Arg0 selects the action: /// 0 = kernel panic (triggers panic!() in syscall context) /// 1 = kernel fault (null pointer deref in kernel context) @@ -374,6 +366,8 @@ pub const SYS_YIELD: u64 = 168; pub const SYS_LOG_LAST_WORDS: u64 = 169; /// List the names held by a namespace, without opening any connector. pub const SYS_NAMESPACE_NAMES: u64 = 170; +/// Point the calling thread's thread pointer somewhere. See [`set_thread_pointer`]. +pub const SYS_SET_THREAD_POINTER: u64 = 171; /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. @@ -381,13 +375,13 @@ pub const SYS_NAMESPACE_NAMES: u64 = 170; /// **The profile's parts sum to its total, and that is the whole requirement.** /// A bin array narrower than the ABI reaches drops calls out of the line while /// the total goes on counting them. -pub const SYSCALL_PROFILE_BINS: usize = 172; +pub const SYSCALL_PROFILE_BINS: usize = 173; /// Where a number this ABI does not issue is counted. Merging is a degradation /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_NAMESPACE_NAMES < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_SET_THREAD_POINTER < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; /// Every bit `SYS_PROCESS_WAIT`'s flag word defines; a wait carrying another @@ -453,26 +447,6 @@ 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 memory object 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 @@ -554,7 +528,7 @@ pub const MAX_SPAWN_LABELS_LEN: usize = MAX_LABELS_LEN - SELF_LABEL.len() - QUIT use crate::end::End; use crate::handle::Rights; use crate::pci::{DmaGrant, DmaMapping}; -use crate::{Pid, RawHandle, HANDLE_INVALID}; +use crate::{Pid, RawHandle, Tid, HANDLE_INVALID}; /// Syscall error with a specific code. Values occupy the top of the u64 range: /// error code N is encoded as `u64::MAX - N`. Any return value `>= u64::MAX - 255` @@ -1188,7 +1162,8 @@ pub const MAX_THREADS: usize = 4096; /// Spawn a new thread with the given entry point, stack pointer, argument, and stack base. /// `stack_base` is the bottom of the user stack (for stack info queries). An -/// `entry` outside the user half is `InvalidArgument`. +/// `entry` outside the user half is `InvalidArgument`. The thread starts with +/// a thread pointer of zero: its first act names its own ([`set_thread_pointer`]). /// /// # Safety /// `entry` must be a valid function pointer and `stack`/`stack_base` must @@ -1197,6 +1172,15 @@ pub unsafe fn thread_spawn(entry: u64, stack: u64, arg: u64, stack_base: u64) -> syscall(SYS_THREAD_SPAWN, entry, stack, arg, stack_base) } +/// Point the calling thread's thread pointer (x86-64's FS base, AArch64's +/// `TPIDR_EL0`) at `tp`, and answer the caller's own [`Tid`]. The pointer is the +/// thread's across every switch and the kernel never reads through it: what +/// lies there is the program's. `InvalidArgument` for a `tp` that is neither +/// zero nor a user-half address. +pub fn set_thread_pointer(tp: u64) -> Result { + check(syscall(SYS_SET_THREAD_POINTER, tp, 0, 0, 0)).map(|tid| Tid::from_raw(tid as u32)) +} + /// Wait for thread `tid` to exit. pub fn thread_join(tid: u64) -> u64 { syscall(SYS_THREAD_JOIN, tid, 0, 0, 0) @@ -1209,7 +1193,7 @@ pub const CPU_MASK_WORDS: usize = 16; /// Let thread `tid` of the calling process run only on the CPUs `mask` names. /// `tid` names a thread as [`thread_join`]'s does, 0 the main thread; a -/// thread names itself by [`current_tid`](crate::current_tid). A shorter +/// thread's own is what [`set_thread_pointer`] answered it. A shorter /// mask's missing words are empty. /// /// The mask is a placement choice, not a right: any non-empty set of online @@ -2458,63 +2442,6 @@ pub fn getpid() -> Pid { Pid(syscall(SYS_GETPID, 0, 0, 0, 0) as u32) } -/// Libraries one process holds, `DT_NEEDED` and loaded alike; [`dl_open`] of a -/// name it does not hold past it is `ResourceExhausted`. -pub const MAX_LIBRARIES: usize = 1024; - -/// 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 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]; - if init_array_ptr != 0 && init_count > 0 { - // SAFETY: `init_array_ptr`/`init_count` are not userland's to make - // up — `SYS_DLOPEN` wrote them, naming the `.init_array` table it - // just built from the loaded ELF's own section headers, so they name - // `init_count` valid `usize`-sized, `usize`-aligned entries. - let entries = unsafe { core::slice::from_raw_parts(init_array_ptr as *const usize, init_count as usize) }; - for &entry in entries { - if entry != 0 { - // SAFETY: a non-zero `.init_array` entry is the address of a - // constructor the loader placed there — by the ELF `DT_INIT_ARRAY` - // contract every entry is a valid `extern "C" fn()`, never data. - let f: extern "C" fn() = unsafe { core::mem::transmute(entry) }; - f(); - } - } - } - 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 -/// The returned address must only be transmuted to the correct function signature. -pub unsafe fn dl_sym(handle: u64, name: &[u8]) -> Result { - check(syscall(SYS_DLSYM, handle, name.as_ptr() as u64, name.len() as u64, 0)) -} - -/// Close a loaded shared library handle. -pub fn dl_close(handle: u64) -> u64 { - syscall(SYS_DLCLOSE, handle, 0, 0, 0) -} - /// Block if `*addr == expected`. Returns 0 on wake, 1 on timeout. /// `None` = wait forever, `Some(nanos)` = timeout. /// @@ -2775,17 +2702,6 @@ pub fn device_dma_unmap(claim: RawHandle, device_addr: u64) -> Result<(), Syscal check_unit(syscall(SYS_DEVICE_DMA_UNMAP, claim.0 as u64, device_addr, 0, 0)) } -/// Allocate a TLS block for a dlopen'd module on the current thread. -/// -/// The block's *virtual* address, which is what the kernel writes into the DTV. -/// `InvalidArgument` for a `module_id` of 0 or one outside the process's module -/// list, `ResourceExhausted` past `DTV_INITIAL_CAPACITY` or when the mapping -/// fails — and every user address is far below where `SyscallError` encodes, so -/// no block is ever read as one. -pub fn tls_alloc_block(module_id: u64) -> Result { - check(syscall(SYS_TLS_ALLOC_BLOCK, module_id, 0, 0, 0)) -} - crate::user_safe! { /// An inbox and where its page of rings is mapped. /// @@ -2829,73 +2745,6 @@ pub fn inbox_submit(handle: RawHandle, to_submit: u32, min_complete: u32, timeou .map(|n| n as u32) } -// Module info (for stack unwinding / backtraces) - -crate::user_safe! { - /// Information about a loaded module (executable or shared library). - /// - /// Buffer layout returned by `SYS_QUERY_MODULES`: - /// `[ModuleInfo; count]` followed by packed path strings. - /// Each `ModuleInfo::path_offset` is relative to the start of the buffer. - #[derive(Clone, Copy)] - pub struct ModuleInfo { - /// Load base address (bias) of this module. - pub base: u64, - /// End of the last mapped segment (base + vaddr_max). - pub text_end: u64, - /// Absolute virtual address of `.eh_frame_hdr` (0 if none). - pub eh_frame_hdr: u64, - /// Size of `.eh_frame_hdr` in bytes. - pub eh_frame_hdr_size: u64, - /// Absolute virtual address of the module's program header table: the - /// kernel loads no module whose table a `PT_LOAD` does not map. - pub phdr: u64, - /// Entries in that table, `e_phnum`: a `u64` so the record has no padding. - pub phnum: u64, - /// Byte offset of the module's path string within the buffer. - pub path_offset: u32, - /// Length of the path string in bytes. - pub path_len: u32, - } -} - -/// Query all loaded modules (exe + dlopen'd libs) in the current process. -/// -/// Returns the number of **bytes** the description needs, which is a count -/// only of bytes: the records carry packed path strings, so a module count -/// cannot size a retry. `n <= buf.len()` means the description is in the -/// buffer; `n > buf.len()` means nothing was written and `n` is what to -/// allocate. An empty buffer is therefore a size query. -/// -/// The records occupy `buf[..records[0].path_offset]` — each module's path is -/// written after the last record, so the first one's `path_offset` is where -/// the array ends. -pub fn query_modules(buf: &mut [u8]) -> Result { - check(syscall(SYS_QUERY_MODULES, buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0)).map(|n| n as usize) -} - -/// Every record of one [`query_modules`] answer with its path, executable -/// first: `answer` is the `n` bytes the call reported. -/// -/// A record or path outside `answer` is a kernel that broke the layout above, -/// and panics. -pub fn modules(answer: &[u8]) -> impl Iterator { - let size = core::mem::size_of::(); - let record = move |i: usize| { - let bytes = &answer[i * size..(i + 1) * size]; - // SAFETY: `bytes` holds `size_of::()` bytes, read without - // an alignment requirement, and every bit pattern of its integer - // fields is a `ModuleInfo`. - unsafe { (bytes.as_ptr() as *const ModuleInfo).read_unaligned() } - }; - let count = if answer.is_empty() { 0 } else { record(0).path_offset as usize / size }; - (0..count).map(move |i| { - let info = record(i); - let path = info.path_offset as usize; - (info, &answer[path..path + info.path_len as usize]) - }) -} - crate::user_safe! { /// A job's account, as [`SYS_ACCOUNT_INFO`] answers it, in bytes. A /// job's memory stays counted to it while an object it made lives, so @@ -2965,84 +2814,6 @@ pub fn process_stats(proc: RawHandle, stats: &mut ProcessStats) -> Result<(), Sy mod tests { use super::*; - /// **The encoder is the wire, so the test decodes the wire.** - /// - /// Not `bytes(&info).len() == size_of::()`, which a padded - /// struct passes: every field is read back out of the slice at the offset - /// `#[repr(C)]` puts it at. - #[test] - fn module_info_s_bytes_are_the_fields_and_nothing_between_them() { - let info = ModuleInfo { - base: 0x1122_3344_5566_7788, - text_end: 0x2233_4455_6677_8899, - eh_frame_hdr: 0x3344_5566_7788_99aa, - eh_frame_hdr_size: 0x44, - phdr: 0x5566_7788_99aa_bbcc, - phnum: 0x77, - path_offset: 0x55, - path_len: 0x66, - }; - let b = crate::usersafe::bytes(&info); - assert_eq!(b.len(), 56); - assert_eq!(u64::from_ne_bytes(b[0..8].try_into().unwrap()), info.base); - assert_eq!(u64::from_ne_bytes(b[8..16].try_into().unwrap()), info.text_end); - assert_eq!(u64::from_ne_bytes(b[16..24].try_into().unwrap()), info.eh_frame_hdr); - assert_eq!(u64::from_ne_bytes(b[24..32].try_into().unwrap()), info.eh_frame_hdr_size); - assert_eq!(u64::from_ne_bytes(b[32..40].try_into().unwrap()), info.phdr); - assert_eq!(u64::from_ne_bytes(b[40..48].try_into().unwrap()), info.phnum); - assert_eq!(u32::from_ne_bytes(b[48..52].try_into().unwrap()), info.path_offset); - assert_eq!(u32::from_ne_bytes(b[52..56].try_into().unwrap()), info.path_len); - } - - fn record(base: u64, path_offset: u32, path_len: u32) -> ModuleInfo { - ModuleInfo { - base, - text_end: base + 0x1000, - eh_frame_hdr: 0, - eh_frame_hdr_size: 0, - phdr: base + 0x40, - phnum: 3, - path_offset, - path_len, - } - } - - /// An answer laid out as the kernel writes one — records, then the paths - /// packed in module order — decodes to those records and paths, in order, - /// at an offset whatever alignment the buffer has. - #[test] - fn modules_decodes_every_record_and_its_path() { - let size = core::mem::size_of::() as u32; - let exe = record(0x100_0000_0000, 2 * size, 9); - let lib = record(0x200_0000_0000, 2 * size + 9, 13); - let mut answer = [0u8; 1 + 2 * 56 + 9 + 13]; - let wire = &mut answer[1..]; - wire[..56].copy_from_slice(crate::usersafe::bytes(&exe)); - wire[56..112].copy_from_slice(crate::usersafe::bytes(&lib)); - wire[112..121].copy_from_slice(b"/bin/prog"); - wire[121..].copy_from_slice(b"/lib/libx.so\0"); - let got: [(u64, u64, &[u8]); 2] = { - let mut it = modules(&answer[1..]).map(|(m, path)| (m.base, m.phdr, path)); - let pair = [it.next().unwrap(), it.next().unwrap()]; - assert!(it.next().is_none(), "two records, and no third read out of the paths"); - pair - }; - assert_eq!(got[0], (exe.base, exe.phdr, &b"/bin/prog"[..])); - assert_eq!(got[1], (lib.base, lib.phdr, &b"/lib/libx.so\0"[..])); - assert_eq!(modules(&[]).count(), 0); - } - - /// A path the kernel says runs past its own answer is a broken layout, - /// not a shorter name. - #[test] - #[should_panic] - fn modules_refuses_a_path_past_the_answer() { - let size = core::mem::size_of::() as u32; - let mut answer = [0u8; 56 + 4]; - answer[..56].copy_from_slice(crate::usersafe::bytes(&record(0, size, 5))); - modules(&answer).for_each(drop); - } - /// The four readers of a `devices` entry share this parser, so what it /// accepts is what a config may say; a PCI function is named by its /// arrival, which no entry spells. diff --git a/sdk/libc/arch/src/dlimage_relocations.rs b/sdk/libc/arch/src/dlimage_relocations.rs new file mode 100644 index 00000000000..09638cefccf --- /dev/null +++ b/sdk/libc/arch/src/dlimage_relocations.rs @@ -0,0 +1,222 @@ +//! The loader's image work on a shared object laid out here: what each +//! relocation it writes is worth, which ones it refuses, and the images it +//! refuses before any is laid out. The image is the host's machine's, so each +//! host checks its own architecture's relocation numbers. + +use crate::dlimage::{self, Mapped, Pass, Scope}; +use toyos_elf::RelocError; + +const SIZE: usize = 0x2000; +const PHDRS: usize = 0x40; +const SYMTAB: usize = 0x200; +const STRTAB: usize = 0x400; +const RELA: usize = 0x600; +const DYNAMIC: usize = 0x800; +/// The writable segment, `[0x1000, 0x2000)` in memory, its first 0x100 bytes file bytes. +const DATA: u64 = 0x1000; +const TLS: u64 = 0x1080; + +#[cfg(target_arch = "x86_64")] +mod machine { + pub const EM: u16 = 62; + pub const RELATIVE: u32 = 8; + pub const GLOB_DAT: u32 = 6; + pub const DTPMOD: u32 = 16; + pub const DTPOFF: u32 = 17; + pub const TPOFF: u32 = 18; +} +#[cfg(target_arch = "aarch64")] +mod machine { + pub const EM: u16 = 183; + pub const RELATIVE: u32 = 1027; + pub const GLOB_DAT: u32 = 1025; + pub const DTPMOD: u32 = 1028; + pub const DTPOFF: u32 = 1029; + pub const TPOFF: u32 = 1030; +} +use machine::*; + +const GLOBAL_FUNC: u8 = (1 << 4) | 2; +const GLOBAL_TLS: u8 = (1 << 4) | 6; + +/// Name, `st_info`, defined, value: 1 an import, 2 this object's +/// thread-local, 3 a function it defines, 4 another's thread-local. +const SYMBOLS: &[(&str, u8, bool, u64)] = &[ + ("", 0, false, 0), + ("imported", GLOBAL_FUNC, false, 0), + ("counter", GLOBAL_TLS, true, 8), + ("exported", GLOBAL_FUNC, true, 0x500), + ("other_tls", GLOBAL_TLS, false, 0), +]; + +/// One relocation: where, its type, its symbol and its addend. +type Entry = (u64, u32, u32, i64); + +/// A shared object, its file bytes its image: text and tables in a read-only +/// page, a writable page whose start the relocations write and whose +/// `PT_TLS` is 0x20 bytes at 0x1080, and `relocs` in a `DT_RELA` at +/// `rela_at`. `edit` changes the bytes last. +fn object(relocs: &[Entry], rela_at: usize, edit: impl FnOnce(&mut [u8])) -> Vec { + let mut bytes = vec![0u8; SIZE]; + let mut put = |at: usize, value: &[u8]| bytes[at..at + value.len()].copy_from_slice(value); + put(0, &[0x7f, b'E', b'L', b'F', 2, 1, 1]); + put(16, &3u16.to_le_bytes()); + put(18, &EM.to_le_bytes()); + put(20, &1u32.to_le_bytes()); + put(24, &0x500u64.to_le_bytes()); + put(32, &(PHDRS as u64).to_le_bytes()); + put(52, &64u16.to_le_bytes()); + put(54, &56u16.to_le_bytes()); + put(56, &4u16.to_le_bytes()); + // Type, flags, offset and vaddr, file size, memory size, alignment. + let phdrs: [(u32, u32, u64, u64, u64, u64); 4] = [ + (1, 5, 0, 0x1000, 0x1000, 0x1000), + (1, 6, DATA, 0x100, 0x1000, 0x1000), + (2, 4, DYNAMIC as u64, 0x100, 0x100, 8), + (7, 4, TLS, 0x10, 0x20, 8), + ]; + for (i, (kind, flags, at, filesz, memsz, align)) in phdrs.into_iter().enumerate() { + let p = PHDRS + i * 56; + put(p, &kind.to_le_bytes()); + put(p + 4, &flags.to_le_bytes()); + for (field, value) in [(8, at), (16, at), (24, at), (32, filesz), (40, memsz), (48, align)] { + put(p + field, &value.to_le_bytes()); + } + } + let mut names = vec![0u8]; + for (i, (name, info, defined, value)) in SYMBOLS.iter().enumerate() { + let at = SYMTAB + i * 24; + if !name.is_empty() { + put(at, &(names.len() as u32).to_le_bytes()); + names.extend_from_slice(name.as_bytes()); + names.push(0); + } + put(at + 4, &[*info]); + put(at + 6, &u16::from(*defined).to_le_bytes()); + put(at + 8, &value.to_le_bytes()); + } + put(STRTAB, &names); + for (i, &(offset, kind, sym, addend)) in relocs.iter().enumerate() { + let at = rela_at + i * 24; + put(at, &offset.to_le_bytes()); + put(at + 8, &(u64::from(sym) << 32 | u64::from(kind)).to_le_bytes()); + put(at + 16, &addend.to_le_bytes()); + } + let tags = [ + (6u64, SYMTAB as u64), + (5, STRTAB as u64), + (10, names.len() as u64), + (7, rela_at as u64), + (8, (relocs.len() * 24) as u64), + (0, 0), + ]; + for (i, (tag, value)) in tags.into_iter().enumerate() { + put(DYNAMIC + i * 16, &tag.to_le_bytes()); + put(DYNAMIC + i * 16 + 8, &value.to_le_bytes()); + } + edit(&mut bytes); + bytes +} + +/// Defines `imported` and module 7's `other_tls` at 0x10 in its block. +struct Loaded; + +impl Scope for Loaded { + fn address(&self, name: &str) -> Option { + (name == "imported").then_some(0xdead_0000) + } + + fn tls(&self, name: &str, addend: i64) -> Option> { + (name == "other_tls").then(|| Ok((7, 0x10u64.checked_add_signed(addend).unwrap()))) + } +} + +/// Defines nothing. +struct Nothing; + +impl Scope for Nothing { + fn address(&self, _: &str) -> Option { + None + } + + fn tls(&self, _: &str, _: i64) -> Option> { + None + } +} + +/// `file` laid out and relocated as module 3 against `scope`: the image's +/// words from `DATA` and how many symbols nothing defined, and the image's base. +fn load(file: &[u8], scope: &dyn Scope, pass: Pass) -> Result<(Vec, usize, u64), &'static str> { + let plan = dlimage::plan(&file[..0x1000])?; + // Whole words, so the image is 8-aligned as a mapping is page-aligned. + let mut words = vec![0u64; plan.span / 8]; + // SAFETY: the buffer's own bytes. + let image = unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), plan.span) }; + plan.lay(file, image)?; + let base = image.as_ptr() as u64; + // SAFETY: the buffer, laid out whole and writable, which nothing else reaches. + let mapped = unsafe { Mapped::new(image.as_mut_ptr(), &plan) }; + let tables = mapped.tables(true)?.expect("a PT_DYNAMIC"); + let unresolved = dlimage::relocate(&mapped, &tables, 3, scope, pass)?; + Ok((words[DATA as usize / 8..DATA as usize / 8 + 8].to_vec(), unresolved, base)) +} + +#[test] +fn each_relocation_is_worth_what_its_psabi_says() { + let relocs = [ + (DATA, RELATIVE, 0, 0x500), + (DATA + 8, GLOB_DAT, 1, 0), + (DATA + 16, GLOB_DAT, 3, 0), + (DATA + 24, DTPMOD, 0, 0), + (DATA + 32, DTPOFF, 2, 4), + (DATA + 40, DTPMOD, 4, 0), + (DATA + 48, DTPOFF, 4, 2), + (DATA + 56, DTPOFF, 0, 0x18), + ]; + let file = object(&relocs, RELA, |_| ()); + let (words, unresolved, base) = load(&file, &Loaded, Pass::Library).unwrap(); + assert_eq!(words, [base + 0x500, 0xdead_0000, base + 0x500, 3, 8 + 4, 7, 0x12, 0x18]); + assert_eq!(unresolved, 0); + + // With nothing loaded before it: an import is zero, another module's + // thread-local names this one and offset zero, each counted. + let (words, unresolved, _) = load(&file, &Nothing, Pass::Library).unwrap(); + assert_eq!((words[1], words[5], words[6]), (0, 3, 0)); + assert_eq!(unresolved, 3); +} + +#[test] +fn an_executables_pass_leaves_the_relatives_it_applied_itself() { + let file = object(&[(DATA, RELATIVE, 0, 0x500), (DATA + 8, GLOB_DAT, 1, 0)], RELA, |_| ()); + let (words, _, _) = load(&file, &Loaded, Pass::Executable).unwrap(); + assert_eq!(&words[..2], [0, 0xdead_0000]); +} + +#[test] +fn what_it_cannot_honour_is_refused_before_any_write() { + // Initial-exec TLS outside the executable's own block. + let file = object(&[(DATA, RELATIVE, 0, 0x500), (DATA + 8, TPOFF, 2, 0)], RELA, |_| ()); + assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err("ELF: initial-exec TLS outside the executable's own block")); + // A write outside the writable segment. + let file = object(&[(0x100, RELATIVE, 0, 0)], RELA, |_| ()); + assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err(RelocError::OutsideWindow.as_str())); + // An offset past this object's own thread-local storage. + let file = object(&[(DATA, DTPOFF, 2, 0x20)], RELA, |_| ()); + assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err(RelocError::TlsOutsideSegment.as_str())); +} + +#[test] +fn a_relocation_table_a_relocation_could_write_is_refused() { + let file = object(&[(DATA + 8, RELATIVE, 0, 0)], DATA as usize + 0x40, |_| ()); + assert!(load(&file, &Loaded, Pass::Library).is_err_and(|why| why.contains(".rela.dyn"))); +} + +#[test] +fn an_image_off_zero_or_with_segments_sharing_a_page_is_refused() { + // The read-only segment's vaddr moved to 0x400. + let shifted = object(&[], RELA, |b| b[PHDRS + 16..PHDRS + 24].copy_from_slice(&0x400u64.to_le_bytes())); + assert!(dlimage::plan(&shifted[..0x1000]).is_err()); + // The writable segment moved into the read-only one's last page. + let shared = object(&[], RELA, |b| b[PHDRS + 56 + 16..PHDRS + 56 + 24].copy_from_slice(&0xf00u64.to_le_bytes())); + assert_eq!(dlimage::plan(&shared[..0x1000]).map(|_| ()), Err("ELF: two PT_LOAD segments share a page")); +} diff --git a/sdk/libc/arch/src/lib.rs b/sdk/libc/arch/src/lib.rs index 9450715b9ee..99197bbde0b 100644 --- a/sdk/libc/arch/src/lib.rs +++ b/sdk/libc/arch/src/lib.rs @@ -47,6 +47,11 @@ mod errcode; #[path = "../../src/dgramreq.rs"] mod dgramreq; #[cfg(test)] +#[path = "../../src/dlimage.rs"] +// The loader's effects (`dl.rs`) use what the tests here do not. +#[allow(dead_code)] +mod dlimage; +#[cfg(test)] #[path = "../../src/elfsym.rs"] mod elfsym; #[cfg(test)] @@ -113,6 +118,8 @@ mod descriptor_requests; #[cfg(test)] mod dladdr_symbols; #[cfg(test)] +mod dlimage_relocations; +#[cfg(test)] mod errno_codes; #[cfg(test)] mod exact_hex; diff --git a/sdk/libc/arch/src/listing_reader.rs b/sdk/libc/arch/src/listing_reader.rs index 2554abababf..354d22bd582 100644 --- a/sdk/libc/arch/src/listing_reader.rs +++ b/sdk/libc/arch/src/listing_reader.rs @@ -1,40 +1,7 @@ -//! What `opendir`, `readdir` and `dladdr` make of the kernel's answers: a -//! whole answer however often it grows between two asks, and a name `d_name` -//! holds or `readdir` refuses. +//! What `readdir` makes of a name: one `d_name` holds, or one it refuses. use crate::listing; -/// A call answering `sizes` in turn, each the length its answer then needs, -/// writing it only into a buffer that holds it. -fn growing(sizes: &[usize]) -> impl FnMut(&mut [u8]) -> Result + '_ { - let mut asked = 0; - move |buf| { - let need = sizes[asked.min(sizes.len() - 1)]; - asked += 1; - if need <= buf.len() { - buf[..need].iter_mut().enumerate().for_each(|(i, b)| *b = i as u8); - } - Ok(need) - } -} - -#[test] -fn an_answer_is_asked_again_until_it_fits() { - // The first ask is of an empty buffer; each growth is met at the next. - for (sizes, want) in [ - (&[0][..], 0), - (&[5], 5), - (&[5, 9], 9), - (&[5, 9, 4000, 70_000], 70_000), - (&[70_000, 3], 3), - ] { - let whole = listing::whole(growing(sizes)).unwrap(); - assert_eq!(whole.len(), want, "{sizes:?}"); - assert!(whole.iter().enumerate().all(|(i, &b)| b == i as u8), "{sizes:?}"); - } - assert_eq!(listing::whole(|_| Err::(7)), Err(7)); -} - #[test] fn a_name_fits_d_name_with_its_nul_or_is_refused() { for len in [0, 1, 254, 255] { diff --git a/sdk/libc/arch/src/memory_refusals.rs b/sdk/libc/arch/src/memory_refusals.rs index 5016fe95a73..d69741ecd50 100644 --- a/sdk/libc/arch/src/memory_refusals.rs +++ b/sdk/libc/arch/src/memory_refusals.rs @@ -1,5 +1,5 @@ //! What libc's memory calls refuse before the kernel or the allocator is -//! asked: `mmap` a file, executable memory, no bytes, a fixed place off a +//! asked: `mmap` a file, memory writable and executable at once, no bytes, a fixed place off a //! page, or no one disposition; `posix_madvise` anything but its five; `madvise` the same, an //! address off a page, and `MADV_DONTNEED`'s discard. @@ -24,7 +24,9 @@ fn mmap_refuses_what_the_kernel_cannot_map_and_nothing_else() { (0, 4096, PROT_READ, 0, Some(MapRefusal::Invalid)), (0, 4096, PROT_READ, MAP_PRIVATE, Some(MapRefusal::File)), (0, 4096, rw, MAP_SHARED, Some(MapRefusal::File)), - (0, 4096, PROT_READ | PROT_EXEC, anonymous, Some(MapRefusal::Exec)), + (0, 4096, PROT_READ | PROT_EXEC, anonymous, None), + (0, 4096, PROT_WRITE | PROT_EXEC, anonymous, Some(MapRefusal::WriteExec)), + (0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, anonymous, Some(MapRefusal::WriteExec)), ] { assert_eq!(memreq::mmap_refusal(addr, len, prot, flags), want, "mmap({addr:#x}, {len}, {prot:#x}, {flags:#x})"); } @@ -80,7 +82,9 @@ fn a_protection_word_names_one_access_or_is_refused() { (PROT_READ, Some(Access::Read)), (PROT_WRITE, Some(Access::ReadWrite)), (PROT_READ | PROT_WRITE, Some(Access::ReadWrite)), - (PROT_READ | PROT_EXEC, Some(Access::Exec)), + (PROT_READ | PROT_EXEC, Some(Access::ReadExec)), + (PROT_EXEC, Some(Access::ReadExec)), + (PROT_WRITE | PROT_EXEC, Some(Access::WriteExec)), (0x8, None), ] { assert_eq!(memreq::access(prot), want, "prot {prot:#x}"); diff --git a/sdk/libc/arch/src/prototypes.rs b/sdk/libc/arch/src/prototypes.rs index 3dfcec7988a..e8369f17ce8 100644 --- a/sdk/libc/arch/src/prototypes.rs +++ b/sdk/libc/arch/src/prototypes.rs @@ -21,6 +21,7 @@ const UNDECLARED: &[(&str, &str)] = &[ ("_Unwind_Resume", "what the precompiled `alloc`'s landing pads name"), ("__cxa_atexit", "the C++ ABI's, which `cxxabi.h` declares"), ("__cxa_thread_atexit_impl", "what libc++abi's `__cxa_thread_atexit` calls, which it declares"), + ("__toyos_load_needed", "what a program's start calls to load the libraries it names"), ]; fn libc() -> PathBuf { diff --git a/sdk/libc/src/dl.rs b/sdk/libc/src/dl.rs new file mode 100644 index 00000000000..09703d57f72 --- /dev/null +++ b/sdk/libc/src/dl.rs @@ -0,0 +1,351 @@ +//! The dynamic loader: `dlopen` and its kin, and the libraries an executable +//! names (`DT_NEEDED`). A library is read wherever its path is served +//! (`toyos::files::read_image`), laid out in memory of this process's own, +//! relocated ([`dlimage`]), given its protections and published in the +//! process's one module list (`toyos::tls`), which every copy of std and libc +//! in the process reads. Its constructors run once it is published, with the +//! list let go, so one may load another. +//! +//! A library is known by the absolute path it was loaded from: a second load +//! of that name answers the module the first published, and nothing leaves +//! the list, so `dlclose` releases nothing. + +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::Cell; +use core::ptr; + +use toyos::process::ImageRefused; +use toyos::tls::{self, Loading, Module}; +use toyos_abi::syscall::{self, SyscallError, VmAt, VmProt}; + +use crate::dlimage::{self, Mapped, Pass, Plan, Scope, Tables, PAGE}; + +/// Where a library a `DT_NEEDED` names by file name alone is looked for. +const LIBRARY_DIR: &str = "/system/lib/"; + +/// The bytes of an image's header a plan is read from: the page its first +/// `PT_LOAD` maps. +const HEADER: usize = PAGE as usize; + +/// Why a library was not loaded. +enum Refused { + Image(ImageRefused), + Memory(SyscallError), + Elf(&'static str), +} + +impl From<&'static str> for Refused { + fn from(why: &'static str) -> Self { + Refused::Elf(why) + } +} + +impl Refused { + /// What `dlerror` says of it. + fn text(&self) -> &'static str { + match self { + Refused::Image(_) => "dlopen: the library could not be read", + Refused::Memory(_) => "dlopen: no memory for the library", + Refused::Elf(why) => why, + } + } + + fn errno(self) -> i32 { + match self { + Refused::Image(refused) => crate::errcode::of_image(refused), + Refused::Memory(e) => crate::errcode::of(e), + Refused::Elf(_) => crate::errcode::ENOEXEC, + } + } +} + +/// A published module and its tables, which a new image's symbols bind to. +struct Known { + module: &'static Module, + tables: Tables<'static>, +} + +/// Every module published, executable first: what a load binds against. +struct Published(Vec); + +impl Published { + /// What the list `_loading` holds: a module whose tables cannot be read + /// binds nothing. + fn of(_loading: &Loading) -> Self { + Published(tls::modules().filter_map(known).collect()) + } +} + +/// `module`'s tables, `None` for one with none to read. +fn known(module: &'static Module) -> Option { + let base = module.image.base; + // SAFETY: every published module's first page is its header, mapped for + // the process's life. + let header = unsafe { core::slice::from_raw_parts(base as *const u8, HEADER) }; + let plan = dlimage::plan(header).ok()?; + // SAFETY: a published module stays mapped as its loader laid it out, and + // nothing writes its tables. + let mapped = unsafe { Mapped::new(base as *mut u8, &plan) }; + let tables = mapped.tables(false).ok()??; + Some(Known { module, tables }) +} + +impl Scope for Published { + fn address(&self, name: &str) -> Option { + self.0.iter().find_map(|k| k.tables.address(name, k.module.image.base as u64)) + } + + fn tls(&self, name: &str, addend: i64) -> Option> { + self.0.iter().filter(|k| k.module.tls.memsz != 0).find_map(|k| Some(k.tables.tls(name, addend)?.map(|at| (k.module.id(), at)))) + } +} + +/// Load the library named `name` and every one it names, depth first, into +/// the list `loading` holds: the module, and each newly loaded one's +/// constructors in `inits`, a library's after those of the ones it names. +fn open(loading: &mut Loading, published: &mut Published, name: &str, inits: &mut Vec<(usize, usize)>) -> Result<&'static Module, Refused> { + if let Some(module) = loading.find(name.as_bytes()) { + return Ok(module); + } + let file = toyos::files::read_image(name.as_bytes()).map_err(Refused::Image)?; + let bytes = file.bytes(); + let plan = dlimage::plan(&bytes[..bytes.len().min(HEADER)])?; + // Committed whole while it is laid out and relocated; each page is given + // its own protection once nothing more is written. + // SAFETY: placed anywhere, so it replaces nothing. + let base = unsafe { syscall::vm_reserve(plan.span, VmProt::ReadWrite, VmAt::ANYWHERE) }.map_err(Refused::Memory)?; + let reservation = Reservation { at: base, len: plan.span }; + // SAFETY: the fresh, zeroed reservation, this function's alone. + plan.lay(bytes, unsafe { core::slice::from_raw_parts_mut(base, plan.span) })?; + // SAFETY: laid out whole and writable, and reached by nothing else until published. + let mapped = unsafe { Mapped::new(base, &plan) }; + let tables = mapped.tables(true)?.ok_or("ELF: a shared object with no PT_DYNAMIC")?; + for needed in tables.needed() { + let path = if needed.contains('/') { String::from(needed) } else { [LIBRARY_DIR, needed].concat() }; + open(loading, published, &path, inits)?; + } + let id = loading.next_id(); + dlimage::relocate(&mapped, &tables, id, published, Pass::Library)?; + protect(&plan, base)?; + let start = base as usize; + let tls = plan.tls().map_or(tls::Tls::default(), |segment| tls::Tls { + template: start + segment.template().start().get() as usize, + filesz: segment.template().len() as usize, + memsz: segment.memsz() as usize, + align: segment.align() as usize, + }); + let (phdr, phnum) = plan.phdrs(); + let image = tls::Image { + base: start, + start, + end: start + plan.span, + phdr: start + phdr as usize, + phnum, + eh_frame_hdr: plan.eh_frame_hdr().map_or(0, |at| start + at as usize), + }; + let name = alloc::ffi::CString::new(name).map_err(|_| "dlopen: a library path holds a NUL")?; + let name: &'static core::ffi::CStr = alloc::boxed::Box::leak(name.into_boxed_c_str()); + core::mem::forget(reservation); + let module = loading.add(Module::new(name, image, tls)); + if let Some((at, count)) = tables.init_array { + inits.push((start + at as usize, count)); + } + published.0.push(Known { module, tables }); + Ok(module) +} + +/// A reservation released unless it is published. +struct Reservation { + at: *mut u8, + len: usize, +} + +impl Drop for Reservation { + fn drop(&mut self) { + // SAFETY: a reservation nothing was published over. + let _ = unsafe { syscall::vm_release(self.at, self.len) }; + } +} + +/// Give each of `plan`'s segments at `base` its pages' protection, the pages +/// between them none, and `PT_GNU_RELRO`'s pages read-only: nothing writes +/// what its relocations wrote, and no page is both writable and executable. +fn protect(plan: &Plan, base: *mut u8) -> Result<(), Refused> { + let mut segments: Vec<_> = plan.segments().collect(); + segments.sort_unstable_by_key(|&(lo, _, _)| lo); + let mut covered = 0; + let give = |lo: u64, hi: u64, prot: VmProt| { + if lo >= hi { + return Ok(()); + } + // SAFETY: whole pages of the reservation `base` begins. + unsafe { syscall::vm_protect(base.add(lo as usize), (hi - lo) as usize, prot) }.map_err(Refused::Memory) + }; + for (lo, hi, flags) in segments { + give(covered, lo, VmProt::None)?; + let prot = if flags.executable() { + VmProt::ReadExec + } else if flags.writable() { + VmProt::ReadWrite + } else { + VmProt::Read + }; + give(lo, hi, prot)?; + covered = hi; + } + give(covered, plan.span as u64, VmProt::None)?; + if let Some((lo, hi)) = plan.relro() { + give(lo, hi, VmProt::Read)?; + } + Ok(()) +} + +/// Call each `(address, count)` `.init_array`'s constructors in order. +/// +/// # Safety +/// Each names a published module's `.init_array`. +unsafe fn construct(inits: &[(usize, usize)]) { + for &(at, count) in inits { + // SAFETY: the caller's; the array is `count` pointers inside the image. + let entries = unsafe { core::slice::from_raw_parts(at as *const usize, count) }; + for &entry in entries { + // The psABI's `0` and `-1` are no constructor. + if entry != 0 && entry != usize::MAX { + // SAFETY: a relocated `.init_array` entry is a constructor. + let f: extern "C" fn() = unsafe { core::mem::transmute(entry) }; + f(); + } + } + } +} + +/// Load the libraries the executable at `header` names and bind its symbols +/// to them, then run their constructors: std's and libc's start call this +/// before anything that could name such a symbol. An executable that names +/// none costs a look at its dynamic section; one whose libraries cannot be +/// had exits 127 with a line on its stderr, as `toyos::relocate` does. +/// +/// # Safety +/// `header` is the running executable's ELF header, and its thread-local +/// storage is laid out (`toyos::tls::start`). +#[no_mangle] +pub unsafe extern "C" fn __toyos_load_needed(header: *const u8) { + // SAFETY: the caller's; the page the header begins is mapped. + let page = unsafe { core::slice::from_raw_parts(header, HEADER) }; + let Ok(plan) = dlimage::plan(page) else { return }; + // SAFETY: the executable as the kernel mapped it, relocated by itself, + // whose writable window only this writes until it returns. + let mapped = unsafe { Mapped::new(header.cast_mut(), &plan) }; + let Ok(Some(tables)) = mapped.tables(false) else { return }; + if tables.needed().next().is_none() { + return; + } + let mut inits = Vec::new(); + let bound = (|| -> Result<(), Refused> { + let mut loading = tls::loading(); + let mut published = Published::of(&loading); + let tables = mapped.tables(true)?.ok_or("ELF: no PT_DYNAMIC")?; + for needed in tables.needed() { + let path = if needed.contains('/') { String::from(needed) } else { [LIBRARY_DIR, needed].concat() }; + open(&mut loading, &mut published, &path, &mut inits)?; + } + dlimage::relocate(&mapped, &tables, 1, &published, Pass::Executable)?; + Ok(()) + })(); + if bound.is_err() { + let line = b"this program's libraries could not be loaded: it cannot start\n"; + // SAFETY: a buffer of `line.len()` bytes. + let _ = unsafe { crate::posix_io::write(2, line.as_ptr(), line.len()) }; + syscall::exit(127); + } + // SAFETY: each a module just published. + unsafe { construct(&inits) }; +} + +/// A C handle is a module's id: never null, which is `dlopen`'s failure. +fn module_of(handle: *mut u8) -> Option<&'static Module> { + tls::modules().find(|m| m.id() == handle as usize) +} + +#[no_mangle] +pub unsafe extern "C" fn dlopen(path: *const u8, _flags: i32) -> *mut u8 { + if path.is_null() { + return ptr::null_mut(); + } + let Ok(path) = core::str::from_utf8(crate::posix_io::c_str_to_bytes(path)) else { + crate::errno::set(crate::errcode::ENOENT); + return ptr::null_mut(); + }; + let mut cwd = alloc::vec![0u8; syscall::MAX_PATH]; + let n = toyos::files::getcwd(&mut cwd); + let name = toyos::files::absolute(&String::from_utf8_lossy(&cwd[..n.min(cwd.len())]), path); + let mut inits = Vec::new(); + let opened = { + let mut loading = tls::loading(); + let mut published = Published::of(&loading); + open(&mut loading, &mut published, &name, &mut inits) + }; + match opened { + Ok(module) => { + // SAFETY: each a module just published. + unsafe { construct(&inits) }; + module.id() as *mut u8 + } + Err(refused) => { + ERROR.set(Some(refused.text())); + crate::errno::set(refused.errno()); + ptr::null_mut() + } + } +} + +/// The symbol `symbol` names: in the module `handle` names, any symbol it +/// defines; with `RTLD_DEFAULT` (null), the first module's that exports it. +#[no_mangle] +pub unsafe extern "C" fn dlsym(handle: *mut u8, symbol: *const u8) -> *mut u8 { + if symbol.is_null() { + return ptr::null_mut(); + } + let Ok(name) = core::str::from_utf8(crate::posix_io::c_str_to_bytes(symbol)) else { + return ptr::null_mut(); + }; + let found = if handle.is_null() { + tls::modules().filter_map(known).find_map(|k| k.tables.address(name, k.module.image.base as u64)) + } else { + module_of(handle).and_then(known).and_then(|k| k.tables.defined(name, k.module.image.base as u64)) + }; + found.map_or_else( + || { + ERROR.set(Some("dlsym: no such symbol")); + ptr::null_mut() + }, + |at| at as *mut u8, + ) +} + +/// Nothing leaves the module list: `0` for a handle `dlopen` answered. +#[no_mangle] +pub unsafe extern "C" fn dlclose(handle: *mut u8) -> i32 { + if module_of(handle).is_some() { 0 } else { -1 } +} + +/// The calling thread's last refusal by `dlopen` or `dlsym` that `dlerror` has not said yet. +#[thread_local] +static ERROR: Cell> = Cell::new(None); + +/// Where `dlerror` writes it, NUL-terminated: valid until the thread's next `dlerror`. +#[thread_local] +static mut TEXT: [u8; 128] = [0; 128]; + +/// The last refusal not said yet, cut to what [`TEXT`] holds; null for none. +#[no_mangle] +pub unsafe extern "C" fn dlerror() -> *const u8 { + let Some(text) = ERROR.take() else { return ptr::null() }; + // SAFETY: the calling thread's own buffer. + let buf = unsafe { &mut *(&raw mut TEXT) }; + let len = text.len().min(buf.len() - 1); + buf[..len].copy_from_slice(&text.as_bytes()[..len]); + buf[len] = 0; + buf.as_ptr() +} diff --git a/sdk/libc/src/dlimage.rs b/sdk/libc/src/dlimage.rs new file mode 100644 index 00000000000..8586fd1c27f --- /dev/null +++ b/sdk/libc/src/dlimage.rs @@ -0,0 +1,350 @@ +//! A shared object's image, as the loader lays it out and relocates it: every +//! table it reads, through `toyos-elf`, out of the image's own mapped bytes. +//! It touches nothing but the memory it is handed, so the host tests it on +//! images it lays out (`toyos-libc-copies`). +//! +//! An image is the process's own input: a crafted one corrupts only the +//! process that loads it, and is refused by name wherever `toyos-elf` or the +//! rules here can tell. + +use alloc::vec::Vec; +use core::marker::PhantomData; + +use toyos_elf::dynamic::ENTRY_SIZE as DYN_SIZE; +use toyos_elf::{ + rela, Dynamic, GnuHash, InitArray, Layout, Machine, Op, RelaTable, RelocError, SegmentFlags, Sym, SymTab, TlsRef, + TlsSegment, +}; + +/// The page an image's protections are given in. +pub(crate) const PAGE: u64 = 4096; + +/// The machine this library runs on, whose images it loads. +#[cfg(target_arch = "x86_64")] +pub(crate) const MACHINE: Machine = Machine::X86_64; +#[cfg(target_arch = "aarch64")] +pub(crate) const MACHINE: Machine = Machine::Aarch64; + +const DT_NEEDED: u64 = 1; + +/// What an image's header says it is, before anything of it is laid out. +pub(crate) struct Plan { + layout: Layout, + /// Bytes from its first page to past its last. + pub(crate) span: usize, +} + +/// The image `header` begins, `header` reaching at least past its program +/// header table: one whose addresses start at zero, whose program header table +/// a `PT_LOAD` maps and whose segments share no page. +pub(crate) fn plan(header: &[u8]) -> Result { + let layout = Layout::parse(header, MACHINE).map_err(|e| e.as_str())?; + // Every bound below is image-relative and every `r_offset` a vaddr, so a + // non-zero `vaddr_min` would shift the two against each other. + if layout.extent().min() != 0 { + return Err("ELF: a shared object must begin at vaddr 0"); + } + if layout.program_headers().is_none() { + return Err("ELF: no PT_LOAD maps the program header table"); + } + if layout.overlapping_load_pages(PAGE).is_some() { + return Err("ELF: two PT_LOAD segments share a page"); + } + let span = layout.span().checked_next_multiple_of(PAGE).and_then(|s| usize::try_from(s).ok()); + let span = span.ok_or("ELF: an image span no address space holds")?; + Ok(Plan { layout, span }) +} + +impl Plan { + /// Copy every `PT_LOAD`'s file bytes from `file` into `image`, its first + /// `span` bytes zeroed and writable. + pub(crate) fn lay(&self, file: &[u8], image: &mut [u8]) -> Result<(), &'static str> { + for seg in self.layout.segments() { + let at = seg.image().start().get() as usize; + let len = seg.filesz() as usize; + let from = usize::try_from(seg.file_offset()).ok().filter(|&from| from.checked_add(len).is_some_and(|end| end <= file.len())); + let from = from.ok_or("ELF: a PT_LOAD's file bytes lie past the file's end")?; + // `Layout` holds `filesz <= memsz` and every segment inside the span. + image[at..at + len].copy_from_slice(&file[from..from + len]); + } + Ok(()) + } + + /// Each segment's whole pages, `[lo, hi)` in image offsets, and what they + /// may be used for: none shares a page with another (`plan`). + pub(crate) fn segments(&self) -> impl Iterator + '_ { + self.layout.segments().iter().map(|seg| { + let (lo, hi) = seg.page_range(PAGE); + (lo, hi, seg.flags()) + }) + } + + /// `PT_GNU_RELRO`'s whole pages, `[lo, hi)` in image offsets. + pub(crate) fn relro(&self) -> Option<(u64, u64)> { + self.layout.relro_pages(PAGE) + } + + pub(crate) fn tls(&self) -> Option { + self.layout.tls().and_then(TlsSegment::occupied) + } + + /// `PT_GNU_EH_FRAME`'s image offset. + pub(crate) fn eh_frame_hdr(&self) -> Option { + self.layout.eh_frame_hdr().map(|r| r.start().get()) + } + + /// The program header table's image offset and entries. + pub(crate) fn phdrs(&self) -> (u64, usize) { + let table = self.layout.program_headers().expect("plan refused an image without one"); + (table.image().start().get(), usize::from(table.count())) + } +} + +/// An image laid out at `base` as `plan` describes it, its memory living for `'m`. +pub(crate) struct Mapped<'p, 'm> { + base: *mut u8, + plan: &'p Plan, + memory: PhantomData<&'m [u8]>, +} + +impl<'p, 'm> Mapped<'p, 'm> { + /// # Safety + /// Every `PT_LOAD` of `plan` is mapped readable at `base` plus its vaddr + /// for `'m`, and nothing but [`relocate`] writes it meanwhile, into its + /// writable window, which is writable. + pub(crate) unsafe fn new(base: *mut u8, plan: &'p Plan) -> Self { + Mapped { base, plan, memory: PhantomData } + } + + pub(crate) fn base(&self) -> u64 { + self.base as u64 + } + + /// `len` bytes from `vaddr`, all inside one `PT_LOAD`. + fn view(&self, vaddr: u64, len: u64) -> Option<&'m [u8]> { + let end = vaddr.checked_add(len)?; + let inside = self.plan.layout.segments().iter().any(|seg| { + let image = seg.image(); + image.start().get() <= vaddr && end <= image.end().get() + }); + // SAFETY: inside a mapped `PT_LOAD` (`new`'s contract). + inside.then(|| unsafe { core::slice::from_raw_parts(self.base.add(vaddr as usize), len as usize) }) + } + + /// From `vaddr` to the end of the `PT_LOAD` holding it. + fn view_to_end(&self, vaddr: u64) -> Option<&'m [u8]> { + let seg = self.plan.layout.segments().iter().find(|seg| seg.image().start().get() <= vaddr && vaddr < seg.image().end().get())?; + self.view(vaddr, seg.image().end().get() - vaddr) + } + + /// The image's dynamic tables, each inside a `PT_LOAD`; `None` for an + /// image with no `PT_DYNAMIC`. `check`ed, every symbol it defines lies in + /// it and no table on a page a relocation writes, as [`relocate`] needs: a + /// module already published was checked as it was loaded. + pub(crate) fn tables(&self, check: bool) -> Result>, &'static str> { + const OUTSIDE: &str = "ELF: a dynamic table outside the image's segments"; + let Some(dynamic) = self.plan.layout.dynamic() else { return Ok(None) }; + let range = dynamic.image(); + let dynamic = self.view(range.start().get(), range.len()).ok_or(OUTSIDE)?; + let parsed = Dynamic::parse(dynamic); + let gnu_hash = parsed.gnu_hash.map(|at| self.view_to_end(at).ok_or(OUTSIDE)).transpose()?; + let count = parsed.sym_count(gnu_hash).unwrap_or_else(|gap| gap); + let syms = match parsed.symtab { + Some(at) => { + let whole = self.view_to_end(at).ok_or(OUTSIDE)?; + &whole[..whole.len().min(count.saturating_mul(toyos_elf::sym::ENTRY_SIZE))] + } + None => &[][..], + }; + let strs = match parsed.strtab { + Some(at) => self.view(at, parsed.strsz.unwrap_or(0)).ok_or(OUTSIDE)?, + None => &[][..], + }; + let table = |t: Option| t.map(|t| self.view(t.vaddr, t.size).ok_or(OUTSIDE)).transpose(); + let (rela, jmprel) = (table(parsed.rela)?, table(parsed.jmprel)?); + let symbols = SymTab::new(syms, strs); + let extent = self.plan.layout.extent(); + let window = self.plan.layout.writable_window().unwrap_or((extent.span(), extent.span())); + let span_of = |bytes: &[u8]| { + let at = bytes.as_ptr() as u64 - self.base(); + (at, at + bytes.len() as u64) + }; + let read = rela::ReadTables { + dynsym: span_of(syms), + dynstr: span_of(strs), + rela: rela.map_or((0, 0), span_of), + jmprel: jmprel.map_or((0, 0), span_of), + }; + if check { + symbols.bounded(extent, self.plan.layout.tls()).map_err(|e| e.as_str())?; + rela::tables_outside_window(&read, window, PAGE)?; + } + let init_array = InitArray::parse(parsed.init_array, extent).map_err(|e| e.as_str())?; + Ok(Some(Tables { + dynamic, + symbols, + gnu_hash: gnu_hash.and_then(GnuHash::parse), + rela, + jmprel, + rules: rela::Rules { extent, window, tls: self.plan.layout.tls() }, + init_array: init_array.map(|a| (a.range().start().get(), a.count() as usize)), + })) + } + + /// Write `value`'s bytes at image offset `at`, which a parse bounded + /// inside the writable window. + fn write(&self, at: u64, value: &[u8]) { + let (lo, hi) = self.plan.layout.writable_window().expect("dlimage: a write in an image with no writable segment"); + assert!(lo <= at && at + value.len() as u64 <= hi, "dlimage: a write at {at:#x} outside the window a parse bounded"); + // SAFETY: inside the writable window (`new`'s contract), which no + // table read here lies on (`tables`). + unsafe { core::ptr::copy_nonoverlapping(value.as_ptr(), self.base.add(at as usize), value.len()) }; + } +} + +/// An image's dynamic tables, each a view of its mapped bytes. +pub(crate) struct Tables<'a> { + dynamic: &'a [u8], + symbols: SymTab<'a>, + gnu_hash: Option>, + rela: Option<&'a [u8]>, + jmprel: Option<&'a [u8]>, + rules: rela::Rules, + /// `DT_INIT_ARRAY`'s image offset and entries. + pub(crate) init_array: Option<(u64, usize)>, +} + +impl<'a> Tables<'a> { + /// Every `DT_NEEDED` name, in the order the image lists them; a name + /// past the string table is `""`. + pub(crate) fn needed(&self) -> impl Iterator + 'a { + let strs = self.symbols.strings(); + self.dynamic + .as_chunks::() + .0 + .iter() + .map(|entry| (u64::from_le_bytes(entry[..8].try_into().unwrap()), u64::from_le_bytes(entry[8..].try_into().unwrap()))) + .take_while(|&(tag, _)| tag != 0) + .filter(|&(tag, _)| tag == DT_NEEDED) + .map(move |(_, at)| toyos_elf::cstr(strs, at)) + } + + /// The symbol this image exports as `name`, through its `.gnu.hash` + /// where it has one. + fn exported(&self, name: &str) -> Option { + let sym = match &self.gnu_hash { + Some(hash) => self.symbols.get(hash.lookup(name, &self.symbols)?)?, + None => self.symbols.find(name)?.1, + }; + sym.is_exported().then_some(sym) + } + + /// The address this image, mapped at `base`, gives the non-TLS symbol it + /// defines as `name`: any defined one, as `dlsym` reads it. + pub(crate) fn defined(&self, name: &str, base: u64) -> Option { + let (_, sym) = self.symbols.find(name)?; + Some(base + sym.address(self.rules.extent)?.get()) + } + + /// The address of the symbol this image, mapped at `base`, exports as `name`. + pub(crate) fn address(&self, name: &str, base: u64) -> Option { + Some(base + self.exported(name)?.address(self.rules.extent)?.get()) + } + + /// `S + A` of the TLS symbol this image exports as `name`, in its block. + pub(crate) fn tls(&self, name: &str, addend: i64) -> Option> { + let sym = self.exported(name).filter(|s| s.kind() == toyos_elf::sym::STT_TLS)?; + let segment = self.rules.tls?; + Some(sym.tls_offset(addend, segment).map(|at| at.get()).ok_or(RelocError::TlsOutsideSegment)) + } + + fn relocations(&self) -> impl Iterator + 'a { + let entries = |table: Option<&'a [u8]>| table.into_iter().flat_map(|t| RelaTable::new(t, MACHINE).iter()); + entries(self.rela).chain(entries(self.jmprel)) + } +} + +/// What the modules loaded before an image define, which its symbols are +/// bound to before its own definitions. +pub(crate) trait Scope { + /// The address a module gives the symbol `name`. + fn address(&self, name: &str) -> Option; + /// The id of the module whose thread-local `name` is, and `S + A` within + /// its block. + fn tls(&self, name: &str, addend: i64) -> Option>; +} + +/// Which of an image's relocations this pass writes: a library's, all; an +/// executable's, all but the `RELATIVE`s it applied to itself. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Pass { + Library, + Executable, +} + +/// Apply `tables`' relocations to `mapped`, module `id` of the process, +/// against `scope` and then the image's own definitions. Every value is +/// computed before the first write, so a refused image is left as laid. +/// Answers how many name a symbol nothing defines, each written as zero, or +/// for a `DTPMOD`, as the image's own id. +pub(crate) fn relocate(mapped: &Mapped<'_, '_>, tables: &Tables<'_>, id: usize, scope: &dyn Scope, pass: Pass) -> Result { + let base = mapped.base(); + let symbol = |i: toyos_elf::SymIndex| -> (Sym, &str) { + let sym = tables.symbols.get(i.get()).expect("dlimage: a parse bounded the index by the table"); + (sym, sym.name_in(tables.symbols.strings())) + }; + let own_tls = |at: toyos_elf::TlsOffset| at.get(); + let mut unresolved = 0; + let mut writes = Vec::new(); + for raw in tables.relocations() { + let Some(r) = rela::parse(raw, &tables.rules, tables.symbols).map_err(|e| e.as_str())? else { continue }; + let value: u64 = match r.op() { + Op::Relative(_) if pass == Pass::Executable => continue, + Op::Relative(target) => base + target.get(), + Op::Bind(i) => { + let (sym, name) = symbol(i); + let own = sym.address(tables.rules.extent).map(|at| base + at.get()); + match scope.address(name).or(own) { + Some(at) => at, + None => { + unresolved += 1; + 0 + } + } + } + Op::Tpoff64(_) | Op::Tpoff32(_) => return Err("ELF: initial-exec TLS outside the executable's own block"), + Op::DtpMod64(i) => match i.map(symbol) { + None => id as u64, + Some((sym, _)) if sym.is_defined() => id as u64, + Some((_, name)) => match scope.tls(name, 0) { + Some(found) => found.map_err(|e| e.as_str())?.0 as u64, + None => { + unresolved += 1; + id as u64 + } + }, + }, + Op::DtpOff64(TlsRef::Own(at)) => own_tls(at), + Op::DtpOff64(TlsRef::Symbol(s)) => { + let (sym, name) = symbol(s.sym()); + if sym.is_defined() { + let segment = tables.rules.tls.ok_or(RelocError::TlsOutsideSegment.as_str())?; + sym.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment.as_str())?.get() + } else { + match scope.tls(name, s.addend()) { + Some(found) => found.map_err(|e| e.as_str())?.1, + None => { + unresolved += 1; + 0 + } + } + } + } + }; + writes.push((r.offset(), value)); + } + for (at, value) in writes { + mapped.write(at, &value.to_le_bytes()); + } + Ok(unresolved) +} diff --git a/sdk/libc/src/lib.rs b/sdk/libc/src/lib.rs index 436e2e38e96..0d77036e407 100644 --- a/sdk/libc/src/lib.rs +++ b/sdk/libc/src/lib.rs @@ -14,6 +14,8 @@ mod atreq; mod conversion; mod ctype; mod dgramreq; +mod dl; +mod dlimage; mod elfsym; mod env; mod errcode; @@ -66,10 +68,21 @@ mod runtime { pub(crate) extern "C" fn start_c(argc: i32, argv: *const *const u8) -> ! { unsafe extern "C" { fn main(argc: i32, argv: *const *const u8) -> i32; + static __ehdr_start: u8; } + // The program goes by its `argv[0]`. + let name = match argc { + 0 => &[][..], + // SAFETY: the kernel's argument block, NUL-terminated strings. + _ => unsafe { crate::posix_io::c_str_to_bytes(*argv) }, + }; + // SAFETY: the executable's own header, and this is its main thread. + unsafe { toyos::tls::start(&raw const __ehdr_start, name) }; // Before the constructors and `main`, either of which may read // `environ` before any call that would make it. crate::env::init(); + // SAFETY: the executable's own header, its thread-local storage laid out. + unsafe { crate::dl::__toyos_load_needed(&raw const __ehdr_start) }; // SAFETY: the linker's bounds of the program's constructor array. unsafe { for hook in hooks(&raw const __init_array_start, &raw const __init_array_end) { diff --git a/sdk/libc/src/link.rs b/sdk/libc/src/link.rs index c417fc106d0..f8b5e0ca547 100644 --- a/sdk/libc/src/link.rs +++ b/sdk/libc/src/link.rs @@ -1,17 +1,12 @@ -//! `dl_iterate_phdr` and `dladdr`, from the kernel's `SYS_QUERY_MODULES` -//! answer. +//! `dl_iterate_phdr` and `dladdr`, from the process's module list +//! (`toyos::tls`). //! //! `dl_iterate_phdr`'s contract is glibc's `dl_iterate_phdr(3)`, which the //! LSB adopts: the executable first, named by the empty string, then every //! library in load order; the walk stops at the first callback that returns -//! non-zero and returns that value, else 0. The walk is over one answer, so a -//! module a callback or another thread loads meanwhile is not visited. - -use alloc::boxed::Box; -use alloc::vec::Vec; -use core::sync::atomic::{AtomicPtr, Ordering}; - -use toyos_abi::syscall; +//! non-zero and returns that value, else 0. A module a callback or another +//! thread loads meanwhile may or may not be visited. Neither allocates: each +//! module's name is the list's own. use crate::elfsym::Image; @@ -28,19 +23,13 @@ type Callback = unsafe extern "C" fn(*mut DlPhdrInfo, usize, *mut u8) -> i32; #[no_mangle] pub unsafe extern "C" fn dl_iterate_phdr(callback: Callback, data: *mut u8) -> i32 { - let answer = answer(); - let mut name = Vec::new(); - for (i, (module, path)) in syscall::modules(&answer).enumerate() { - name.clear(); - if i > 0 { - name.extend_from_slice(path); - } - name.push(0); + for (i, module) in toyos::tls::modules().enumerate() { + let name = if i == 0 { c"" } else { module.name() }; let mut info = DlPhdrInfo { - dlpi_addr: module.base, - dlpi_name: name.as_ptr(), - dlpi_phdr: module.phdr as *const u8, - dlpi_phnum: u16::try_from(module.phnum).expect("SYS_QUERY_MODULES: a phnum past e_phnum's u16"), + dlpi_addr: module.image.base as u64, + dlpi_name: name.as_ptr().cast(), + dlpi_phdr: module.image.phdr as *const u8, + dlpi_phnum: u16::try_from(module.image.phnum).expect("tls: a phnum past e_phnum's u16"), }; // SAFETY: the caller's function, handed a record valid for the call. let stop = unsafe { callback(&mut info, core::mem::size_of::(), data) }; @@ -65,21 +54,16 @@ pub struct DlInfo { /// exports none; 0 where no image holds `addr`. #[no_mangle] pub unsafe extern "C" fn dladdr(addr: *const u8, info: *mut DlInfo) -> i32 { - let answer = answer(); - for (module, path) in syscall::modules(&answer) { - let image = Image { - base: module.base, - phdr: module.phdr as *const u8, - phnum: usize::try_from(module.phnum).expect("SYS_QUERY_MODULES: a phnum past usize"), - }; - // SAFETY: the kernel's description of an image this process has loaded. + for module in toyos::tls::modules() { + let image = Image { base: module.image.base as u64, phdr: module.image.phdr as *const u8, phnum: module.image.phnum }; + // SAFETY: the module list's description of an image this process has loaded. if !unsafe { image.contains(addr.addr() as u64) } { continue; } let symbol = unsafe { image.symbol(addr.addr() as u64) }; unsafe { *info = DlInfo { - dli_fname: named(module.base, path), + dli_fname: module.name().as_ptr().cast(), dli_fbase: core::ptr::with_exposed_provenance_mut(image.start() as usize), dli_sname: symbol.map_or(core::ptr::null(), |(name, _)| name.as_ptr().cast()), dli_saddr: symbol.map_or(core::ptr::null_mut(), |(_, at)| core::ptr::with_exposed_provenance_mut(at as usize)), @@ -90,45 +74,3 @@ pub unsafe extern "C" fn dladdr(addr: *const u8, info: *mut DlInfo) -> i32 { 0 } -/// A path `dladdr` has answered with, NUL-terminated, beside the base of the -/// image it named: kept for the life of the process, since `dli_fname` must -/// outlive every call, and each image's added once. -struct Named { - base: u64, - path: Vec, - next: *mut Named, -} - -static NAMED: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); - -/// `path` of the image at `base`, as a C string that outlives the process's -/// every call. Two threads naming a new image at once may each add it. -fn named(base: u64, path: &[u8]) -> *const u8 { - let mut at = NAMED.load(Ordering::Acquire); - while !at.is_null() { - // SAFETY: a node once published is never freed or changed. - let node = unsafe { &*at }; - if node.base == base && node.path[..node.path.len() - 1] == *path { - return node.path.as_ptr(); - } - at = node.next; - } - let mut owned = Vec::with_capacity(path.len() + 1); - owned.extend_from_slice(path); - owned.push(0); - let node = Box::into_raw(Box::new(Named { base, path: owned, next: core::ptr::null_mut() })); - loop { - let head = NAMED.load(Ordering::Acquire); - // SAFETY: `node` is this thread's until the exchange publishes it. - unsafe { (*node).next = head }; - if NAMED.compare_exchange(head, node, Ordering::AcqRel, Ordering::Acquire).is_ok() { - // SAFETY: published, and never freed. - return unsafe { (*node).path.as_ptr() }; - } - } -} - -/// One whole `SYS_QUERY_MODULES` answer. -fn answer() -> Vec { - crate::listing::whole(syscall::query_modules).expect("SYS_QUERY_MODULES refused a buffer this process owns") -} diff --git a/sdk/libc/src/listing.rs b/sdk/libc/src/listing.rs index 40a4e476d20..f88283c452a 100644 --- a/sdk/libc/src/listing.rs +++ b/sdk/libc/src/listing.rs @@ -1,24 +1,5 @@ -//! What `opendir`, `readdir` and `dladdr` make of the kernel's answers before -//! a caller sees them. It reads nothing but what it is handed, so the host -//! tests it (`toyos-libc-copies`). - -use alloc::vec::Vec; - -/// The whole answer of a call that writes nothing into a buffer too short for -/// it and answers the length it needs: asked again at that length until one -/// fits, since a listing or a module table can grow between two asks. Each ask -/// is longer than the last, and the kernel bounds what it answers. -pub(crate) fn whole(mut ask: impl FnMut(&mut [u8]) -> Result) -> Result, E> { - let mut buf = Vec::new(); - loop { - let need = ask(&mut buf)?; - if need <= buf.len() { - buf.truncate(need); - return Ok(buf); - } - buf.resize(need, 0); - } -} +//! What `readdir` makes of a name before a caller sees it. It reads nothing +//! but what it is handed, so the host tests it (`toyos-libc-copies`). /// The bytes of `struct dirent`'s `d_name`: `{NAME_MAX}` and its NUL. pub(crate) const D_NAME: usize = 256; diff --git a/sdk/libc/src/memreq.rs b/sdk/libc/src/memreq.rs index 4a50f4b4f78..1b3f1123dbb 100644 --- a/sdk/libc/src/memreq.rs +++ b/sdk/libc/src/memreq.rs @@ -28,8 +28,8 @@ pub(crate) enum MapRefusal { Invalid, /// A file's bytes, which the kernel maps for no process: `ENODEV`. File, - /// Executable memory, which the kernel does not map: `ENOTSUP`. - Exec, + /// Memory writable and executable at once, which no page is: `ENOTSUP`. + WriteExec, } /// Why `mmap` of `len` bytes at `addr` with `prot` and `flags` is refused, if @@ -46,8 +46,8 @@ pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Op if flags & MAP_ANONYMOUS == 0 { return Some(MapRefusal::File); } - if prot & PROT_EXEC != 0 { - return Some(MapRefusal::Exec); + if prot & PROT_EXEC != 0 && prot & PROT_WRITE != 0 { + return Some(MapRefusal::WriteExec); } None } @@ -88,8 +88,11 @@ pub(crate) enum Access { Read, /// Writing, which on every machine ToyOS runs on brings reading with it. ReadWrite, - /// Executing, which no call maps yet: `ENOTSUP`. - Exec, + /// Executing, which brings reading with it. + ReadExec, + /// Writing and executing at once, which no page allows: `ENOTSUP`. A + /// program writes its code, then protects it executable. + WriteExec, } /// What `prot` asks; `None` (`EINVAL`) for a bit `PROT_*` does not define. @@ -97,8 +100,10 @@ pub(crate) fn access(prot: i32) -> Option { if prot & !(PROT_READ | PROT_WRITE | PROT_EXEC) != 0 { return None; } - Some(if prot & PROT_EXEC != 0 { - Access::Exec + Some(if prot & PROT_EXEC != 0 && prot & PROT_WRITE != 0 { + Access::WriteExec + } else if prot & PROT_EXEC != 0 { + Access::ReadExec } else if prot & PROT_WRITE != 0 { Access::ReadWrite } else if prot & PROT_READ != 0 { diff --git a/sdk/libc/src/misc.rs b/sdk/libc/src/misc.rs index d0cc75cb8cd..ef91427e182 100644 --- a/sdk/libc/src/misc.rs +++ b/sdk/libc/src/misc.rs @@ -471,56 +471,6 @@ pub unsafe extern "C" fn longjmp(_env: *mut u8, _val: i32) -> ! { panic!("longjmp not implemented") } -// dlopen/dlsym/dlclose -// -// A C handle is the kernel's module index plus one: index 0 is a module, and -// NULL is dlopen's failure. - -fn module_index(handle: *mut u8) -> Option { - (handle as u64).checked_sub(1) -} - -#[no_mangle] -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 toyos::files::dl_open(path_bytes) { - Ok(index) => (index + 1) as *mut u8, - 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() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dlsym(handle: *mut u8, symbol: *const u8) -> *mut u8 { - if symbol.is_null() { return ptr::null_mut(); } - let index = module_index(handle).expect("dlsym: RTLD_DEFAULT not implemented"); - let name = super::posix_io::c_str_to_bytes(symbol); - // SAFETY: handle is from a prior dlopen, name is a valid C string - match unsafe { syscall::dl_sym(index, name) } { - Ok(addr) => addr as *mut u8, - Err(_) => ptr::null_mut(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn dlclose(handle: *mut u8) -> i32 { - match module_index(handle) { - Some(index) => syscall::dl_close(index) as i32, - None => -1, - } -} - -#[no_mangle] -pub unsafe extern "C" fn dlerror() -> *const u8 { - ptr::null() -} - /// `getentropy`: at most 256 bytes of the kernel's random source, all or an /// error, as POSIX says. #[no_mangle] diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index 56d6ab7799c..1eac55c5d7b 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -813,7 +813,8 @@ fn vm_prot(prot: i32) -> Result { match crate::memreq::access(prot) { None => Err(EINVAL), // POSIX's ENOTSUP, which is EOPNOTSUPP here. - Some(Access::Exec) => Err(crate::errcode::EOPNOTSUPP), + Some(Access::WriteExec) => Err(crate::errcode::EOPNOTSUPP), + Some(Access::ReadExec) => Ok(syscall::VmProt::ReadExec), Some(Access::None) => Ok(syscall::VmProt::None), Some(Access::Read) => Ok(syscall::VmProt::Read), Some(Access::ReadWrite) => Ok(syscall::VmProt::ReadWrite), @@ -841,7 +842,7 @@ pub unsafe extern "C" fn mmap( crate::errno::set(match refusal { MapRefusal::Invalid => EINVAL, MapRefusal::File => crate::errcode::ENODEV, - MapRefusal::Exec => crate::errcode::EOPNOTSUPP, + MapRefusal::WriteExec => crate::errcode::EOPNOTSUPP, }); return MAP_FAILED; } diff --git a/sdk/libc/src/pthread.rs b/sdk/libc/src/pthread.rs index aed4bf68480..28b680a1514 100644 --- a/sdk/libc/src/pthread.rs +++ b/sdk/libc/src/pthread.rs @@ -38,9 +38,11 @@ struct Thread { /// Nonzero once `tid` is written; the thread waits for it before it runs. published: AtomicU32, state: AtomicU32, - /// The stack's reservation: [`GUARD`] reserved, then the stack committed. + /// The thread's memory (`toyos::tls::ThreadMemory`): its area, a guard and its stack. reservation: *mut u8, reserved: usize, + /// What the thread names as its thread pointer first. + tp: usize, start: StartRoutine, arg: *mut u8, result: AtomicPtr, @@ -63,28 +65,6 @@ static MARKER: u8 = 0; pub(crate) use toyos::sync::Lock; -/// Reserved pages below a thread's stack, so that running off it faults: -/// one page, which C's frames reach through only by skipping it, as a guard -/// page on every POSIX system can be skipped. -const GUARD: usize = syscall::PAGE_SIZE; - -/// A stack of `size` bytes in whole pages above a [`GUARD`], committed before -/// the thread starts: the reservation, its length, and the stack's base. -fn reserve_stack(size: usize) -> Option<(*mut u8, usize, *mut u8)> { - let pages = size.checked_next_multiple_of(syscall::PAGE_SIZE)?; - let reserved = pages.checked_add(GUARD)?; - // SAFETY: placed anywhere, so it replaces nothing. - let reservation = unsafe { syscall::vm_reserve(reserved, syscall::VmProt::None, syscall::VmAt::ANYWHERE) }.ok()?; - let base = reservation.wrapping_add(GUARD); - // SAFETY: the stack's pages, inside the reservation just made. - if unsafe { syscall::vm_protect(base, pages, syscall::VmProt::ReadWrite) }.is_err() { - // SAFETY: the reservation just made, which nothing reaches. - let _ = unsafe { syscall::vm_release(reservation, reserved) }; - return None; - } - Some((reservation, reserved, base)) -} - /// Wait for `thread` to be gone, free its stack and block, and answer what it /// returned. unsafe fn reap(thread: *mut Thread) -> *mut u8 { @@ -101,6 +81,8 @@ unsafe fn reap(thread: *mut Thread) -> *mut u8 { unsafe extern "C" fn thread_entry(arg: u64) { let thread = arg as *mut Thread; let t = unsafe { &*thread }; + // Before anything reads a thread-local. + toyos::tls::enter(t.tp).expect("libc: the kernel refused a thread's thread pointer"); while t.published.load(Ordering::Acquire) == 0 { // SAFETY: `published` is a live, aligned u32. unsafe { syscall::futex_wait(t.published.as_ptr(), 0, None) }; @@ -116,11 +98,13 @@ unsafe fn exit_thread(result: *mut u8) -> ! { run_thread_dtors(); run_key_destructors(); } + // Its tid is read below off the TCB, which outlives this. + toyos::tls::leave(); let thread = SELF.get(); if !thread.is_null() { let t = unsafe { &*thread }; if t.counted { - crate::signal::thread_ended(toyos_abi::current_tid().raw()); + crate::signal::thread_ended(toyos::tls::current_tid().raw()); } t.result.store(result, Ordering::Release); if t.state.swap(EXITED, Ordering::AcqRel) == DETACHED { @@ -169,9 +153,12 @@ pub(crate) fn spawn_own(start: StartRoutine) { unsafe fn create(attr: *const u64, start_routine: StartRoutine, arg: *mut u8, counted: bool) -> Result<*mut Thread, i32> { let attr = if attr.is_null() { attr_default() } else { unsafe { *attr } }; let stack_size = (attr & !ATTR_DETACHED) as usize; - let Some((reservation, reserved, stack)) = reserve_stack(stack_size) else { + // A guard page below the stack, which C's frames reach through only by + // skipping it, as a guard page on every POSIX system can be skipped. + let Some(memory) = toyos::tls::ThreadMemory::reserve(stack_size) else { return Err(EAGAIN); }; + let toyos::tls::ThreadMemory { reservation, reserved, stack_base: stack, stack_top, tp } = memory; let state = if attr & ATTR_DETACHED != 0 { DETACHED } else { JOINABLE }; let block = Box::into_raw(Box::new(Thread { tid: AtomicU64::new(0), @@ -179,12 +166,13 @@ unsafe fn create(attr: *const u64, start_routine: StartRoutine, arg: *mut u8, co state: AtomicU32::new(state), reservation, reserved, + tp, start: start_routine, arg, result: AtomicPtr::new(ptr::null_mut()), counted, })); - let top = (reservation as usize + reserved) & !(STACK_ALIGN - 1); + let top = stack_top as usize & !(STACK_ALIGN - 1); // SAFETY: the entry is a function of this library, and the stack is the // committed top of a fresh reservation. let tid = unsafe { syscall::thread_spawn(thread_entry as *const () as u64, top as u64, block as u64, stack as u64) }; @@ -298,7 +286,7 @@ pub unsafe extern "C" fn sched_setaffinity(pid: i32, cpusetsize: usize, mask: *c } let set = unsafe { core::slice::from_raw_parts(mask, cpusetsize) }; match crate::affinity::requested(set, toyos_abi::syscall::cpu_count() as usize) { - Ok(words) => match toyos_abi::syscall::thread_set_affinity(u64::from(toyos_abi::current_tid().0), &words) { + Ok(words) => match toyos_abi::syscall::thread_set_affinity(u64::from(toyos::tls::current_tid().0), &words) { Ok(()) => 0, Err(e) => crate::posix_io::set_errno(e), }, @@ -313,7 +301,7 @@ pub unsafe extern "C" fn sched_getaffinity(pid: i32, cpusetsize: usize, mask: *m if pid != 0 { return crate::posix_io::refuse(ESRCH); } - let words = match toyos_abi::syscall::thread_affinity(u64::from(toyos_abi::current_tid().0)) { + let words = match toyos_abi::syscall::thread_affinity(u64::from(toyos::tls::current_tid().0)) { Ok(words) => words, Err(e) => return crate::posix_io::set_errno(e), }; diff --git a/sdk/libc/src/signal.rs b/sdk/libc/src/signal.rs index 0067b09261b..3e85a5b1d6f 100644 --- a/sdk/libc/src/signal.rs +++ b/sdk/libc/src/signal.rs @@ -139,7 +139,7 @@ impl State { } fn tid() -> u32 { - toyos_abi::current_tid().raw() + toyos::tls::current_tid().raw() } /// `sigaction`'s work: `sig`'s action made `new`, if any, and the old one diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index c8a41ede92d..5ddb29ca49c 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -209,10 +209,8 @@ unsafe extern "C" fn _start() -> ! { ); } -/// .init_array constructor: registers the EH frame finder for DWARF unwinding. -/// For executables, this runs before `_start`. For cdylib .so files loaded via -/// dlopen, the kernel returns the .init_array to userspace which calls it. -/// This ensures panic unwinding works from code inside shared libraries. +/// .init_array constructor: registers the EH frame finder for DWARF unwinding +/// in a library, whose loader runs it; an executable's start registers its own. extern "C" fn init_eh_frame() { eh_frame::init(); } @@ -224,112 +222,68 @@ static INIT_EH_FRAME: extern "C" fn() = init_eh_frame; extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { unsafe extern "C" { fn main(argc: i32, argv: *const *const u8) -> i32; + static __ehdr_start: u8; } + // The C library's loader (`sdk/libc/src/dl.rs`), where the program links + // it: libtoyos_c is built from this std, so std cannot name it itself. + unsafe extern "C" { + #[linkage = "extern_weak"] + static __toyos_load_needed: Option; + } + // The program goes by its `argv[0]`. + let name = match argc { + 0 => &[][..], + _ => unsafe { crate::ffi::CStr::from_ptr((*argv).cast()) }.to_bytes(), + }; + // SAFETY: the executable's own header, and this is its main thread. + unsafe { toyos::tls::start(&raw const __ehdr_start, name) }; ARGC.store(argc, Ordering::Relaxed); ARGV.store(argv.expose_provenance(), Ordering::Relaxed); - // Register EH frame finder (also in .init_array for cdylib, but exes don't run .init_array) eh_frame::init(); - - // Initialize environment variables crate::sys::env::init(); + // Before anything that names a symbol of a library it links. + // SAFETY: a weak symbol's value, read once. + match unsafe { __toyos_load_needed } { + // SAFETY: the executable's own header, its thread-local storage laid out. + Some(load) => unsafe { load(&raw const __ehdr_start) }, + None if toyos::relocate::names_a_library() => { + // As `toyos::relocate` refuses its program: a line and 127. + let _ = crate::io::Write::write_all(&mut crate::io::stderr(), NO_LOADER); + toyos_abi::syscall::exit(127) + } + None => {} + } let code = unsafe { main(argc as i32, argv) }; crate::sys::stdio::finish(); toyos_abi::syscall::exit(code) } +/// What a program that names libraries and links no loader says as it ends. +const NO_LOADER: &[u8] = b"this program names libraries and links no loader: it cannot start\n"; + pub fn abort_internal() -> ! { toyos_abi::syscall::exit(128 + 6) // SIGABRT-like — kill entire process } -/// DWARF EH frame finder for the `unwinding` crate. -/// Locates `.eh_frame_hdr` for a given PC via `SYS_QUERY_MODULES`. +/// DWARF EH frame finder for the `unwinding` crate: the `.eh_frame_hdr` of the +/// module that holds a PC, off the process's module list (`toyos::tls`). mod eh_frame { - use toyos_abi::syscall::ModuleInfo; - - use crate::sync::Mutex; - - struct Module { - base: usize, - end: usize, - eh_frame_hdr: usize, - eh_frame_hdr_size: usize, - } - - static CACHE: Mutex> = Mutex::new(Vec::new()); - - fn load_modules() -> Vec { - let mut buf = vec![0u8; 4096]; - loop { - match toyos_abi::syscall::query_modules(&mut buf) { - Ok(count) => { - let info_size = core::mem::size_of::(); - let mut modules = Vec::with_capacity(count); - for i in 0..count { - let off = i * info_size; - if off + info_size > buf.len() { - break; - } - let info = unsafe { &*(buf.as_ptr().add(off) as *const ModuleInfo) }; - modules.push(Module { - base: info.base as usize, - end: info.text_end as usize, - eh_frame_hdr: info.eh_frame_hdr as usize, - eh_frame_hdr_size: info.eh_frame_hdr_size as usize, - }); - } - return modules; - } - Err(_) => { - buf.resize(buf.len() * 2, 0); - if buf.len() > 1024 * 1024 { - return Vec::new(); - } - } - } - } - } - struct ToyOsEhFrameFinder; static FINDER: ToyOsEhFrameFinder = ToyOsEhFrameFinder; unsafe impl unwind::EhFrameFinder for ToyOsEhFrameFinder { fn find(&self, pc: usize) -> Option { - // Fast path: check cached modules - { - let cache = CACHE.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(m) = cache.iter().find(|m| pc >= m.base && pc < m.end) { - if m.eh_frame_hdr != 0 { - return Some(unwind::FrameInfo { - text_base: Some(m.base), - kind: unwind::FrameInfoKind::EhFrameHdr(m.eh_frame_hdr), - }); - } - return None; - } - } - - // Cache miss — reload module list (handles dlopen) - let modules = load_modules(); - let result = modules.iter().find(|m| pc >= m.base && pc < m.end).and_then(|m| { - if m.eh_frame_hdr != 0 { - Some(unwind::FrameInfo { - text_base: Some(m.base), - kind: unwind::FrameInfoKind::EhFrameHdr(m.eh_frame_hdr), - }) - } else { - None - } - }); - *CACHE.lock().unwrap_or_else(|e| e.into_inner()) = modules; - result + let module = toyos::tls::modules().find(|m| m.holds(pc))?; + (module.image.eh_frame_hdr != 0).then(|| unwind::FrameInfo { + text_base: Some(module.image.base), + kind: unwind::FrameInfoKind::EhFrameHdr(module.image.eh_frame_hdr), + }) } } pub(super) fn init() { - let modules = load_modules(); - *CACHE.lock().unwrap_or_else(|e| e.into_inner()) = modules; unwind::set_custom_eh_frame_finder(&FINDER).ok(); } } diff --git a/sdk/std/sys/pal/tls.rs b/sdk/std/sys/pal/tls.rs index 37174982911..86eac2bba91 100644 --- a/sdk/std/sys/pal/tls.rs +++ b/sdk/std/sys/pal/tls.rs @@ -1,89 +1,38 @@ -/// DTV-based TLS access for shared libraries. -/// -/// Called by shared library code when accessing `#[thread_local]` variables of -/// a module whose TLS block is found through the dynamic thread vector (DTV). -/// -/// DTV layout: -/// [0x00] generation: u64 -/// [0x08] len: u64 -/// [0x10] entries[0]: u64 (module_id=1) -/// [0x18] entries[1]: u64 (module_id=2) -/// ... -/// -/// Entry value is the base address of that module's TLS block, -/// or DTV_UNALLOCATED (!0) if not yet allocated. -/// -/// x86-64 (TLS variant II, GD/LD model): the linker preserves -/// `call __tls_get_addr` in .so files and emits R_X86_64_DTPMOD64/DTPOFF64 GOT -/// slot pairs. At load time, the kernel fills: -/// GOT[0] = module_id (DTV index, 1-based) -/// GOT[1] = offset within module's TLS segment -/// -/// TCB layout (at fs_base / TP): -/// fs:[0x00] = self_ptr -/// fs:[0x08] = dtv_ptr -/// -/// __tls_get_addr receives a pointer to TlsIndex {module_id, offset} in %rdi, -/// returns the address of the TLS variable in %rax. -/// -/// AArch64 (TLS variant I). TCB layout (at TPIDR_EL0 / TP): -/// [TP+0x00] = dtv_ptr -/// [TP+0x08] = reserved -/// followed by the executable's TLS block at TP + align_up(16, p_align). -/// A shared library's thread-locals need a TLS descriptor resolver, which -/// AArch64 does not have yet; the loader refuses R_AARCH64_TLSDESC. +//! `__tls_get_addr`, the general-dynamic TLS model's call: a shared object +//! reaches a module's thread-locals through a GOT pair `[module id, offset]` +//! its loader filled, and the module's block through the calling thread's DTV +//! (`toyos::tls`), which the TCB at `fs:8` names. AArch64 shared objects +//! address thread-locals through TLS descriptors, which the loader refuses. -/// Slow path: the DTV entry is unallocated or out of range. -/// Calls SYS_TLS_ALLOC_BLOCK to allocate the TLS block on demand. #[cfg(target_arch = "x86_64")] #[inline(never)] -unsafe extern "C" fn __tls_get_addr_slow(module_id: u64, offset: u64) -> *mut u8 { - // The caller's ABI is an address and there is nobody to return an error - // to: a refusal added to `offset` is a pointer near the top of the address - // space that the caller would then dereference. - match toyos_abi::syscall::tls_alloc_block(module_id) { - Ok(block) => core::ptr::without_provenance_mut((block + offset) as usize), - Err(_) => rtabort!("no TLS block for a dlopen'd module"), - } +unsafe extern "C" fn __tls_get_addr_slow(module: usize, offset: usize) -> *mut u8 { + toyos::tls::block(module).wrapping_add(offset) } -/// Fast path: naked asm reads DTV directly from fs:[8], checks bounds and allocation, -/// falls through to slow path only when needed. +/// The fast path: the DTV's slot for the module, when it is in range and its +/// block is made; anything else is the slow path's. #[cfg(target_arch = "x86_64")] #[unsafe(no_mangle)] #[unsafe(naked)] -pub unsafe extern "C" fn __tls_get_addr(ti: *const [u64; 2]) -> *mut u8 { +pub unsafe extern "C" fn __tls_get_addr(ti: *const [usize; 2]) -> *mut u8 { core::arch::naked_asm!( - // ti is in %rdi: [module_id, offset] - "mov rsi, [rdi + 8]", // rsi = offset - "mov rdi, [rdi]", // rdi = module_id - - // module_id == 0 guard (shouldn't happen, but be safe) + "mov rsi, [rdi + 8]", + "mov rdi, [rdi]", "test rdi, rdi", "jz 2f", - - // Load DTV pointer from TCB: fs:[8] "mov rax, fs:[8]", - - // Bounds check: module_id <= dtv[1] (len) - "cmp rdi, [rax + 8]", + "cmp rdi, [rax + {capacity}]", "ja 2f", - - // Load DTV entry: dtv[2 + (module_id - 1)] - "lea rcx, [rdi - 1]", - "mov rax, [rax + rcx * 8 + 16]", - - // Check for DTV_UNALLOCATED (!0) + "mov rax, [rax + rdi * 8 + {slots} - 8]", "cmp rax, -1", "je 2f", - - // Fast path: return entry + offset "add rax, rsi", "ret", - - // Slow path "2:", "jmp {slow}", + capacity = const toyos::tls::DTV_CAPACITY, + slots = const toyos::tls::DTV_SLOTS, slow = sym __tls_get_addr_slow, ); } diff --git a/sdk/std/sys/thread.rs b/sdk/std/sys/thread.rs index 2b09e6b3dc6..dbc89bdab06 100644 --- a/sdk/std/sys/thread.rs +++ b/sdk/std/sys/thread.rs @@ -5,7 +5,6 @@ use crate::ffi::CStr; use crate::io; use crate::mem::ManuallyDrop; use crate::num::NonZero; -use crate::ptr; use crate::sync::atomic::{AtomicU32, Ordering}; use crate::thread::ThreadInit; use crate::time::Duration; @@ -24,9 +23,11 @@ unsafe impl Sync for Thread {} struct Block { tid: AtomicU32, state: AtomicU32, - /// The stack's reservation: [`GUARD`] reserved, then the stack committed. + /// The thread's memory (`toyos::tls::ThreadMemory`): its area, a guard and its stack. reservation: *mut u8, reserved: usize, + /// What the thread names as its thread pointer first. + tp: usize, /// Taken by the thread as it starts. init: UnsafeCell>>, } @@ -37,11 +38,6 @@ const EXITED: u32 = 2; pub const DEFAULT_MIN_STACK_SIZE: usize = 2 * 1024 * 1024; -/// Reserved pages below a thread's stack, so that running off it faults: one -/// page is enough, since every frame larger than a page probes each page it -/// spans (the target's inline stack probes). -const GUARD: usize = syscall::PAGE_SIZE; - /// Wait for `block`'s thread to be gone, then free its stack and `block`. A /// join the kernel refuses frees nothing, since the stack may still be in use. /// @@ -59,30 +55,21 @@ unsafe fn reap(block: *mut Block) { impl Thread { pub unsafe fn new(stack: usize, init: Box) -> io::Result { - // The size asked for, in whole pages, committed before the thread - // starts so that no touch of it can find no memory. - let Some(stack_size) = stack.max(1).checked_next_multiple_of(syscall::PAGE_SIZE) else { - return Err(io::const_error!(io::ErrorKind::InvalidInput, "thread stack size too large")); + // The stack asked for, in whole pages, and the thread's area, committed + // before the thread starts so that no touch of either can find no + // memory. One guard page is enough below the stack, since every frame + // larger than a page probes each page it spans (the target's inline + // stack probes). + let Some(memory) = toyos::tls::ThreadMemory::reserve(stack) else { + return Err(io::const_error!(io::ErrorKind::OutOfMemory, "no memory for a thread")); }; - let reserved = stack_size.checked_add(GUARD).ok_or(io::const_error!( - io::ErrorKind::InvalidInput, - "thread stack size too large" - ))?; - // SAFETY: placed anywhere, so it replaces nothing, and the stack is - // inside the reservation. - let reservation = unsafe { syscall::vm_reserve(reserved, syscall::VmProt::None, syscall::VmAt::ANYWHERE) } - .map_err(|_| io::const_error!(io::ErrorKind::OutOfMemory, "no room for a thread stack"))?; - let stack_base = reservation.wrapping_add(GUARD); - if unsafe { syscall::vm_protect(stack_base, stack_size, syscall::VmProt::ReadWrite) }.is_err() { - let _ = unsafe { syscall::vm_release(reservation, reserved) }; - return Err(io::const_error!(io::ErrorKind::OutOfMemory, "thread stack commit failed")); - } - let stack_top = stack_base.expose_provenance() as u64 + stack_size as u64; + let toyos::tls::ThreadMemory { reservation, reserved, stack_base, stack_top, tp } = memory; let block = Box::into_raw(Box::new(Block { tid: AtomicU32::new(0), state: AtomicU32::new(JOINABLE), reservation, reserved, + tp, init: UnsafeCell::new(Some(init)), })); @@ -90,7 +77,7 @@ impl Thread { let tid = unsafe { syscall::thread_spawn( (thread_trampoline as *const ()).expose_provenance() as u64, - stack_top, + stack_top.expose_provenance() as u64, block.expose_provenance() as u64, stack_base.expose_provenance() as u64, ) @@ -130,6 +117,11 @@ impl Drop for Thread { extern "C" fn thread_trampoline(data: u64) { let block = crate::ptr::with_exposed_provenance_mut::(data as usize); + // Before anything reads a thread-local. + // SAFETY: the spawner wrote the block, which outlives this thread. + if toyos::tls::enter(unsafe { (*block).tp }).is_err() { + rtabort!("the kernel refused a thread's thread pointer"); + } // SAFETY: the spawner put the init here and only this thread takes it. let init = unsafe { (*(*block).init.get()).take() }.expect("a thread's init is taken once"); let main = init.init(); @@ -140,10 +132,11 @@ extern "C" fn thread_trampoline(data: u64) { crate::sys::thread_local::destructors::run(); } crate::rt::thread_cleanup(); + toyos::tls::leave(); // SAFETY: the block is this thread's until its state says otherwise. if unsafe { &*block }.state.swap(EXITED, Ordering::AcqRel) == DETACHED { // Nobody holds the block but this thread, which frees it and leaves - // its stack to the kernel to release. + // its memory to the kernel to release. let Block { reservation, reserved, .. } = *unsafe { Box::from_raw(block) }; // SAFETY: the stack is this thread's alone, and ends with it. let refused = unsafe { syscall::thread_exit_detached(0, reservation, reserved) }; @@ -154,7 +147,7 @@ extern "C" fn thread_trampoline(data: u64) { /// The CPUs the calling thread may run on, as Linux's counts its affinity. pub fn available_parallelism() -> io::Result> { - let mask = syscall::thread_affinity(u64::from(toyos_abi::current_tid().0)) + let mask = syscall::thread_affinity(u64::from(toyos::tls::current_tid().0)) .map_err(|_| io::const_error!(io::ErrorKind::NotFound, "the kernel reads no mask for this thread"))?; NonZero::new(mask.iter().map(|word| word.count_ones() as usize).sum()) .ok_or(io::const_error!(io::ErrorKind::NotFound, "the thread's mask names no CPU")) diff --git a/sdk/toyos/src/arch/aarch64.rs b/sdk/toyos/src/arch/aarch64.rs index c8682c2bf89..fc07f38c773 100644 --- a/sdk/toyos/src/arch/aarch64.rs +++ b/sdk/toyos/src/arch/aarch64.rs @@ -131,3 +131,31 @@ pub(crate) mod timing { } } } + +/// The thread control block the thread pointer (`TPIDR_EL0`) addresses, psABI +/// TLS variant I: the two words at TP, `TP + 0` the DTV and `TP + 8` the word +/// the psABI leaves to the implementation, this SDK's tid; the executable's +/// TLS data starts `max(16, p_align)` above TP. +pub(crate) mod tcb { + pub(crate) const VARIANT: crate::tls::Variant = crate::tls::Variant::I; + pub(crate) const BYTES: usize = 16; + pub(crate) const DTV: usize = 0; + pub(crate) const TID: usize = 8; + + /// The calling thread's thread pointer. + #[inline] + pub(crate) fn thread_pointer() -> usize { + let tp: usize; + // SAFETY: a read of `TPIDR_EL0` into a register. + unsafe { core::arch::asm!("mrs {}, tpidr_el0", out(reg) tp, options(nomem, nostack, preserves_flags)) }; + tp + } + + /// The calling thread's id, read off its TCB. + #[inline] + pub(crate) fn current_tid() -> u32 { + // SAFETY: `TP + TID` lies inside the TCB every thread's startup lays + // out before it names its thread pointer. + unsafe { core::ptr::read_volatile((thread_pointer() + TID) as *const u32) } + } +} diff --git a/sdk/toyos/src/arch/mod.rs b/sdk/toyos/src/arch/mod.rs index 63ccb62aab5..896fe90ae7d 100644 --- a/sdk/toyos/src/arch/mod.rs +++ b/sdk/toyos/src/arch/mod.rs @@ -3,9 +3,9 @@ #[cfg(target_arch = "aarch64")] mod aarch64; #[cfg(target_arch = "aarch64")] -pub(crate) use aarch64::{copy, timing, zero}; +pub(crate) use aarch64::{copy, tcb, timing, zero}; #[cfg(target_arch = "x86_64")] mod x86_64; #[cfg(target_arch = "x86_64")] -pub(crate) use x86_64::{copy, timing, zero}; +pub(crate) use x86_64::{copy, tcb, timing, zero}; diff --git a/sdk/toyos/src/arch/x86_64.rs b/sdk/toyos/src/arch/x86_64.rs index 185da368621..b228b2dd9be 100644 --- a/sdk/toyos/src/arch/x86_64.rs +++ b/sdk/toyos/src/arch/x86_64.rs @@ -57,3 +57,40 @@ pub(crate) mod timing { } } } + +/// The thread control block the thread pointer (`FS.base`) addresses, psABI +/// TLS variant II: `TP + 0` the self-pointer, `TP + 8` the DTV, then this +/// SDK's words; the executable's TLS data lies below TP. +pub(crate) mod tcb { + pub(crate) const VARIANT: crate::tls::Variant = crate::tls::Variant::II; + pub(crate) const BYTES: usize = 64; + pub(crate) const DTV: usize = 8; + pub(crate) const TID: usize = 16; + + /// The calling thread's thread pointer, read off its self-pointer. + #[inline] + pub(crate) fn thread_pointer() -> usize { + let tp: usize; + // SAFETY: `fs:0` is the TCB's self-pointer, which every thread's + // startup writes before it names its thread pointer. + unsafe { core::arch::asm!("mov {}, fs:[0]", out(reg) tp, options(nostack, readonly, preserves_flags)) }; + tp + } + + /// The calling thread's id, read off its TCB in one load. + #[inline] + pub(crate) fn current_tid() -> u32 { + let tid: u32; + // SAFETY: `TP + TID` lies inside the TCB every thread's startup lays + // out before it names its thread pointer. + unsafe { + core::arch::asm!( + "mov {tid:e}, dword ptr fs:[{at}]", + tid = out(reg) tid, + at = const TID, + options(nostack, readonly, preserves_flags), + ); + } + tid + } +} diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index 9a81377acd3..bb11cbb4f6f 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -410,8 +410,8 @@ pub fn read_dir(fd: Fd, cursor: u64) -> Result<(Vec, Option), Error> { } /// The whole of the file at `path`, wherever it is served: what a spawn names -/// as its program (`SpawnArgs::image`) and [`dl_open`] loads, since the kernel -/// opens neither. The object its server holds it in where there is one +/// as its program (`SpawnArgs::image`) and a library's loader maps, since the +/// kernel opens neither. The object its server holds it in where there is one /// ([`crate::fs::IMAGE`]), and otherwise a copy read into memory of this /// process's own. pub fn read_image(path: &[u8]) -> Result> { @@ -441,28 +441,6 @@ pub fn read_image_for(path: &[u8], job: Option>) -> Re 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 = 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) -} - /// `path` as a path names something: UTF-8, and not empty, which names /// nothing, as POSIX resolves no empty path. A call that takes the empty path /// as the descriptor itself (`stat` and `set_times` of `At::Fd`, a `link` diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 1e07b7351dd..63a64899272 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -45,6 +45,7 @@ pub mod syscap; pub mod sync; pub mod system; pub mod timing; +pub mod tls; pub mod transport; pub mod volatile; diff --git a/sdk/toyos/src/log/stdio.rs b/sdk/toyos/src/log/stdio.rs index 891806987be..cad0f25efcc 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -306,7 +306,7 @@ mod target { fn stamp(body: &mut Body) { body.at_ns = toyos_abi::clock::stamp_ns(); body.pid = PID.load(Ordering::Relaxed); - body.tid = toyos_abi::current_tid().0; + body.tid = crate::tls::current_tid().0; } fn push(ring: &Ring) -> impl FnMut(&mut Body) + '_ { @@ -361,7 +361,7 @@ mod target { /// lane is claimed. pub fn claim_lane() -> Option { match sink(Stream::Err) { - Sink::Ring(ring) => ring.claim_lane(PID.load(Ordering::Relaxed), toyos_abi::current_tid().0), + Sink::Ring(ring) => ring.claim_lane(PID.load(Ordering::Relaxed), crate::tls::current_tid().0), Sink::Descriptor(_) => None, } } diff --git a/sdk/toyos/src/relocate/mod.rs b/sdk/toyos/src/relocate/mod.rs index 0626148249b..dd2d2897879 100644 --- a/sdk/toyos/src/relocate/mod.rs +++ b/sdk/toyos/src/relocate/mod.rs @@ -13,14 +13,16 @@ //! memory. //! //! A static PIE carries `RELATIVE` relocations and nothing else, so that is -//! all this applies. Every other form is refused by name: a library this -//! process would have to load (`DT_NEEDED`), a symbol it would have to bind -//! (`DT_JMPREL` with entries, any other type), a table form it does not read +//! all this applies. An image that names a library (`DT_NEEDED`) leaves every +//! other relocation in its tables to the loader, which binds them once its +//! libraries are loaded (libc's `__toyos_load_needed`). Every other form is +//! refused by name: in an image that names no library, a symbol it would have +//! to bind (`DT_JMPREL` with entries, any other type); a table form it does not read //! (`DT_REL`, every `RELR`, Android's packed tables, and every one's size), //! and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). //! A write lands only inside a writable `PT_LOAD` and never in the `PT_TLS` -//! template, which the kernel copies from the file into every thread's block -//! and which a write here would never reach. +//! template, which every thread's block is copied from as it was mapped and +//! which a write here would never reach in a block already made. #![deny( clippy::indexing_slicing, @@ -33,8 +35,19 @@ #[cfg(test)] mod tests; +use core::sync::atomic::{AtomicBool, Ordering}; + use toyos_abi::RawHandle; +/// Whether the image [`relocate_self`] relocated names a library (`DT_NEEDED`). +static NAMES_A_LIBRARY: AtomicBool = AtomicBool::new(false); + +/// Whether this program names a library its start must load before anything +/// binds to it: what [`relocate_self`] found. +pub fn names_a_library() -> bool { + NAMES_A_LIBRARY.load(Ordering::Relaxed) +} + /// Why a program's relocations are refused. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Refusal { @@ -46,8 +59,6 @@ enum Refusal { HeaderSegment, /// `PT_DYNAMIC` lies in no `PT_LOAD`'s file bytes. Dynamic, - /// `DT_NEEDED`: a library this process would have to load. - Needed, /// `DT_JMPREL` with entries, or with no `DT_PLTRELSZ` to say it has none: /// slots this process would have to bind. JmpRel, @@ -103,7 +114,7 @@ pub unsafe extern "C" fn relocate_self(header: *const u8) { /// kernel's endowment table with nothing but the stack and the code's own /// addresses: no word of the image is relocated yet, so no table, `str` /// pointer or formatting is reached. -fn stderr() -> Option { +pub(crate) fn stderr() -> Option { use toyos_abi::syscall::{endowments, MAX_ENDOWMENTS, MAX_LABELS_LEN}; const ENTRY: usize = 16; let word = |b: &[u8], at: usize| -> Option { @@ -155,13 +166,15 @@ unsafe fn apply_to_self(header: *const u8) -> Result<(), Refusal> { }; image.rela(dynamic)? }; + let Tables { rela, needed } = rela; + NAMES_A_LIBRARY.store(needed, Ordering::Relaxed); let Some(rela) = rela else { return Ok(()) }; // SAFETY: inside a `PT_LOAD`'s file bytes that no writable segment // overlaps, so no write below reaches it. let rela = unsafe { core::slice::from_raw_parts(core::ptr::with_exposed_provenance::(at(rela.vaddr, bias)), rela.len) }; - image.apply(rela, bias, |vaddr, value| { + image.apply(rela, bias, needed, |vaddr, value| { let slot = core::ptr::with_exposed_provenance_mut::(at(vaddr, bias)); // SAFETY: `apply` names only a vaddr whose 8 bytes lie in a writable // `PT_LOAD`, which the kernel mapped writable at `bias + vaddr`, and @@ -213,6 +226,13 @@ const DT_AARCH64_AUTH_RELR: u64 = 0x7000_0012; const DT_AARCH64_AUTH_RELRENT: u64 = 0x7000_0013; const DF_TEXTREL: u64 = 4; +/// The `DT_RELA` table a dynamic section names, and whether it names a library. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Tables { + rela: Option, + needed: bool, +} + /// A run of the image's bytes, at its link-time address. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Span { @@ -346,15 +366,15 @@ impl<'a> Image<'a> { Ok(Some(Span { vaddr: dynamic.vaddr, len })) } - /// The `DT_RELA` table `dynamic` names, or `None` when it names none. - fn rela(&self, dynamic: &[u8]) -> Result, Refusal> { - let (mut rela, mut size, mut jmprel, mut plt) = (None, None, false, None); + /// The `DT_RELA` table `dynamic` names, if any, and whether it names a library. + fn rela(&self, dynamic: &[u8]) -> Result { + let (mut rela, mut size, mut jmprel, mut plt, mut needed) = (None, None, false, None, false); let aarch64 = self.relative == R_AARCH64_RELATIVE; for entry in dynamic.as_chunks::().0 { let (Some(tag), Some(value)) = (u64_at(entry, 0), u64_at(entry, 8)) else { break }; match tag { DT_NULL => break, - DT_NEEDED => return Err(Refusal::Needed), + DT_NEEDED => needed = true, DT_PLTRELSZ => plt = Some(value), DT_JMPREL => jmprel = true, DT_RELA => rela = Some(value), @@ -375,12 +395,13 @@ impl<'a> Image<'a> { _ => {} } } - // A `DT_JMPREL` whose size is missing is no table of known length. - if plt.is_some_and(|plt| plt != 0) || (jmprel && plt.is_none()) { + // A `DT_JMPREL` whose size is missing is no table of known length; one + // with entries is the loader's to bind, in an image that names a library. + if (!needed && plt.is_some_and(|plt| plt != 0)) || (jmprel && plt.is_none()) { return Err(Refusal::JmpRel); } let (vaddr, size) = match (rela, size) { - (None, None) => return Ok(None), + (None, None) => return Ok(Tables { rela: None, needed }), (Some(vaddr), Some(size)) if size.is_multiple_of(RELA_SIZE as u64) => (vaddr, size), _ => return Err(Refusal::RelaShape), }; @@ -388,12 +409,14 @@ impl<'a> Image<'a> { return Err(Refusal::RelaPlacement); } let len = usize::try_from(size).map_err(|_| Refusal::RelaPlacement)?; - Ok(Some(Span { vaddr, len })) + Ok(Tables { rela: Some(Span { vaddr, len }), needed }) } - /// Write `bias + addend` at every entry of `rela` through `write`, which - /// is handed the entry's link-time address; the first refusal stops it. - fn apply(&self, rela: &[u8], bias: u64, mut write: impl FnMut(u64, u64)) -> Result<(), Refusal> { + /// Write `bias + addend` at every `RELATIVE` entry of `rela` through + /// `write`, which is handed the entry's link-time address; the first + /// refusal stops it. Any other type is the loader's in an image that names + /// a library (`needed`), and refused in one that does not. + fn apply(&self, rela: &[u8], bias: u64, needed: bool, mut write: impl FnMut(u64, u64)) -> Result<(), Refusal> { for entry in rela.as_chunks::().0 { let (Some(offset), Some(info), Some(addend)) = (u64_at(entry, 0), u64_at(entry, 8), u64_at(entry, 16)) else { @@ -402,6 +425,9 @@ impl<'a> Image<'a> { // `r_info`'s low half is the type: the mask keeps it. let kind = (info & 0xffff_ffff) as u32; if kind != self.relative { + if needed { + continue; + } return Err(Refusal::Kind(kind)); } if !self.loads().any(|p| p.writable_load() && p.holds(offset, 8, p.memsz)) { diff --git a/sdk/toyos/src/relocate/tests.rs b/sdk/toyos/src/relocate/tests.rs index f4a9a3e6d9a..f8f08c82a49 100644 --- a/sdk/toyos/src/relocate/tests.rs +++ b/sdk/toyos/src/relocate/tests.rs @@ -63,9 +63,9 @@ fn writes(file: &[u8], bias: u64) -> Result, Refusal> { let at = |span: Span| (span.vaddr - image.header_vaddr) as usize; let slice = |span: Span| &memory[at(span)..at(span) + span.len]; let dynamic = image.dynamic()?.expect("a PT_DYNAMIC"); - let rela = image.rela(slice(dynamic))?.expect("a DT_RELA"); + let rela = image.rela(slice(dynamic))?.rela.expect("a DT_RELA"); let mut out = Vec::new(); - image.apply(slice(rela), bias, |vaddr, value| out.push((vaddr, value)))?; + image.apply(slice(rela), bias, false, |vaddr, value| out.push((vaddr, value)))?; Ok(out) } @@ -143,7 +143,7 @@ fn rela(offset: u64, kind: u64, addend: u64) -> Vec { } /// The x86-64 fixture's own table, with `tags` beside it. -fn x86_tables(tags: &[(u64, u64)]) -> Result, Refusal> { +fn x86_tables(tags: &[(u64, u64)]) -> Result { // `readelf -d x86_64.elf`: `RELA 0x210`, `RELASZ 216`. let own = [(DT_RELA, 0x210), (DT_RELASZ, 216), (DT_RELAENT, 24)]; image_of(X86_64).rela(&dynamic(&[&own[..], tags].concat())) @@ -151,8 +151,8 @@ fn x86_tables(tags: &[(u64, u64)]) -> Result, Refusal> { #[test] fn every_dynamic_form_it_does_not_apply_is_refused_by_name() { - assert_eq!(x86_tables(&[]), Ok(Some(Span { vaddr: 0x210, len: 216 }))); - assert_eq!(x86_tables(&[(DT_NEEDED, 1)]), Err(Refusal::Needed)); + let own = Some(Span { vaddr: 0x210, len: 216 }); + assert_eq!(x86_tables(&[]), Ok(Tables { rela: own, needed: false })); assert_eq!(x86_tables(&[(DT_JMPREL, 0x210), (DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); assert_eq!(x86_tables(&[(DT_PLTRELSZ, 24)]), Err(Refusal::JmpRel)); // A `DT_JMPREL` whose size is missing is no table of known length. @@ -172,7 +172,7 @@ fn every_dynamic_form_it_does_not_apply_is_refused_by_name() { // in an AArch64 image. for tag in [DT_AARCH64_AUTH_RELRSZ, DT_AARCH64_AUTH_RELR, DT_AARCH64_AUTH_RELRENT] { assert_eq!(image_of(AARCH64).rela(&dynamic(&[(tag, 0)])), Err(Refusal::Relr), "{tag:#x}"); - assert_eq!(image_of(X86_64).rela(&dynamic(&[(tag, 0)])), Ok(None), "{tag:#x}"); + assert_eq!(image_of(X86_64).rela(&dynamic(&[(tag, 0)])), Ok(Tables { rela: None, needed: false }), "{tag:#x}"); } assert_eq!(x86_tables(&[(DT_TEXTREL, 0)]), Err(Refusal::TextRel)); assert_eq!(x86_tables(&[(DT_FLAGS, DF_TEXTREL)]), Err(Refusal::TextRel)); @@ -180,10 +180,28 @@ fn every_dynamic_form_it_does_not_apply_is_refused_by_name() { assert!(x86_tables(&[(DT_FLAGS, 8)]).is_ok()); } +/// An image that names a library leaves its symbols to the loader: its +/// `DT_JMPREL` is no refusal, and only its `RELATIVE` entries are written. +#[test] +fn an_image_that_names_a_library_leaves_every_symbol_to_the_loader() { + let own = Some(Span { vaddr: 0x210, len: 216 }); + let needed = [(DT_NEEDED, 1), (DT_JMPREL, 0x210), (DT_PLTRELSZ, 24)]; + assert_eq!(x86_tables(&needed), Ok(Tables { rela: own, needed: true })); + // Still no table of known length. + assert_eq!(x86_tables(&[(DT_NEEDED, 1), (DT_JMPREL, 0x210)]), Err(Refusal::JmpRel)); + let mut out = Vec::new(); + let entries = [rela(0x2428, 6, 0), rela(0x2430, 8, 0x13d0), rela(0x2438, 7, 0)].concat(); + assert_eq!(image_of(X86_64).apply(&entries, BASE, true, |v, w| out.push((v, w))), Ok(())); + assert_eq!(out, vec![(0x2430, BASE + 0x13d0)]); + // A `RELATIVE` outside its place is refused all the same. + let outside = rela(0x13d0, 8, 0); + assert_eq!(image_of(X86_64).apply(&outside, BASE, true, |_, _| ()), Err(Refusal::OutsideWritable)); +} + #[test] fn a_rela_table_of_the_wrong_shape_or_place_is_refused() { let image = image_of(X86_64); - assert_eq!(image.rela(&dynamic(&[])), Ok(None)); + assert_eq!(image.rela(&dynamic(&[])), Ok(Tables { rela: None, needed: false })); assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x210)])), Err(Refusal::RelaShape)); assert_eq!(image.rela(&dynamic(&[(DT_RELASZ, 24)])), Err(Refusal::RelaShape)); assert_eq!(image.rela(&dynamic(&[(DT_RELA, 0x210), (DT_RELASZ, 25)])), Err(Refusal::RelaShape)); @@ -197,7 +215,7 @@ fn a_rela_table_of_the_wrong_shape_or_place_is_refused() { fn applied(file: &[u8], entries: &[Vec]) -> Result, Refusal> { let mut out = Vec::new(); - image_of(file).apply(&entries.concat(), BASE, |vaddr, value| out.push((vaddr, value)))?; + image_of(file).apply(&entries.concat(), BASE, false, |vaddr, value| out.push((vaddr, value)))?; Ok(out) } @@ -240,7 +258,7 @@ fn every_type_but_the_machines_relative_is_refused() { } // The first refusal stops it: nothing after it is written. let mut out = Vec::new(); - let refused = image_of(X86_64).apply(&[rela(0x2428, 7, 0), rela(0x2430, 8, 0)].concat(), BASE, |v, w| out.push((v, w))); + let refused = image_of(X86_64).apply(&[rela(0x2428, 7, 0), rela(0x2430, 8, 0)].concat(), BASE, false, |v, w| out.push((v, w))); assert_eq!((refused, out), (Err(Refusal::Kind(7)), vec![])); } diff --git a/sdk/toyos/src/tls/layout.rs b/sdk/toyos/src/tls/layout.rs new file mode 100644 index 00000000000..2553bdf2c1c --- /dev/null +++ b/sdk/toyos/src/tls/layout.rs @@ -0,0 +1,126 @@ +//! Where a thread's area puts its DTV, its executable's TLS data and its +//! thread control block, in the layout its machine's psABI names +//! ([`Variant`]), with the DTV at the area's first byte: +//! +//! ```text +//! variant II (x86-64): [DTV] [pad] [data] [TCB] TP at the TCB +//! variant I (AArch64): [DTV] [pad] [TCB] [gap] [data] TP at the TCB +//! ``` +//! +//! Variant II's linker fixed every local-exec offset as `S - round(memsz, +//! p_align)`, so the executable's data, so rounded, ends at TP. Variant I's +//! fixed them as `align_up(16, p_align) + S`, so the data starts that far +//! above TP. Every input is a number the executable declared. + +/// Which of the psABIs' two TLS layouts a machine uses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Variant { + /// TP addresses a two-word TCB, `[DTV, implementation]`, and the + /// executable's data follows it. + I, + /// TP addresses a TCB, `[self, DTV, ...]`, after the executable's data. + II, +} + +/// One thread's area, in offsets from its first byte. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Area { + /// Bytes from the first to past the last. + pub size: usize, + /// What the first byte is aligned to: a power of two, at least 16. + pub align: usize, + /// Where the executable's TLS data starts. + pub data: usize, + /// Where the thread pointer goes. + pub tp: usize, +} + +/// The area for an executable whose `PT_TLS` is `memsz` bytes declared +/// aligned to `p_align` (`memsz` zero for none), behind `dtv` bytes of DTV and +/// beside a `tcb`-byte TCB. `None` for an alignment that is not a power of two +/// or a size no `usize` holds. +pub fn area(variant: Variant, memsz: usize, p_align: usize, dtv: usize, tcb: usize) -> Option { + // Zero and one are "no constraint", as the ELF specification has them. + let p_align = p_align.max(1); + if !p_align.is_power_of_two() { + return None; + } + let align = p_align.max(16); + match variant { + Variant::II => { + let extent = align_up(memsz, p_align)?; + let tp = align_up(dtv.checked_add(extent)?, align)?; + Some(Area { size: tp.checked_add(tcb)?, align, data: tp - extent, tp }) + } + Variant::I => { + let tp = align_up(dtv, align)?; + let data = tp.checked_add(align)?; + let size = data.checked_add(memsz)?.max(tp.checked_add(tcb)?); + Some(Area { size, align, data, tp }) + } + } +} + +/// `value` rounded up to `granule`, a power of two. +pub(crate) fn align_up(value: usize, granule: usize) -> Option { + value.checked_add(granule - 1).map(|v| v & !(granule - 1)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const DTV: usize = 24 + 16 * 8; + + /// Every size and alignment a file can declare up to a few pages, both + /// variants: the data is on its alignment, clear of the DTV and the TCB, + /// inside the area, and exactly where the linker put the thread pointer + /// against it. + #[test] + fn the_data_is_where_the_linker_fixed_it_and_clear_of_everything_else() { + for (variant, tcb) in [(Variant::II, 64), (Variant::I, 16)] { + for memsz in (0..300).chain([4095, 4096, 4097, 3 * 4096 + 5]) { + for p_align in [0, 1, 2, 4, 8, 16, 32, 64, 4096, 1 << 21] { + let a = area(variant, memsz, p_align, DTV, tcb).unwrap(); + let p = p_align.max(1); + assert_eq!(a.data % p, 0, "{variant:?} memsz {memsz} align {p_align}: data off its alignment"); + assert!(a.align.is_power_of_two() && a.align >= 16 && a.align % p == 0); + assert!(a.data >= DTV, "the data meets the DTV"); + assert!(a.data + memsz <= a.size && a.tp + tcb <= a.size, "past the area's end"); + assert_eq!(a.tp % 16, 0, "a thread pointer off 16"); + match variant { + Variant::II => { + assert_eq!(a.data + memsz.next_multiple_of(p), a.tp, "variant II's data ends at TP"); + } + Variant::I => { + assert_eq!(a.data, a.tp + p.max(16), "variant I's data starts align_up(16, p_align) past TP"); + assert!(a.tp >= DTV); + } + } + } + } + } + } + + /// rust-lld's own answers. AArch64, an `aarch64-unknown-toyos` executable + /// whose `PT_TLS` is 0xb0 bytes aligned to 64: `add x0, x8, #0x40` for the + /// datum at offset 0 of `.tdata`, `#0x80` for the one at 0x40. x86-64: a + /// local-exec access is `S - round(p_memsz, p_align)`, so 0xa8 bytes aligned + /// to 0x40 end 0xc0 below TP. + #[test] + fn the_data_is_where_lld_linked_it() { + let a = area(Variant::I, 0xb0, 64, DTV, 16).unwrap(); + assert_eq!((a.data - a.tp, a.data + 0x40 - a.tp), (0x40, 0x80)); + let a = area(Variant::II, 0xa8, 0x40, DTV, 64).unwrap(); + assert_eq!(a.tp - a.data, 0xc0); + } + + #[test] + fn an_alignment_that_is_no_power_of_two_or_a_size_past_the_space_is_refused() { + assert_eq!(area(Variant::II, 8, 24, DTV, 64), None); + assert_eq!(area(Variant::I, 8, 3, DTV, 16), None); + assert_eq!(area(Variant::II, usize::MAX - 8, 16, DTV, 64), None); + assert_eq!(area(Variant::I, usize::MAX - 8, 16, DTV, 16), None); + } +} diff --git a/sdk/toyos/src/tls/mod.rs b/sdk/toyos/src/tls/mod.rs new file mode 100644 index 00000000000..a87fda81437 --- /dev/null +++ b/sdk/toyos/src/tls/mod.rs @@ -0,0 +1,460 @@ +//! Thread-local storage, which a program lays out itself: the kernel points a +//! thread's thread pointer where the thread names (`SYS_SET_THREAD_POINTER`) +//! and knows nothing else of it. +//! +//! **Every image of a process is one [`Module`] in one list**, the executable +//! first and each library in the order it was loaded, and a module's id is its +//! place in the list from 1: the index its `DTPMOD` names and its thread's +//! DTV slot. Nothing leaves the list. Every thread's DTV names the list, so +//! each copy of this crate in a process (std's, libc's, a program's own, a +//! loaded library's std's) finds the one the program's start made. +//! +//! **A thread's area** holds its DTV, the executable's data and its thread +//! control block ([`layout`]); a thread spawned here gets one below its stack, +//! released with it. Another module's block is made the first time the thread +//! asks for it ([`block`], `__tls_get_addr`'s slow path), and goes with +//! [`leave`]. Its DTV grows with the list, in the process's own memory. + +pub mod layout; + +use core::ffi::CStr; +use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering}; + +use toyos_abi::syscall::{self, SyscallError, VmAt, VmProt, PAGE_SIZE}; +use toyos_abi::Tid; + +use crate::arch::tcb; +pub use layout::{Area, Variant}; + +/// A DTV slot whose block has not been made. +pub const UNALLOCATED: usize = usize::MAX; + +/// A DTV's header, before its slots: the list, how many slots follow, and the +/// bytes of a reservation of its own, zero for the one inside a thread's area. +#[repr(C)] +struct Header { + modules: *const Modules, + capacity: usize, + owned: usize, +} + +/// Where a DTV's slot count is, from its first byte: `__tls_get_addr` bounds a +/// module id by it. +pub const DTV_CAPACITY: usize = core::mem::offset_of!(Header, capacity); +/// Where a DTV's slots start: module `n`'s is word `n - 1` from here. +pub const DTV_SLOTS: usize = core::mem::size_of::
(); +/// Slots in the DTV inside a thread's area. +const AREA_SLOTS: usize = 16; +const AREA_DTV: usize = DTV_SLOTS + AREA_SLOTS * 8; + +/// What a loaded image is, as everything that walks the list reads it. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct Image { + /// What a virtual address of the file is moved by. + pub base: usize, + /// The span its `PT_LOAD`s are mapped over. + pub start: usize, + pub end: usize, + /// Its program header table, as mapped. + pub phdr: usize, + pub phnum: usize, + /// Its `.eh_frame_hdr` as mapped, zero for none. + pub eh_frame_hdr: usize, +} + +/// A module's `PT_TLS`: its template as mapped, the bytes of it that are file +/// bytes, the whole block's bytes and its alignment. `memsz` zero for none. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct Tls { + pub template: usize, + pub filesz: usize, + pub memsz: usize, + pub align: usize, +} + +/// One image in the list. +#[repr(C)] +pub struct Module { + next: AtomicPtr, + id: usize, + name: *const core::ffi::c_char, + pub image: Image, + pub tls: Tls, +} + +// SAFETY: published once, never written again, and its pointers name memory +// that lives as long as the process. +unsafe impl Sync for Module {} + +impl Module { + /// A module to [`Loading::add`]: `name` is what it is known by, which a + /// second load of that name finds. + pub fn new(name: &'static CStr, image: Image, tls: Tls) -> Module { + Module { next: AtomicPtr::new(core::ptr::null_mut()), id: 0, name: name.as_ptr(), image, tls } + } + + pub fn id(&self) -> usize { + self.id + } + + pub fn name(&self) -> &'static CStr { + // SAFETY: a `&'static CStr` when it was made. + unsafe { CStr::from_ptr(self.name) } + } + + /// Whether `addr` lies in the span this image is mapped over. + pub fn holds(&self, addr: usize) -> bool { + (self.image.start..self.image.end).contains(&addr) + } +} + +/// The list, and what a thread's area is. +#[repr(C)] +pub struct Modules { + /// Held across a whole load, so a name is loaded once and ids are taken in order. + loading: AtomicU32, + first: AtomicPtr, + last: AtomicPtr, + count: AtomicUsize, + area: Area, +} + +/// The list, from the calling thread's DTV; `None` after [`leave`] released a +/// DTV of its own. +fn list() -> Option<&'static Modules> { + // SAFETY: a DTV's header names the list, which lives for the process. + unsafe { dtv().as_ref().map(|dtv| &*dtv.modules) } +} + +/// The calling thread's DTV. Every function here but [`start`] and [`enter`] +/// is for a thread that has named its thread pointer, which every thread the +/// SDK starts has: on x86-64 the read faults before then. +fn dtv() -> *mut Header { + // SAFETY: TP addresses the thread's TCB, whose DTV word its startup wrote. + unsafe { *((tcb::thread_pointer() + tcb::DTV) as *const *mut Header) } +} + +/// Every module, the executable first. A module published meanwhile may or may +/// not be visited. +pub fn modules() -> impl Iterator { + let mut at = list().map_or(core::ptr::null_mut(), |list| list.first.load(Ordering::Acquire)); + core::iter::from_fn(move || { + // SAFETY: a published module, never freed. + let module = unsafe { at.as_ref()? }; + at = module.next.load(Ordering::Acquire); + Some(module) + }) +} + +/// The list held for one load: the only way to add to it. +pub struct Loading(&'static Modules); + +impl Loading { + /// The module known by `name`, if one is. + pub fn find(&self, name: &[u8]) -> Option<&'static Module> { + modules().find(|m| m.name().to_bytes() == name) + } + + /// The id the next module added takes. + pub fn next_id(&self) -> usize { + self.0.count.load(Ordering::Relaxed) + 1 + } + + /// Publish `module` under [`next_id`](Self::next_id). + pub fn add(&mut self, mut module: Module) -> &'static Module { + module.id = self.next_id(); + let module: &'static Module = alloc::boxed::Box::leak(alloc::boxed::Box::new(module)); + let at = (module as *const Module).cast_mut(); + // SAFETY: the last module published, never freed; the list is held. + match unsafe { self.0.last.load(Ordering::Relaxed).as_ref() } { + Some(last) => last.next.store(at, Ordering::Release), + None => self.0.first.store(at, Ordering::Release), + } + self.0.last.store(at, Ordering::Relaxed); + self.0.count.store(module.id, Ordering::Release); + module + } +} + +impl Drop for Loading { + fn drop(&mut self) { + crate::sync::unlock_word(&self.0.loading); + } +} + +/// The list held for one load. +pub fn loading() -> Loading { + let list = list().expect("tls: a load on a thread that left"); + crate::sync::lock_word(&list.loading); + Loading(list) +} + +/// The calling thread's id, read off its TCB: what `SYS_SET_THREAD_POINTER` +/// answered its startup. +#[inline] +pub fn current_tid() -> Tid { + Tid::from_raw(tcb::current_tid()) +} + +/// What the executable at `header` is, from its own program headers, which the +/// kernel mapped and `toyos::relocate` already trusted. +/// +/// # Safety +/// `header` is the running executable's ELF header. +unsafe fn executable(header: *const u8) -> (Image, Tls) { + // SAFETY: the caller's; every offset below is inside the header or the + // program header table its segment maps. + unsafe { + let word = |at: usize| (header.add(at) as *const u64).read_unaligned() as usize; + let phoff = word(32); + let phnum = (header.add(56) as *const u16).read_unaligned() as usize; + let phdr = header.add(phoff); + let field = |i: usize, at: usize| (phdr.add(i * 56 + at) as *const u64).read_unaligned() as usize; + let kind = |i: usize| (phdr.add(i * 56) as *const u32).read_unaligned(); + let loads = (0..phnum).filter(|&i| kind(i) == PT_LOAD); + let Some(first) = loads.clone().find(|&i| field(i, 8) == 0) else { + refuse(b"no PT_LOAD maps this program's header: it cannot start\n"); + }; + let base = header as usize - (field(first, 16) & !(PAGE_SIZE - 1)); + let start = loads.clone().map(|i| field(i, 16)).min().unwrap_or(0) & !(PAGE_SIZE - 1); + let end = loads.map(|i| field(i, 16) + field(i, 40)).max().unwrap_or(0); + let of = |wanted: u32| (0..phnum).find(|&i| kind(i) == wanted); + let eh_frame_hdr = of(PT_GNU_EH_FRAME).map_or(0, |i| base + field(i, 16)); + let tls = of(PT_TLS).map_or(Tls::default(), |i| Tls { + template: base + field(i, 16), + filesz: field(i, 32), + memsz: field(i, 40), + align: field(i, 48), + }); + (Image { base, start: base + start, end: base + end, phdr: phdr as usize, phnum, eh_frame_hdr }, tls) + } +} + +const PT_LOAD: u32 = 1; +const PT_TLS: u32 = 7; +const PT_GNU_EH_FRAME: u32 = 0x6474_e550; + +/// The program's start: the list, with the executable at `header` in it as +/// `name`, and the calling thread's area, its thread pointer named. First +/// thing after relocation, before anything reads a thread-local; exits the +/// process when its memory cannot be had, since nothing of it can run. +/// +/// # Safety +/// `header` is the running executable's ELF header, and this is its main +/// thread's one call. +pub unsafe fn start(header: *const u8, name: &[u8]) { + // SAFETY: the caller's. + let (image, tls) = unsafe { executable(header) }; + let Some(area) = layout::area(tcb::VARIANT, tls.memsz, tls.align, AREA_DTV, tcb::BYTES) else { + refuse(b"this program's PT_TLS has no layout: it cannot start\n"); + }; + let list: &'static Modules = alloc::boxed::Box::leak(alloc::boxed::Box::new(Modules { + loading: AtomicU32::new(0), + first: AtomicPtr::new(core::ptr::null_mut()), + last: AtomicPtr::new(core::ptr::null_mut()), + count: AtomicUsize::new(0), + area, + })); + // No panic here, which reads a thread-local: an `argv[0]` holds no NUL. + let name = alloc::ffi::CString::new(name).unwrap_or_default(); + let name: &'static CStr = alloc::boxed::Box::leak(name.into_boxed_c_str()); + let mut first = Loading(list); + first.add(Module::new(name, image, tls)); + core::mem::forget(first); + let Ok(at) = reserve(area.size, area.align) else { + refuse(b"no memory for this program's thread-local storage: it cannot start\n"); + }; + // SAFETY: a fresh reservation of the area's size and alignment, and the + // executable just published as module 1. + let tp = unsafe { place(list, at) }; + if enter(tp).is_err() { + refuse(b"the kernel refused this program's thread pointer: it cannot start\n"); + } +} + +/// Write `line` to slot 2 and exit 127: a program that cannot lay out its +/// thread-local storage has nothing that could report it otherwise. +fn refuse(line: &[u8]) -> ! { + if let Some(stderr) = crate::relocate::stderr() { + let _ = syscall::write(stderr, line); + } + syscall::exit(127) +} + +/// `len` bytes of committed memory aligned to `align`. +fn reserve(len: usize, align: usize) -> Result<*mut u8, SyscallError> { + // SAFETY: placed anywhere, so it replaces nothing. + unsafe { syscall::vm_reserve(len, VmProt::ReadWrite, VmAt::Anywhere { align: align.max(PAGE_SIZE) }) } +} + +/// Lay a thread's area out at `at`, from zeroed memory: the executable's +/// template in, a DTV naming it and the list, and the TCB. Answers the thread +/// pointer, whose tid word [`enter`] writes. +/// +/// # Safety +/// `at` is zeroed, writable, `list.area.size` bytes and aligned to `list.area.align`. +unsafe fn place(list: &'static Modules, at: *mut u8) -> usize { + let area = list.area; + let base = at as usize; + // SAFETY: the caller's; every write is inside the area by its layout. + unsafe { + let exe = &*list.first.load(Ordering::Acquire); + core::ptr::copy_nonoverlapping(exe.tls.template as *const u8, at.add(area.data), exe.tls.filesz); + let dtv = at as *mut Header; + dtv.write(Header { modules: list, capacity: AREA_SLOTS, owned: 0 }); + let slots = at.add(DTV_SLOTS) as *mut usize; + slots.write(base + area.data); + for i in 1..AREA_SLOTS { + slots.add(i).write(UNALLOCATED); + } + let tp = base + area.tp; + if tcb::VARIANT == Variant::II { + (tp as *mut usize).write(tp); + } + ((tp + tcb::DTV) as *mut usize).write(base); + tp + } +} + +/// Name `tp` as the calling thread's thread pointer, and write its tid into +/// the TCB there. A spawned thread's first act. +pub fn enter(tp: usize) -> Result<(), SyscallError> { + let tid = syscall::set_thread_pointer(tp as u64)?; + // SAFETY: the TCB `place` laid out at `tp`, the calling thread's. + unsafe { ((tp + tcb::TID) as *mut u32).write(tid.raw()) }; + Ok(()) +} + +/// A thread's memory: below a [guard](GUARD), its stack, and below that its +/// area. Released whole, by its joiner or by a detached thread's exit. +pub struct ThreadMemory { + pub reservation: *mut u8, + pub reserved: usize, + /// The stack's lowest and one past its highest byte. + pub stack_base: *mut u8, + pub stack_top: *mut u8, + /// What the thread names first ([`enter`]). + pub tp: usize, +} + +/// Reserved pages below a thread's stack, so that running off it faults. +pub const GUARD: usize = PAGE_SIZE; + +impl ThreadMemory { + /// Memory for a thread whose stack is at least `stack` bytes: `None` when + /// it cannot be had. + pub fn reserve(stack: usize) -> Option { + let list = list()?; + let area = list.area; + let area_len = area.size.checked_next_multiple_of(PAGE_SIZE)?; + let stack = stack.max(1).checked_next_multiple_of(PAGE_SIZE)?; + let reserved = area_len.checked_add(GUARD)?.checked_add(stack)?; + // SAFETY: placed anywhere, so it replaces nothing. + let reservation = unsafe { + syscall::vm_reserve(reserved, VmProt::None, VmAt::Anywhere { align: area.align.max(PAGE_SIZE) }) + } + .ok()?; + let stack_base = reservation.wrapping_add(area_len + GUARD); + // SAFETY: the area's pages and the stack's, inside the reservation just made. + let committed = unsafe { + syscall::vm_protect(reservation, area_len, VmProt::ReadWrite) + .and_then(|()| syscall::vm_protect(stack_base, stack, VmProt::ReadWrite)) + }; + if committed.is_err() { + // SAFETY: the reservation just made, which nothing reaches. + let _ = unsafe { syscall::vm_release(reservation, reserved) }; + return None; + } + // SAFETY: the area's pages, fresh and so zeroed, at the reservation's + // aligned start. + let tp = unsafe { place(list, reservation) }; + Some(ThreadMemory { reservation, reserved, stack_base, stack_top: stack_base.wrapping_add(stack), tp }) + } +} + +/// The calling thread's block of module `id`, made from its template the +/// first time this thread asks: `__tls_get_addr`'s slow path, which has +/// nobody to answer a refusal to, so one panics. +pub fn block(id: usize) -> *mut u8 { + let mut dtv = dtv(); + assert!(!dtv.is_null() && id != 0, "tls: block {id} asked of a thread with no thread-local storage"); + // SAFETY: the calling thread's own DTV, which only it writes. + unsafe { + if id > (*dtv).capacity { + dtv = grow(dtv, id); + } + let slot = (dtv as *mut u8).add(DTV_SLOTS + (id - 1) * 8) as *mut usize; + if *slot == UNALLOCATED { + let module = modules().find(|m| m.id == id).unwrap_or_else(|| panic!("tls: no module {id}")); + let tls = module.tls; + let at = reserve(block_len(&tls), tls.align) + .unwrap_or_else(|e| panic!("tls: no memory for module {id}'s block: {e:?}")); + core::ptr::copy_nonoverlapping(tls.template as *const u8, at, tls.filesz); + *slot = at as usize; + } + *slot as *mut u8 + } +} + +/// The bytes a dynamic block of `tls` is reserved as. +fn block_len(tls: &Tls) -> usize { + tls.memsz.max(1).next_multiple_of(PAGE_SIZE) +} + +/// A DTV with room for module `id`, made of `dtv`'s slots, now the calling +/// thread's; `dtv` is released if it had a reservation of its own. +/// +/// # Safety +/// `dtv` is the calling thread's. +unsafe fn grow(dtv: *mut Header, id: usize) -> *mut Header { + // SAFETY: the caller's. + unsafe { + let old = &*dtv; + let capacity = id.max(old.capacity * 2); + let len = (DTV_SLOTS + capacity * 8).next_multiple_of(PAGE_SIZE); + let at = reserve(len, PAGE_SIZE).unwrap_or_else(|e| panic!("tls: no memory for a DTV of {capacity}: {e:?}")); + let new = at as *mut Header; + new.write(Header { modules: old.modules, capacity, owned: len }); + let (from, to) = ((dtv as *const u8).add(DTV_SLOTS) as *const usize, at.add(DTV_SLOTS) as *mut usize); + core::ptr::copy_nonoverlapping(from, to, old.capacity); + for i in old.capacity..capacity { + to.add(i).write(UNALLOCATED); + } + ((tcb::thread_pointer() + tcb::DTV) as *mut *mut Header).write(new); + if old.owned != 0 { + let _ = syscall::vm_release(dtv as *mut u8, old.owned); + } + new + } +} + +/// Release the calling thread's blocks of every module but the executable's, +/// and its DTV if it grew: the thread's last act before it exits, after its +/// destructors. Nothing of a library's thread-locals may be read after it. +pub fn leave() { + let dtv = dtv(); + if dtv.is_null() { + return; + } + // SAFETY: the calling thread's own DTV and blocks, which only it reaches. + unsafe { + let capacity = (*dtv).capacity; + let slots = (dtv as *mut u8).add(DTV_SLOTS) as *mut usize; + for module in modules().skip(1).take_while(|m| m.id <= capacity) { + let slot = slots.add(module.id - 1); + if *slot != UNALLOCATED { + let _ = syscall::vm_release(*slot as *mut u8, block_len(&module.tls)); + *slot = UNALLOCATED; + } + } + let owned = (*dtv).owned; + if owned != 0 { + // Nothing names what is released. + let tp = tcb::thread_pointer(); + ((tp + tcb::DTV) as *mut usize).write(0); + let _ = syscall::vm_release(dtv as *mut u8, owned); + } + } +} diff --git a/tests/corpus/tinycc/206_libc_refusals.c b/tests/corpus/tinycc/206_libc_refusals.c index c2582d490eb..63085cc9efb 100644 --- a/tests/corpus/tinycc/206_libc_refusals.c +++ b/tests/corpus/tinycc/206_libc_refusals.c @@ -82,7 +82,7 @@ int main(void) { said("getrlimit", getrlimit(RLIMIT_STACK, &limit)); said("setrlimit", setrlimit(RLIMIT_CORE, &limit)); said("msync", msync(page, 4096, MS_SYNC)); - said("mprotect executable", mprotect(page, 4096, PROT_READ | PROT_EXEC)); + said("mprotect writable and executable", mprotect(page, 4096, PROT_READ | PROT_WRITE | PROT_EXEC)); said("mprotect off a page", mprotect(page + 1, 4096, PROT_READ)); said("utimes", utimes(FILE_PATH, NULL)); int status = 0; @@ -127,12 +127,12 @@ int main(void) { printf("getpwuid_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); /* mmap: no file, private or shared, at its start or a later page; no - executable page; no empty mapping. */ + page writable and executable at once; no empty mapping. */ said("mmap a file", mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0) == MAP_FAILED ? -1 : 0); said("mmap a file at a page", mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 4096) == MAP_FAILED ? -1 : 0); said("mmap a file shared", mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) == MAP_FAILED ? -1 : 0); said("mmap a file shared at a page", mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 4096) == MAP_FAILED ? -1 : 0); - said("mmap executable", mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); + said("mmap writable and executable", mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); said("mmap nothing", mmap(NULL, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); /* munmap: any run of whole pages, as POSIX unmaps them, and a fixed diff --git a/tests/corpus/tinycc/206_libc_refusals.expect b/tests/corpus/tinycc/206_libc_refusals.expect index 05ec019f581..7ff74a15a1e 100644 --- a/tests/corpus/tinycc/206_libc_refusals.expect +++ b/tests/corpus/tinycc/206_libc_refusals.expect @@ -12,7 +12,7 @@ fstatvfs: -1, ENOSYS getrlimit: -1, ENOSYS setrlimit: -1, ENOSYS msync: -1, ENOSYS -mprotect executable: -1, ENOTSUP +mprotect writable and executable: -1, ENOTSUP mprotect off a page: -1, EINVAL utimes: -1, ENOTSUP wait: -1, ECHILD @@ -39,7 +39,7 @@ mmap a file: -1, ENODEV mmap a file at a page: -1, ENODEV mmap a file shared: -1, ENODEV mmap a file shared at a page: -1, ENODEV -mmap executable: -1, ENOTSUP +mmap writable and executable: -1, ENOTSUP mmap nothing: -1, EINVAL mmap the first of two pages fixed: 0, no errno pages after it: 0 s diff --git a/tests/corpus/tinycc/207_libc_names.c b/tests/corpus/tinycc/207_libc_names.c index b2a04fcb21b..9af68468a8d 100644 --- a/tests/corpus/tinycc/207_libc_names.c +++ b/tests/corpus/tinycc/207_libc_names.c @@ -221,6 +221,12 @@ static void dl(void) { printf("dladdr(tls_get_label + 1): %d, %s, %s at the symbol's address: %s\n", found, found ? info.dli_fname : "-", found && info.dli_sname ? info.dli_sname : "none", found && info.dli_saddr == code ? "yes" : "no"); + + /* The global scope: the first module that exports a name, and none for a + name nobody exports. */ + printf("dlsym(RTLD_DEFAULT, tls_get_label) is the library's: %s; of a name nobody exports: %s\n", + dlsym(RTLD_DEFAULT, "tls_get_label") == code ? "yes" : "no", + dlsym(RTLD_DEFAULT, "no_module_exports_this") ? "found" : "null"); } int main(void) { diff --git a/tests/corpus/tinycc/207_libc_names.expect b/tests/corpus/tinycc/207_libc_names.expect index 8f77a75e67a..310f47a326b 100644 --- a/tests/corpus/tinycc/207_libc_names.expect +++ b/tests/corpus/tinycc/207_libc_names.expect @@ -24,6 +24,7 @@ opendir of nothing: -1, ENOENT dladdr(main): 1; names this program: yes; starts where dl_iterate_phdr says: yes; symbol: none dladdr(a stack address): 0 dladdr(tls_get_label + 1): 1, /system/lib/libtls_lib.so, tls_get_label at the symbol's address: yes +dlsym(RTLD_DEFAULT, tls_get_label) is the library's: yes; of a name nobody exports: null strnlen: 3 2 0 strsignal: Segmentation fault; Unknown signal modf(-3.25): -3 and -0.25 diff --git a/tests/guest/osbench/src/sys/toyos.rs b/tests/guest/osbench/src/sys/toyos.rs index dd74082bd70..a43d1a41204 100644 --- a/tests/guest/osbench/src/sys/toyos.rs +++ b/tests/guest/osbench/src/sys/toyos.rs @@ -35,7 +35,7 @@ pub fn core_of(cpu: u32) -> u32 { pub type Tid = u64; pub fn tid() -> Tid { - u64::from(toyos_abi::current_tid().0) + u64::from(toyos::tls::current_tid().0) } /// The CPUs the calling thread may run on. diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 55d77d60806..12f8eafb46a 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -117,10 +117,11 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core_affinity" version = "0.8.3" -source = "git+https://github.com/ToyOSOrg/core_affinity?branch=toyos-0.8.3#9b2f9bdfef0d77e000496d48689ae3048f7e2283" +source = "git+https://github.com/ToyOSOrg/core_affinity?branch=toyos-0.8.3#61cf0afe562c2757bec723424484baf3cc4a714e" dependencies = [ "libc", "num_cpus", + "toyos", "toyos-abi", "winapi", ] @@ -420,11 +421,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" version = "0.8.9" -source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#da187608c2662c6e251386192ee1297c83d3ebd8" +source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#d38a45d2cb833ef562304723801f4e4aeb21361c" 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 deleted file mode 100644 index 766f3cd3ee4..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_dlopen_ledger.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! A process's libraries are bounded at `MAX_LIBRARIES`: past it a load of a -//! 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"; - -/// Loads attempted: past where a library ledger with no bound outgrows one -/// heap allocation. -const ATTEMPTS: usize = 4 * MAX_LIBRARIES; - -/// 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; - let mut elf = vec![0u8; LEN]; - elf[..8].copy_from_slice(&[0x7f, b'E', b'L', b'F', 2, 1, 1, 0]); - elf[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN - elf[18..20].copy_from_slice(&62u16.to_le_bytes()); // EM_X86_64 - elf[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version - elf[32..40].copy_from_slice(&64u64.to_le_bytes()); // e_phoff - elf[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize - elf[54..56].copy_from_slice(&56u16.to_le_bytes()); // e_phentsize - elf[56..58].copy_from_slice(&1u16.to_le_bytes()); // e_phnum - let ph = &mut elf[64..120]; - ph[0..4].copy_from_slice(&1u32.to_le_bytes()); // PT_LOAD - ph[4..8].copy_from_slice(&5u32.to_le_bytes()); // PF_R | PF_X - ph[32..40].copy_from_slice(&(LEN as u64).to_le_bytes()); // p_filesz - ph[40..48].copy_from_slice(&SPAN.to_le_bytes()); // p_memsz - ph[48..56].copy_from_slice(&0x1000u64.to_le_bytes()); // p_align - elf -} - -/// The libraries this process holds, the executable not among them. -fn libraries() -> usize { - let need = syscall::query_modules(&mut []).expect("the modules' size"); - let mut answer = vec![0u8; need]; - let got = syscall::query_modules(&mut answer).expect("the modules"); - syscall::modules(&answer[..got]).count() - 1 -} - -fn main() { - std::fs::create_dir_all(DIR).unwrap_or_else(|e| panic!("make {DIR}: {e}")); - let lib = format!("{DIR}/lib.so"); - std::fs::write(&lib, image()).unwrap_or_else(|e| panic!("write {lib}: {e}")); - let held = libraries(); - - let mut first = None; - let mut loaded = 0usize; - let mut refusal = None; - for i in 0..ATTEMPTS { - // A name of its own for one file: the ledger is keyed by name. - let name = format!("{DIR}/{i}.so"); - files::symlink(lib.as_bytes(), (files::At::Cwd, name.as_bytes())).unwrap_or_else(|e| panic!("link {name}: {e:?}")); - match files::dl_open(name.as_bytes()) { - Ok(handle) => { - first.get_or_insert((name, handle)); - loaded += 1; - } - Err(e) => { - refusal = Some(e); - break; - } - } - } - let refusal = refusal.unwrap_or_else(|| { - panic!("{loaded} names of one library all loaded: the library count is unbounded") - }); - 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"); - 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_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs index 39903ab4170..7448929e026 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs @@ -2,17 +2,19 @@ //! kernel's own address-space rules forbid. //! //! `abuse_elf_segments` covers the sizes a program header declares. This one -//! covers what the loader *derives* from them: the address a segment lands at -//! once the image is rebased to `USER_VM_BASE`, the TLS block's layout, and -//! what `dlopen` reads of a library's tables. Each case is a kernel panic or +//! covers what the kernel *derives* from them: the address a segment lands at +//! once the image is rebased to `USER_VM_BASE`. Each case is a kernel panic or //! a forbidden mapping before the fix; each must be an error return after it, -//! with the kernel intact. +//! with the kernel intact. A library is the program's own to load (libc's +//! `dlopen`), so a crafted one is that program's to refuse, and its loader's +//! host tests (`toyos-libc-copies`) hold that. //! -//! An executable's dynamic section is the program's own -//! (`toyos::relocate`): the spawn cases crafted in one are answered either way, -//! the kernel lives, and a child the kernel starts dies ([`dies`]). A real std -//! image whose dynamic section names a form its own `_start` refuses exits -//! [`REFUSED_STATUS`] with [`REFUSED_LINE`] ([`refuses_itself`]). +//! An executable's dynamic section and thread-local storage are the program's +//! own (`toyos::relocate`, `toyos::tls`): the spawn cases crafted in them are +//! answered either way, the kernel lives, and a child the kernel starts dies +//! ([`dies`]). A real std image whose dynamic section names what its own +//! `_start` refuses exits [`REFUSED_STATUS`] with the line it says +//! ([`refuses_itself`]). #[path = "../image.rs"] mod image; @@ -307,29 +309,6 @@ fn spawn_dies(name: &str, bytes: &[u8]) { dies(name, spawn_result(name, bytes)); } -/// Load it and throw it away. These cases are about a *walk* the loader does, -/// not about a header it can reject: a library whose `.gnu.hash` never -/// terminates or whose TLS symbol resolves nowhere is still a library, and it -/// may legitimately load with nothing in it. The assertion is that the kernel -/// comes back — which the checks at the end of `main` make, and which the -/// panic each of these used to raise made impossible. -fn dlopen_survives(name: &str, bytes: &[u8]) { - let path = write_file(name, bytes); - drop(unsafe { libloading::Library::new(&path) }); -} - -/// `dlopen` must refuse this file. A success is the defect. -fn dlopen_refused(name: &str, bytes: &[u8]) { - let path = write_file(name, bytes); - match unsafe { libloading::Library::new(&path) } { - Ok(lib) => { - drop(lib); - panic!("{name}: dlopen loaded an image the loader must refuse"); - } - Err(e) => assert!(!format!("{e}").is_empty(), "{name}: dlopen error message"), - } -} - /// A minimal, honest exe: one PT_LOAD covering the whole file at vaddr 0. fn base_exe(size: usize) -> Elf { Elf::new(size).ph(Phdr::load(0, 0, size as u64, size as u64, PF_R | PF_X)).entry(0) @@ -461,23 +440,6 @@ fn main() { )), ); - // 10. A .gnu.hash whose chain array never terminates. The symbol-count - // walk followed `chain[i] & 1` with no bound but the slice's own - // assert, and a zeroed image never sets that bit. - dlopen_survives( - "gnu_hash_runaway.so", - &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 }) - .dynamic(0x1000, &[(DT_GNU_HASH, 0x2000), (DT_STRTAB, 0x3000), (DT_STRSZ, 16)]) - // nbuckets=1, symoffset=0, bloom_size=1, bloom_shift=0, bloom[0], - // buckets[0]=1 — then chains, all zero to the end of the image. - .poke(0x2000, &1u32.to_le_bytes()) - .poke(0x2008, &1u32.to_le_bytes()) - .poke(0x2018, &1u32.to_le_bytes()) - .build(), - ); - // 12. Two relocation tables the ceiling accepted, whose derived index did // not fit: `DT_RELASZ` and `DT_PLTRELSZ` were bounded apart and both // fed one index the kernel built at spawn, 2.7 MiB in a single `Vec`. @@ -526,11 +488,6 @@ fn main() { spawn_dies("too_many_needed_libs", &elf.dynamic(0x1000, &tags).build()); } - // 11. A DTPMOD64 relocation naming a TLS symbol no loaded module defines. - // Its two resolvers panicked where every other unresolved-symbol path - // in the loader logs and carries on. - dlopen_survives("dtpmod_unresolved.so", &so_with_dtpmod()); - // 13. A RELATIVE write beginning in a page's last seven bytes: r_offset // 0x1FFE + 8 crosses 0x2000, which the kernel's per-page applier dropped. spawn_dies( @@ -544,50 +501,28 @@ fn main() { .build(), ); - // 15. `.dynsym`, `.dynstr` and `.rela.dyn` placed inside the module's own - // writable window. The loader holds a `&[u8]` over each across the - // writes, and the window relocations were bounded to began 2 MiB-rounded - // *down* from the first writable byte — so this image loaded, and the - // borrow and the write covered the same bytes. - dlopen_refused("tables_in_write_window.so", &so_with_tables_in_write_window()); - - // 16. The other half of that rounding: a RELATIVE write below the first - // writable byte, which `page_prot` maps `ReadExec` in every process. - dlopen_refused("reloc_below_writable.so", &so_with_reloc_below_writable()); - - // 17. An image whose lowest vaddr is not 0. Every window the loader - // computes is image-relative and every `r_offset` is a vaddr, so the - // two sit `vaddr_min` apart: a relocation validated inside the writable - // window lands that many bytes lower in the image — here inside - // `.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, which the kernel refused by name while it read one. globdat_past_short_dynsym(); - // 20. The apply-time TLS refusals, each named by its reason in the log. - tls_apply_time_refusals_are_reached(); + // 20. Thread-local references an executable's own start refuses. + executable_tls_refs_die(); // 21. A program header table no `PT_LOAD` maps: the one segment's file // bytes start at 0x1000, past the table at 0x40. Its vaddr is 0, so - // `rebase_base` and `load_shared_lib`'s vaddr-0 check pass it and only - // the table refuses it, in `spawn` and in `load_shared_lib` alike. + // `rebase_base` passes it and only the table refuses it. let unmapped = Elf::new(0x2000).ph(Phdr::load(0x1000, 0, 0x1000, 0x1000, PF_R | PF_X)).entry(0).build(); spawn_refused("phdrs_unmapped", &unmapped); - dlopen_refused("phdrs_unmapped.so", &unmapped); - // 22. This program's own image, a library to load in one copy and a `RELR` - // table in another: its `_start` refuses both before it writes a word. - refuses_itself("needs_a_library", DT_NEEDED as u64); - refuses_itself("carries_relr", DT_RELR); + // 22. This program's own image, a `RELR` table in one copy, which its + // `_start` refuses before it writes a word, and a library in another, + // which its start refuses before `main`, since it links no loader. + refuses_itself("carries_relr", DT_RELR, REFUSED_LINE); + refuses_itself("needs_a_library", DT_NEEDED as u64, NO_LIBRARY_LINE); // The kernel heap is intact: allocate and touch enough to walk it, then // prove the real loader still works. @@ -610,116 +545,10 @@ fn main() { println!("crafted ELF derivations rejected, kernel intact"); } -/// A library that loads far enough to reach `apply_dtpmod_relocs`, carrying -/// one DTPMOD64 whose symbol is undefined here and defined nowhere else. -/// -/// Everything in it exists to get past a check: PT_TLS so the module is given -/// a DTV id at all, a writable PT_LOAD so the relocation's `r_offset` is -/// inside the private window the loader validates against, and an SHT_DYNSYM -/// section header so `r_sym` is inside a symbol table. -fn so_with_dtpmod() -> Vec { - Elf::new(0x5000) - .ph(Phdr::load(0, 0, 0x4000, 0x20_0000, PF_R | PF_X)) - .ph(Phdr::load(0x4000, 0x20_0000, 0x1000, 0x20_0000, PF_R | PF_W)) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x100, vaddr: 0x100, filesz: 0, memsz: 8, align: 8 }) - .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) - .sections(0x3800, 1, 64) - .dynamic( - 0x1000, - &[ - (DT_SYMTAB, 0x2000), - (DT_STRTAB, 0x3000), - (DT_STRSZ, 0x100), - (DT_RELA, 0x2800), - (DT_RELASZ, 24), - ], - ) - // sym[1]: undefined (st_shndx == 0), so resolution goes cross-module. - .sym(0x2018, 1, 6 /* STT_TLS */, 0, 0) - .poke(0x3001, b"tls_nowhere\0") - .rela(0x2800, 0x20_0000, (1u64 << 32) | R_X86_64_DTPMOD64, 0) - .shdr(0x3800, SHT_DYNSYM, 0x2000, 48, 24) - .build() -} - -/// Text at `[0, 0x1000)`, writable data at `[0x1000, 0x5000)`, and every table -/// the loader reads inside that writable range. `rw_lo & !(2 MiB - 1)` is 0, so -/// the window the old bound permitted was the whole image. -fn so_with_tables_in_write_window() -> 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: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) - .sections(0x3800, 1, 64) - .dynamic(0x1000, &[ - (DT_SYMTAB, 0x2000), - (DT_STRTAB, 0x3000), - (DT_STRSZ, 0x100), - (DT_RELA, 0x2800), - (DT_RELASZ, 24), - ]) - .sym(0x2018, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) - .poke(0x3001, b"in_the_window\0") - // Inside the writable range either way, so the refusal is the tables'. - .rela(0x2800, 0x1400, R_X86_64_RELATIVE, 0) - .shdr(0x3800, SHT_DYNSYM, 0x2000, 48, 24) - .build() -} - -/// The same shape with its tables in the text segment, and one RELATIVE write -/// at `0x100` — below `rw_lo`, inside what the rounded-down window permitted. -fn so_with_reloc_below_writable() -> 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 }) - .sections(0x900, 1, 64) - .dynamic(0x600, &[ - (DT_SYMTAB, 0x200), - (DT_STRTAB, 0x300), - (DT_STRSZ, 0x100), - (DT_RELA, 0x800), - (DT_RELASZ, 24), - ]) - .sym(0x218, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) - .poke(0x301, b"below_the_window\0") - .rela(0x800, 0x100, R_X86_64_RELATIVE, 0) - .shdr(0x900, SHT_DYNSYM, 0x200, 48, 24) - .build() -} - -/// Text at vaddr `[0x4000, 0x8000)`, data at `[0x8000, 0xa000)`, so the image -/// begins at `vaddr_min = 0x4000` and the image-relative writable window is -/// `[0x4000, 0x6000)`. `.dynsym` is at image `[0x100, 0x130)` — outside that -/// window — and the one RELATIVE entry names vaddr `0x4100`, which is inside -/// it. `ModuleImage::slice` subtracts `vaddr_min`, so the write lands at image -/// `0x100`: inside `.dynsym`, and inside a page mapped `ReadExec`. -fn so_with_non_zero_vaddr_min() -> Vec { - Elf::new(0x6000) - .ph(Phdr::load(0, 0x4000, 0x4000, 0x4000, PF_R | PF_X)) - .ph(Phdr::load(0x4000, 0x8000, 0x2000, 0x2000, PF_R | PF_W)) - .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x600, vaddr: 0x4600, filesz: 0x200, memsz: 0x200, align: 8 }) - .entry(0x4000) - .sections(0x900, 1, 64) - .dynamic(0x600, &[ - (DT_SYMTAB, 0x4100), - (DT_STRTAB, 0x4300), - (DT_STRSZ, 0x100), - (DT_RELA, 0x4800), - (DT_RELASZ, 24), - ]) - .sym(0x118, 1, (STB_GLOBAL << 4) | STT_TLS, 0, 0) - .poke(0x301, b"shifted\0") - .rela(0x800, 0x4100, R_X86_64_RELATIVE, 0) - .shdr(0x900, SHT_DYNSYM, 0x100, 48, 24) - .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 where the kernel reads -/// it, a library's, and dies where it no longer does, an executable's; the -/// kernel living through them is what the checks at the end of `main` assert. +/// Every file-chosen value an executable's start 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`. Each child dies; 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_dies( @@ -746,19 +575,6 @@ fn values_are_bounded_by_the_image() { spawn_dies("export_past_image", &exe.build()); } - dlopen_refused("so_relative_addend_past_image.so", &so_with(&[], &[(0x1400, 0, R_X86_64_RELATIVE, i64::MAX)], None)); - 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"; @@ -791,117 +607,12 @@ fn exe_with(tags: &[(i64, u64)], relas: &[(u64, u64, i64)], tls: Option) -> 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) }; -} - -/// psABI `R_X86_64_TPOFF64` is `S + A - tp`. Two references to one cross-module -/// symbol, addends 0 and `ADDEND`, are differenced so the kernel-side absolute -/// parts cancel and the gap is the addend alone — `ADDEND` kept, 0 dropped. -fn f13_cross_module_addend_is_kept() { - const ADDEND: i64 = 0x140; - 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(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"); - - let read = |name: &[u8]| -> u64 { - let sym = unsafe { lib_refs.get::<*const u64>(name) } - .unwrap_or_else(|e| panic!("dlsym {}: {e}", String::from_utf8_lossy(name))); - let addr: *const u64 = *sym; - unsafe { addr.read() } - }; - let v0 = read(b"probe0"); - let vn = read(b"probeN"); - assert_eq!( - vn.wrapping_sub(v0) as i64, - ADDEND, - "cross-module TPOFF dropped the addend: probe0={v0:#x} probeN={vn:#x}", - ); - drop(lib_refs); - drop(lib_defs); -} - -/// 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, 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] `name`: defined (st_shndx == 1), STT_TLS, offset 8 in the block. - .sym(0x1218, 1, (STB_GLOBAL << 4) | STT_TLS, 1, 8) - .poke(0x1401, name) - .shdr(0x1C00, SHT_DYNSYM, 0x1200, 48, 24) - .build() -} - -/// 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 executable's -/// spawn dies instead, its tables its own. -fn tls_apply_time_refusals_are_reached() { - // dlopen, the module's own symbol: `apply_tpoff_relocs` refuses, and the - // mapping guard takes the library back down. - dlopen_refused("tls_apply_refs.so", &so_tls_ref_past_segment()); - - // spawn, the executable's own symbol, which `apply_tls_relocs` refused. +/// Thread-local references an executable's own start refuses: one to its own +/// symbol whose `S + A` leaves its `PT_TLS`, and one to a library's symbol, +/// which no executable may reach with the initial-exec model. +fn executable_tls_refs_die() { spawn_dies("tls_apply_spawn", &exe_tls_ref_past_segment()); - // dlopen, another module's symbol: `S + A` (8 + 0x140) leaves the defining - // module's 0x20-byte `PT_TLS`. Named apart from f13's `xtls`, which stays - // 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"; // `.dynstr` at 0x1800 holds "wtls\0\0". let exe = exe_with(&[(DT_NEEDED, 6)], &[(0x2000, (1u64 << 32) | R_X86_64_TPOFF64, i64::MAX)], None) @@ -909,11 +620,6 @@ fn tls_apply_time_refusals_are_reached() { .poke(0x1801, b"wtls\0") .poke(0x1806, dep.as_bytes()); spawn_dies("tpoff_overflow_spawn", &exe.build()); - - let defs = write_file("tpoff_overflow_defs.so", &tls_defs_so(b"vtls", PAST_I64)); - let lib_defs = unsafe { libloading::Library::new(&defs) }.expect("dlopen tpoff_overflow_defs.so"); - 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 @@ -937,27 +643,6 @@ fn globdat_past_short_dynsym() { spawn_dies("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 { @@ -977,32 +662,11 @@ fn exe_tls_ref_past_segment() -> Vec { .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(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)) - .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), - (DT_RELA, 0x1600), (DT_RELASZ, 48), - ]) - // 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, 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) - .build() -} - /// What `toyos::relocate` writes to its stderr, `fd:2`, when it refuses its own program. const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; +/// What std's start writes there for a program that names a library and links +/// no loader, as this one links none. +const NO_LIBRARY_LINE: &str = "this program names libraries and links no loader: it cannot start\n"; /// The status it exits with then. const REFUSED_STATUS: i32 = 127; /// The argument a refused copy is handed, which its `main` panics on. @@ -1031,9 +695,9 @@ fn own_image_tagged(tag: u64) -> Vec { } /// A copy of this program tagged `tag` exits [`REFUSED_STATUS`] and says -/// [`REFUSED_LINE`] on the stderr it is endowed. Endowed none, it says -/// nothing and exits the same. -fn refuses_itself(name: &str, tag: u64) { +/// `line` on the stderr it is endowed. Endowed none, it says nothing and +/// exits the same. +fn refuses_itself(name: &str, tag: u64, line: &str) { let path = write_file(name, &own_image_tagged(tag)); let out = Command::new(&path) .arg(REFUSED_COPY) @@ -1041,7 +705,7 @@ fn refuses_itself(name: &str, tag: u64) { .unwrap_or_else(|e| panic!("{name}: spawn: {e}")); let said = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(REFUSED_STATUS), "{name}: exited {:?}, said {said:?}", out.status); - assert_eq!(said, REFUSED_LINE, "{name}"); + assert_eq!(said, line, "{name}"); let child = spawn_argv(&path, &format!("{path}\0{REFUSED_COPY}\0")) .unwrap_or_else(|e| panic!("{name} without a stderr: spawn gave {e:?}")); 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 b9c1ea278c7..5c8060ad514 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs @@ -4,14 +4,15 @@ //! loader, so the spec's `p_filesz <= p_memsz` is memory safety here. Inverted, //! the copy overruns its destination with file-chosen length *and* content: //! -//! - `PT_TLS` on the exe path: `OwnedAlloc::new(tls_memsz)` then -//! `copy_nonoverlapping(.., tls_filesz)` — a kernel heap overflow. -//! - `PT_LOAD` in a `dlopen`ed `.so`: the image is sized from `p_memsz` and -//! each segment is read in at `p_filesz` — a PMM overflow past the image. +//! `PT_TLS` on the exe path was `OwnedAlloc::new(tls_memsz)` then +//! `copy_nonoverlapping(.., tls_filesz)`, a kernel heap overflow, while the +//! kernel laid out thread-local storage; it is the program's now, and the +//! header is still refused at the spawn. //! //! A `p_memsz` no allocator can satisfy, and program-header vaddrs outside the //! loaded image, are the other two ways in. Every case must be an error return, -//! with the kernel intact afterwards. +//! with the kernel intact afterwards. A library's headers are the program's +//! own loader's to refuse (libc's `dlopen`). use std::fs; @@ -25,16 +26,8 @@ const DIR: &str = "/tmp/abuse_elf"; const ET_DYN: u16 = 3; const EM_X86_64: u16 = 62; const PT_LOAD: u32 = 1; -const PT_DYNAMIC: u32 = 2; const PT_TLS: u32 = 7; -const DT_STRTAB: i64 = 5; -const DT_SYMTAB: i64 = 6; -const DT_RELA: i64 = 7; -const DT_RELASZ: i64 = 8; -const DT_STRSZ: i64 = 10; -const DT_GNU_HASH: i64 = 0x6fff_fef5u32 as i32 as i64; - /// One PT_LOAD covering the whole file plus one extra header the caller fills. /// `entry` and the load segment are honest; only the extra header lies. fn elf(extra: Phdr) -> Vec { @@ -64,40 +57,6 @@ fn elf(extra: Phdr) -> Vec { out } -/// One PT_LOAD at vaddr 0x1000 (so `vaddr_min` is non-zero and vaddr 0 is -/// *below* the image) plus a PT_DYNAMIC holding `tags`, DT_NULL-terminated. -/// vaddr V maps to file offset V - 0x1000; the image spans [0x1000, 0x5000). -fn dyn_elf(tags: &[(i64, u64)]) -> Vec { - let mut out = vec![0u8; 0x4000]; - - out[..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']); - out[4] = 2; - out[5] = 1; - out[6] = 1; - out[16..18].copy_from_slice(&ET_DYN.to_le_bytes()); - out[18..20].copy_from_slice(&EM_X86_64.to_le_bytes()); - out[20..24].copy_from_slice(&1u32.to_le_bytes()); - out[24..32].copy_from_slice(&0x1000u64.to_le_bytes()); - out[32..40].copy_from_slice(&64u64.to_le_bytes()); - out[52..54].copy_from_slice(&64u16.to_le_bytes()); - out[54..56].copy_from_slice(&56u16.to_le_bytes()); - out[56..58].copy_from_slice(&2u16.to_le_bytes()); - out[58..60].copy_from_slice(&64u16.to_le_bytes()); - - Phdr { kind: PT_LOAD, flags: 6, offset: 0, vaddr: 0x1000, filesz: 0x4000, memsz: 0x4000, align: 0x1000 } - .write(&mut out[64..120]); - Phdr { kind: PT_DYNAMIC, flags: 6, offset: 0x1000, vaddr: 0x2000, filesz: 0x400, memsz: 0x400, align: 8 } - .write(&mut out[120..176]); - - let mut off = 0x1000; - for &(tag, val) in tags { - out[off..off + 8].copy_from_slice(&tag.to_le_bytes()); - out[off + 8..off + 16].copy_from_slice(&val.to_le_bytes()); - off += 16; - } - out -} - struct Phdr { kind: u32, flags: u32, @@ -160,16 +119,6 @@ fn spawn_as(path: &str, image: &[u8]) -> SyscallError { .unwrap_err() } -/// Same bytes, reached through `dlopen` instead of `spawn`. -fn dlopen_err(name: &str, bytes: &[u8]) -> String { - let path = format!("{DIR}/{name}"); - fs::write(&path, bytes).unwrap_or_else(|e| panic!("write {path}: {e}")); - match unsafe { libloading::Library::new(&path) } { - Ok(_) => panic!("{name}: dlopen succeeded — the header is malformed"), - Err(e) => e.to_string(), - } -} - fn main() { fs::create_dir_all(DIR).expect("create /tmp/abuse_elf"); @@ -208,82 +157,8 @@ fn main() { })); assert_eq!(err, SyscallError::InvalidArgument, "PT_LOAD filesz > memsz"); - // 5. The same lie through `dlopen`, where the segment copy is not clamped: - // `load_shared_lib` sizes the image from p_memsz and reads p_filesz - // bytes into it, overrunning the PMM allocation. - let msg = dlopen_err("load_filesz.so", &elf(Phdr { - kind: PT_LOAD, flags: 6, offset: 0x2000, vaddr: 0x4000, - filesz: 0x2000, memsz: 0x10, align: 0x1000, - })); - assert!(!msg.is_empty(), "dlopen error message"); - - // 6. And a .so whose PT_TLS lies the same way. - let msg = dlopen_err("tls_filesz.so", &elf(Phdr { - kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x2000, - filesz: 0x4000, memsz: 16, align: 8, - })); - assert!(!msg.is_empty(), "dlopen error message"); - - // 7. PT_DYNAMIC whose DT_* tags point outside the loaded image. - // `load_shared_lib` maps each tag's vaddr to an image offset as - // `vaddr - vaddr_min`; below vaddr_min that wraps, and the wrapped - // `offset + size` can wrap back under a slice's own bounds check. - // DT_STRSZ and the implied symbol count also size kernel allocations. - // `dyn_elf` puts the image at [0x1000, 0x5000), so vaddr 0 is below - // vaddr_min; the base tags are honest and each case overrides one. - let base = [(DT_SYMTAB, 0x3000), (DT_STRTAB, 0x3000), (DT_STRSZ, 8)]; - for (name, over) in [ - ("dyn_symtab_low", &[(DT_SYMTAB, 0)][..]), - ("dyn_strtab_low", &[(DT_STRTAB, 0)][..]), - ("dyn_strsz_huge", &[(DT_STRSZ, u64::MAX)][..]), - ("dyn_gnuhash_far", &[(DT_GNU_HASH, 0x7000_0000)][..]), - ("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 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 - // read is `r_sym * 24` into a slice sized from the file's own count. - ("dyn_relsym_oob", &[(DT_RELA, 0x3800), (DT_RELASZ, 24)][..]), - ] { - let tags: Vec<(i64, u64)> = base.iter().chain(over.iter()).copied().collect(); - let mut bytes = dyn_elf(&tags); - // (r_offset, r_info, r_addend) for the cases that plant one entry at - // vaddr 0x3800 — file offset 0x2800. - let entry = match name { - "dyn_reloc_oob" => Some((0x7000_0000u64, 8u64, 0i64)), // R_X86_64_RELATIVE - "dyn_dtpoff_oob" => Some((0x7000_0000, 17, 0x4141_4141_4141_4141u64 as i64)), - "dyn_globdat_oob" => Some((0x7000_0000, 6, 0)), - "dyn_relsym_oob" => Some((0x3000, (0xFFFF_FFFFu64 << 32) | 6, 0)), - _ => None, - }; - if let Some((r_offset, r_info, r_addend)) = entry { - let r = 0x2800; // file offset of vaddr 0x3800 - bytes[r..r + 8].copy_from_slice(&r_offset.to_le_bytes()); - bytes[r + 8..r + 16].copy_from_slice(&r_info.to_le_bytes()); - bytes[r + 16..r + 24].copy_from_slice(&r_addend.to_le_bytes()); - } - let msg = dlopen_err(&format!("{name}.so"), &bytes); - assert!(!msg.is_empty(), "{name}: dlopen error message"); - } - - // 8. e_shentsize = 0 with a section table present: consumers divide a byte - // count by it, and #DE in the kernel is not a survivable fault. A - // well-formed enough library may legitimately load here — the assertion - // is only that the kernel comes back at all. - let mut bytes = dyn_elf(&base); - bytes[40..48].copy_from_slice(&0x1000u64.to_le_bytes()); // e_shoff - bytes[58..60].copy_from_slice(&0u16.to_le_bytes()); // e_shentsize - bytes[60..62].copy_from_slice(&4u16.to_le_bytes()); // e_shnum - let path = format!("{DIR}/shentsize_zero.so"); - fs::write(&path, &bytes).expect("write shentsize_zero.so"); - drop(unsafe { libloading::Library::new(&path) }); - // The kernel heap is intact: allocate and touch enough to walk it, then - // prove spawn and the real loader still work. + // prove spawn still works. let mut blocks: Vec> = Vec::new(); for i in 0..256 { blocks.push(vec![(i % 251) as u8; 4096]); 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 d44c40a9e95..b0564d817ae 100644 --- a/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs +++ b/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs @@ -1,44 +1,24 @@ -//! Two syscalls take an address from userland and hand it to +//! `SYS_FUTEX_WAIT` takes an address from userland and hands it to //! `AddressSpace::translate`, which walked any PML4 index. A user address space //! shallow-copies the kernel's PML4 half, so a kernel address resolved to a -//! present, writable 2 MiB leaf of the direct map. +//! present, writable 2 MiB leaf of the direct map. The word was guarded, but by +//! an expression at the dispatch arm whose value was thrown away, in a +//! different file from the `pub fn` it protected. The bound now lives where it +//! cannot be forgotten — in `translate` itself, and in `futex_wait`'s +//! signature. //! -//! `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 -//! away, in a different file from the `pub fn` it protected. Both bounds now -//! live where they cannot be forgotten — in `translate` itself, and in -//! `futex_wait`'s signature. -//! -//! **The verdict is not the assertion.** A kernel that still made that write -//! would return the same error to a userland that cannot read a byte of the -//! kernel's address space to notice. So the kernel keeps sixteen bytes with a -//! known value and answers two questions about them -//! (`SYS_DEBUG` actions 10 and 11): where they are, and whether they still say -//! what it put there. +//! **The verdict is not the assertion.** A kernel that still read there would +//! return the same error to a userland that cannot read a byte of the kernel's +//! address space to notice. So the kernel keeps sixteen bytes with a known +//! value and answers two questions about them (`SYS_DEBUG` actions 10 and 11): +//! where they are, and whether they still say what it put there. -use toyos_abi::syscall::{self, DlopenArgs, VmAt, VmProt, SyscallError}; +use toyos_abi::syscall::{self, VmAt, VmProt, SyscallError}; use toyos_abi::syscall::debug_action::{CANARY_ADDR, CANARY_CHANGED}; -/// The one the TLS tests load, chosen because it exists in this image and has -/// an `init_array` — so a successful call has something to report. -const LIB: &[u8] = b"/system/lib/libtls_dlopen_lib.so"; - const PAGE: u64 = syscall::PAGE_SIZE as u64; -/// `dl_open` builds its own `init_info` on the stack, so the typed wrapper -/// 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 { - 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 { SyscallError::from_u64(ret) } @@ -56,67 +36,16 @@ fn main() { ); assert!(canary_intact(), "the canary was already changed before the test ran"); - // 1. The defect itself: a kernel address as `init_out`. - let ret = dlopen_raw(LIB, canary); - assert_eq!(err(ret), Some(SyscallError::BadAddress), "dlopen took a kernel address"); - assert!(canary_intact(), "dlopen wrote {canary:#x} — sixteen bytes of kernel memory"); - - // 2. The rest of the kernel half, and the non-canonical hole above the user - // half, which `translate` reaches by the same walk. - for &addr in &[0xFFFF_8000_0000_0000u64, 0x0000_8000_0000_0000, u64::MAX & !7] { - let ret = dlopen_raw(LIB, addr); - assert_eq!(err(ret), Some(SyscallError::BadAddress), "dlopen took {addr:#x}"); - } - assert!(canary_intact(), "the canary changed while other kernel addresses were tried"); - - // 3. A user address off the value's alignment is still refused. // SAFETY: placed anywhere, so it replaces nothing. let region = unsafe { syscall::vm_reserve(2 * PAGE as usize, VmProt::ReadWrite, VmAt::ANYWHERE) }.expect("reserve two pages"); let base = region as u64; - let boundary = base + PAGE; - let ret = dlopen_raw(LIB, base + 4); - assert_eq!(err(ret), Some(SyscallError::BadAddress), "dlopen took {:#x}", base + 4); - - // 3b. A refused dlopen must register nothing: the library loads and maps - // before the copy-out, so a `BadAddress` used to leave a module in the - // process's list. `query_modules`' size is the census and climbs per leak. - let before = syscall::query_modules(&mut []).expect("query_modules size query"); - for _ in 0..8 { - let ret = dlopen_raw(LIB, base + 4); - assert_eq!(err(ret), Some(SyscallError::BadAddress), "a misaligned init_out was not refused"); - } - let after = syscall::query_modules(&mut []).expect("query_modules size query"); - assert_eq!( - after, before, - "refused dlopens grew the module census {before} -> {after}: a BadAddress left modules registered", - ); - - // 4. And the syscall still does its job, which is what none of the above - // may cost, at an `init_out` whose two words lie on two pages. Poisoned - // first, because a library with an empty init_array is written two - // zeros and that is not distinguishable from a write that never - // happened. - const POISON: u64 = 0x_DEAD_BEEF_DEAD_BEEF; - let out = boundary - 8; - let words = out as *mut u64; - unsafe { - words.write_volatile(POISON); - words.add(1).write_volatile(POISON); - } - let ret = dlopen_raw(LIB, out); - assert!(err(ret).is_none(), "dlopen refused an init_out across two pages: {ret:#x}"); - let info = unsafe { [words.read_volatile(), words.add(1).read_volatile()] }; - assert!( - info[0] != POISON && info[1] != POISON, - "dlopen returned a handle and wrote nothing to init_out: {info:#x?}", - ); - // 5. The futex word, which the *scheduler* dereferences on every wake check - // long after the syscall returned. A kernel address here is a read - // oracle: `futex_wait` blocks when the word equals `expected` and returns - // at once when it does not, so a caller that may name kernel memory can - // ask whether a kernel word holds a value — one bit per call, from - // timing alone. Neither answer is what an address it may not name gets. + // The futex word, which the *scheduler* dereferences on every wake check + // long after the syscall returned. A kernel address here is a read + // oracle: `futex_wait` blocks when the word equals `expected` and returns + // at once when it does not, so a caller that may name kernel memory can + // ask whether a kernel word holds a value — one bit per call, from + // timing alone. Neither answer is what an address it may not name gets. for &expected in &[0u32, 0x0F17_1E55, 0x1A55] { let ret = unsafe { syscall::futex_wait(canary as *const u32, expected, Some(50_000_000)) @@ -147,5 +76,5 @@ fn main() { unsafe { syscall::vm_release(region, 2 * PAGE as usize) }.expect("release"); assert!(canary_intact(), "the canary changed during the run"); - println!("dlopen and futex refuse an address they may not reach, and kernel memory is untouched"); + println!("futex refuses an address it may not reach, and kernel memory is untouched"); } diff --git a/tests/guest/rust-tests/src/bin/abuse_tls_alloc.rs b/tests/guest/rust-tests/src/bin/abuse_tls_alloc.rs deleted file mode 100644 index 5db11a2c80d..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_tls_alloc.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! `SYS_TLS_ALLOC_BLOCK` must hand a thread the same TLS block every time it -//! asks for one, and must take a thread's blocks out of the address space -//! before it gives their pages back. -//! -//! Both properties are memory safety, not bookkeeping: the block's virtual -//! address is returned to userland, so a mapping that outlives its pages is a -//! user-writable window onto whatever the PMM hands out next. - -use std::sync::atomic::{AtomicU64, Ordering}; - -use toyos_abi::syscall::{self, VmAt, VmProt, SyscallError}; - -const PAGE_2M: usize = 2 * 1024 * 1024; - -/// The executable's own TLS module. Its DTV entry is filled at thread -/// creation, so nothing in the process reaches this syscall for it — which is -/// what makes it safe to drive directly. -const MODULE_EXE: u64 = 1; - -/// The kernel's fixed DTV capacity (`loader::DTV_INITIAL_CAPACITY`). -const DTV_CAPACITY: u64 = 64; - -/// This thread's DTV, from the TCB at `fs:[8]`. -fn dtv_entry(module_id: u64) -> *mut u64 { - let p: u64; - unsafe { core::arch::asm!("mov {}, fs:[8]", out(reg) p, options(nostack, readonly)) }; - unsafe { (p as *mut u64).add(2 + (module_id - 1) as usize) } -} - -/// Allocate a block for `module_id`, leaving the DTV as it was found. The -/// syscall points the entry at the new block, and this thread's own -/// `#[thread_local]`s — the panic machinery and TLS destructors included — -/// resolve through it. -fn alloc_block_preserving_dtv(module_id: u64) -> Result { - let slot = dtv_entry(module_id); - let saved = unsafe { *slot }; - let r = syscall::tls_alloc_block(module_id); - unsafe { *slot = saved }; - r -} - -fn map_2m() -> *mut u8 { - let p = unsafe { - syscall::vm_reserve(PAGE_2M, VmProt::ReadWrite, VmAt::ANYWHERE).unwrap_or(core::ptr::null_mut()) - }; - assert!(!p.is_null(), "mmap failed"); - p -} - -static THREAD_BLOCK: AtomicU64 = AtomicU64::new(0); - -/// Raw thread body, run on one stack this test maps and reuses, so the -/// address space holds still under what part 3 measures; std's -/// `thread::spawn` allocates each thread a stack of its own. -extern "C" fn thread_body(_arg: u64) { - let r = alloc_block_preserving_dtv(MODULE_EXE); - THREAD_BLOCK.store(r.unwrap_or(0), Ordering::Release); - syscall::thread_exit(0); -} - -/// Run `thread_body` on `stack` and wait for it. Returns the block it got. -fn run_thread(stack: *mut u8) -> u64 { - THREAD_BLOCK.store(0, Ordering::Release); - let top = stack as u64 + PAGE_2M as u64; - let tid = unsafe { - syscall::thread_spawn(thread_body as *const () as u64, top, 0, stack as u64) - }; - assert!(SyscallError::from_u64(tid).is_none(), "thread_spawn failed"); - syscall::thread_join(tid); - let block = THREAD_BLOCK.load(Ordering::Acquire); - assert!(block != 0, "thread failed to allocate a TLS block"); - block -} - -fn main() { - // 1. Every module id the process does not have is an error return, never a - // panic: zero, one past the DTV's capacity, a saturated one, and one - // inside the capacity that no module claims. - assert_eq!( - syscall::tls_alloc_block(0).unwrap_err(), - SyscallError::InvalidArgument, - "module_id 0", - ); - assert_eq!( - syscall::tls_alloc_block(DTV_CAPACITY).unwrap_err(), - SyscallError::InvalidArgument, - "module id inside the DTV but not in the module list", - ); - assert_eq!( - syscall::tls_alloc_block(DTV_CAPACITY + 1).unwrap_err(), - SyscallError::ResourceExhausted, - "module id past the DTV's capacity", - ); - assert_eq!( - syscall::tls_alloc_block(u64::MAX).unwrap_err(), - SyscallError::ResourceExhausted, - "saturated module id", - ); - - // 2. Asking twice returns the same block, and the first block's pages are - // still ours afterwards. A second block would have freed them while the - // first mapping stayed live and writable, so the loop below would read - // back whatever the new owner of those pages wrote. - const MINE: u8 = 0x5A; - const THEIRS: u8 = 0xC3; - let first = alloc_block_preserving_dtv(MODULE_EXE).expect("first alloc"); - let block: *mut u8 = core::ptr::with_exposed_provenance_mut(first as usize); - // The block is whole pages of the module's TLS; its first is enough. - unsafe { block.write_volatile(MINE) }; - - let second = alloc_block_preserving_dtv(MODULE_EXE).expect("second alloc"); - assert_eq!( - first, second, - "a repeat call handed out a second block ({first:#x} then {second:#x})", - ); - - let claimed: Vec<*mut u8> = (0..16).map(|_| map_2m()).collect(); - for p in &claimed { - for off in (0..PAGE_2M).step_by(4096) { - unsafe { p.add(off).write_volatile(THEIRS) }; - } - } - assert_eq!(unsafe { block.read_volatile() }, MINE, "TLS block at {first:#x} was reissued to another mapping"); - for p in claimed { - unsafe { syscall::vm_release(p, PAGE_2M) }.expect("munmap"); - } - - // 3. A thread's mappings come back when it exits. Each run gets a fresh - // tid and so its own block; the kernel's placement is deterministic, so if every region a run creates — the thread's own TLS - // block and the one it allocated — is released, every run must land on - // the same pair of addresses. A leak pushes each run below the last. - let stack = map_2m(); - let runs: [u64; 4] = core::array::from_fn(|_| run_thread(stack)); - assert!( - runs.iter().all(|a| *a == runs[0]), - "a thread's TLS mappings outlived it: per-run blocks landed at {runs:#x?}", - ); - - println!("TLS block allocation is idempotent and released at thread exit"); -} diff --git a/tests/guest/rust-tests/src/bin/affinity_pins.rs b/tests/guest/rust-tests/src/bin/affinity_pins.rs index 8a0efd29721..daa1d926173 100644 --- a/tests/guest/rust-tests/src/bin/affinity_pins.rs +++ b/tests/guest/rust-tests/src/bin/affinity_pins.rs @@ -62,7 +62,7 @@ fn main() { mask[cpu as usize / 64] = 1 << (cpu % 64); mask }; - assert_eq!(toyos_abi::current_tid().0, 0, "the main thread is tid 0"); + assert_eq!(toyos::tls::current_tid().0, 0, "the main thread is tid 0"); thread::spawn(move || syscall::thread_set_affinity(0, &words(first)).expect("pin the main thread by its tid")) .join() .expect("the pinning thread panicked"); diff --git a/tests/guest/rust-tests/src/bin/dlopen_dedup.rs b/tests/guest/rust-tests/src/bin/dlopen_dedup.rs index 9416a950d7f..85b1c7c250e 100644 --- a/tests/guest/rust-tests/src/bin/dlopen_dedup.rs +++ b/tests/guest/rust-tests/src/bin/dlopen_dedup.rs @@ -1,19 +1,38 @@ //! A repeat `dlopen` of one library shares that module rather than mapping a -//! new copy — `SYS_DLOPEN` used to grow the address space without bound. Read +//! new copy — the kernel's loader used to grow the address space without bound. Read //! 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). The SDK's -//! `dl_open` and raw `dl_sym`, not `libloading`, so the handle integer is -//! observable. +//! module, one TLS block; a fresh second module would read zero). libc's +//! `dlopen` and `dlsym`, not `libloading`, so the handle is observable. +use std::ffi::{c_char, c_void, CStr}; use std::sync::{Arc, Barrier}; -use toyos_abi::syscall; +// The C library's loader. +#[link(name = "toyos_c", kind = "static")] +unsafe extern "C" { + fn dlopen(path: *const c_char, flags: i32) -> *mut c_void; + fn dlsym(handle: *mut c_void, name: *const c_char) -> *mut c_void; +} -const LIB: &[u8] = b"/system/lib/libtls_dlopen_lib.so"; +const LIB: &CStr = c"/system/lib/libtls_dlopen_lib.so"; /// A name this binary loads nowhere else, so the concurrent arm starts clean. -const OTHER_LIB: &[u8] = b"/system/lib/libtls_multi_crate.so"; +const OTHER_LIB: &CStr = c"/system/lib/libtls_multi_crate.so"; + +/// libc's handle for `path`, as an integer so it can be compared and sent. +fn open(path: &CStr) -> Option { + // SAFETY: a NUL-terminated path. + let handle = unsafe { dlopen(path.as_ptr(), 2) }; + (!handle.is_null()).then_some(handle as usize) +} + +/// The address `handle`'s module gives `name`. +fn sym(handle: usize, name: &CStr) -> Option { + // SAFETY: a handle `dlopen` answered, and a NUL-terminated name. + let at = unsafe { dlsym(handle as *mut c_void, name.as_ptr()) }; + (!at.is_null()).then_some(at as usize) +} const LOADS: usize = 64; fn main() { @@ -24,9 +43,9 @@ fn main() { } /// The mechanism: repeated loads of one name are one handle. -fn handle_identity() -> Vec { - let handles: Vec = (0..LOADS) - .map(|i| toyos::files::dl_open(LIB).unwrap_or_else(|e| panic!("dlopen #{i}: {e:?}"))) +fn handle_identity() -> Vec { + let handles: Vec = (0..LOADS) + .map(|i| open(LIB).unwrap_or_else(|| panic!("dlopen #{i}"))) .collect(); assert!( @@ -40,21 +59,18 @@ fn handle_identity() -> Vec { /// The ELF/POSIX same-object guarantee: two handles to one name share the /// module's TLS. Written through `first`, read through `last`. -fn object_identity(first: u64, last: u64) { +fn object_identity(first: usize, last: usize) { // Same symbol, same address: a second, cheaper witness of the one mapping. - let get_via_first = - unsafe { syscall::dl_sym(first, b"dl_tls_get_a") }.expect("dl_tls_get_a via first"); - let get_via_last = - unsafe { syscall::dl_sym(last, b"dl_tls_get_a") }.expect("dl_tls_get_a via last"); + let get_via_first = sym(first, c"dl_tls_get_a").expect("dl_tls_get_a via first"); + let get_via_last = sym(last, c"dl_tls_get_a").expect("dl_tls_get_a via last"); assert_eq!( get_via_first, get_via_last, "the same symbol resolved to two addresses — the library is mapped twice" ); - let inc = - unsafe { syscall::dl_sym(first, b"dl_tls_increment_a") }.expect("dl_tls_increment_a"); - let inc: extern "C" fn() -> u64 = unsafe { core::mem::transmute(inc as usize) }; - let get_last: extern "C" fn() -> u64 = unsafe { core::mem::transmute(get_via_last as usize) }; + let inc = sym(first, c"dl_tls_increment_a").expect("dl_tls_increment_a"); + let inc: extern "C" fn() -> u64 = unsafe { core::mem::transmute(inc) }; + let get_last: extern "C" fn() -> u64 = unsafe { core::mem::transmute(get_via_last) }; inc(); inc(); @@ -81,11 +97,11 @@ fn one_name_under_contention() { let line = Arc::clone(&line); std::thread::spawn(move || { line.wait(); - toyos::files::dl_open(OTHER_LIB).unwrap_or_else(|e| panic!("racer {i}: {e:?}")) + open(OTHER_LIB).unwrap_or_else(|| panic!("racer {i}")) }) }) .collect(); - let handles: Vec = racers.into_iter().map(|t| t.join().expect("a racer")).collect(); + let handles: Vec = racers.into_iter().map(|t| t.join().expect("a racer")).collect(); assert!( handles.iter().all(|&h| h == handles[0]), "{RACERS} threads loading one name concurrently got {handles:?} — the dedup holds \ diff --git a/tests/guest/rust-tests/src/bin/mmap_stress.rs b/tests/guest/rust-tests/src/bin/mmap_stress.rs index d8fac187da8..b81e8edd399 100644 --- a/tests/guest/rust-tests/src/bin/mmap_stress.rs +++ b/tests/guest/rust-tests/src/bin/mmap_stress.rs @@ -142,12 +142,14 @@ fn protection() { unsafe { syscall::vm_protect(three.add(PAGE), PAGE, VmProt::None) }.expect("make the page unreachable"); unsafe { syscall::vm_protect(three.add(PAGE), PAGE, VmProt::ReadWrite) }.expect("make the page writable again"); assert_eq!(byte(three, 1), 0x31, "None and back lost the page's byte"); + // And executable, as a loader protects the code it wrote. + unsafe { syscall::vm_protect(three.add(PAGE), PAGE, VmProt::ReadExec) }.expect("make the page executable"); + assert_eq!(byte(three, 1), 0x31, "executable lost the page's byte"); + unsafe { syscall::vm_protect(three.add(PAGE), PAGE, VmProt::ReadWrite) }.expect("make the page writable again"); - // What protect refuses, all or nothing: a page no range holds, and - // executable memory, which no call maps yet. + // What protect refuses, all or nothing: a page no range holds. unsafe { syscall::vm_release(three.add(2 * PAGE), PAGE) }.expect("release the last page"); assert_eq!(unsafe { syscall::vm_protect(three, 3 * PAGE, VmProt::Read) }, Err(SyscallError::BadAddress), "protect over a hole"); - assert_eq!(unsafe { syscall::vm_protect(three, PAGE, VmProt::ReadExec) }, Err(SyscallError::NotSupported), "protect executable"); set(three, 1, 0x32); assert_eq!(byte(three, 1), 0x32, "a refused protect changed the committed page"); unsafe { syscall::vm_release(three, 2 * PAGE) }.expect("release the rest"); diff --git a/tests/guest/rust-tests/src/bin/query_modules_size.rs b/tests/guest/rust-tests/src/bin/query_modules_size.rs deleted file mode 100644 index a8dc4a85e0a..00000000000 --- a/tests/guest/rust-tests/src/bin/query_modules_size.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! `SYS_QUERY_MODULES` has to be able to say how big an answer is. -//! -//! Its ABI doc claimed a too-small buffer came back as -//! `Err(InvalidArgument)` "with the required buffer size encoded". Nothing -//! encoded anything: `SyscallError` is a fixed set of codes, the handler -//! returned a bare one, and a caller had no way to size a retry and no way to -//! learn that was why it failed. A doc comment is a claim to verify. -//! -//! The contract now: the return is the length in bytes either way, and -//! nothing is written unless all of it fits. - -use core::mem::size_of; - -use toyos_abi::syscall::{self, ModuleInfo}; - -/// Not zero, so a handler that wrote a prefix would be caught doing it. -const POISON: u8 = 0xCD; - -fn main() { - let mut empty: [u8; 0] = []; - let need = syscall::query_modules(&mut empty).expect("an empty buffer is a size query"); - assert!( - need >= size_of::(), - "the required size is {need} bytes, which is less than one record" - ); - println!(" PASS: an empty buffer reports {need} bytes required"); - - // One byte short. The answer must still be the size, and the buffer must - // come back exactly as it went in — a partial description is worse than - // none, because every offset in it points into a record that is not there. - let mut short = vec![POISON; need - 1]; - let again = syscall::query_modules(&mut short).expect("a short buffer is a size query too"); - assert_eq!(again, need, "a short buffer reported {again}, not the required {need}"); - assert!( - short.iter().all(|&b| b == POISON), - "a buffer one byte too small was written to" - ); - println!(" PASS: one byte short reports {need} and writes nothing"); - - let mut buf = vec![POISON; need]; - let got = syscall::query_modules(&mut buf).expect("a buffer of the reported size"); - assert_eq!(got, need, "the exact buffer reported {got}, not {need}"); - - // The records end where the first path begins, which is what makes the - // buffer self-describing without a count in the return value. - let exe = unsafe { (buf.as_ptr() as *const ModuleInfo).read_unaligned() }; - let records = exe.path_offset as usize; - assert_eq!( - records % size_of::(), - 0, - "the record array is {records} bytes, not a whole number of records" - ); - let count = records / size_of::(); - assert!(count >= 1, "no modules at all"); - assert!(exe.base > 0, "the executable has no load base"); - - let start = exe.path_offset as usize; - let path = core::str::from_utf8(&buf[start..start + exe.path_len as usize]) - .expect("the executable's path is not UTF-8"); - assert!( - path.contains("query_modules_size"), - "the first module is {path:?}, which is not this program" - ); - println!(" PASS: {count} module(s), this one at {path}"); - - println!("all query_modules sizing tests passed"); -} diff --git a/tests/guest/rust-tests/src/bin/std_threads.rs b/tests/guest/rust-tests/src/bin/std_threads.rs index 3b01d2a6785..3557f1c0535 100644 --- a/tests/guest/rust-tests/src/bin/std_threads.rs +++ b/tests/guest/rust-tests/src/bin/std_threads.rs @@ -153,5 +153,5 @@ fn main() { /// The calling thread's id, as the affinity calls name it. fn own() -> u64 { - u64::from(toyos_abi::current_tid().0) + u64::from(toyos::tls::current_tid().0) } diff --git a/tests/guest/rust-tests/src/bin/std_tls.rs b/tests/guest/rust-tests/src/bin/std_tls.rs new file mode 100644 index 00000000000..0a1b0e4021b --- /dev/null +++ b/tests/guest/rust-tests/src/bin/std_tls.rs @@ -0,0 +1,75 @@ +//! An executable that links a library at load time (`DT_NEEDED`): its start +//! loads `libtls_lib.so` and binds its calls to it before `main` +//! (`__toyos_load_needed`), the executable's own thread-locals sit where its +//! linker put them, and the library's are a block of their own, reached +//! through the thread's DTV. + +use std::cell::Cell; + +// The C library's loader, which this program's start runs: a Rust program +// that links a library at load time links it (`__toyos_load_needed`). +#[link(name = "toyos_c", kind = "static")] +unsafe extern "C" { + fn __toyos_load_needed(header: *const u8); +} +#[used] +static LOADER: unsafe extern "C" fn(*const u8) = __toyos_load_needed; + +// libtls_lib.so's, which this executable names in a `DT_NEEDED`. +#[link(name = "tls_lib")] +extern "C" { + fn tls_increment() -> u64; + fn tls_get_counter() -> u64; + fn tls_get_label() -> u64; + fn tls_set_label(val: u64); +} + +// 64-byte aligned by its own type, so the exe's `PT_TLS` declares at least that +// whatever the linker would pick, and `LOCAL_VALUE`'s address is on 64 exactly +// when the loader placed the exe's module on its declared alignment. +#[repr(align(64))] +struct Aligned(Cell); + +thread_local! { + static LOCAL_VALUE: Aligned = const { Aligned(Cell::new(42)) }; +} + +fn main() { + // Test 0: the exe's TLS module begins on its declared 64-byte p_align. + LOCAL_VALUE.with(|v| { + let addr = v as *const Aligned as usize; + assert_eq!(addr % 64, 0, "exe TLS module base off its declared 64-byte alignment: {addr:#x}"); + }); + println!("PASS: exe TLS honours its declared alignment"); + + // Test 1: exe-local thread_local works + LOCAL_VALUE.with(|v| { + assert_eq!(v.0.get(), 42, "exe TLS initial value"); + v.0.set(100); + assert_eq!(v.0.get(), 100, "exe TLS after set"); + }); + println!("PASS: exe thread_local"); + + // Test 2: shared library TLS works + unsafe { + assert_eq!(tls_get_counter(), 0, "lib TLS initial counter"); + assert_eq!(tls_increment(), 1, "lib TLS first increment"); + assert_eq!(tls_increment(), 2, "lib TLS second increment"); + assert_eq!(tls_get_counter(), 2, "lib TLS counter after increments"); + } + println!("PASS: lib thread_local counter"); + + // Test 3: shared library TLS doesn't alias exe TLS + unsafe { + assert_eq!(tls_get_label(), 0xDEAD_BEEF, "lib TLS initial label"); + tls_set_label(0xCAFE_BABE); + assert_eq!(tls_get_label(), 0xCAFE_BABE, "lib TLS label after set"); + } + // Verify exe TLS wasn't corrupted + LOCAL_VALUE.with(|v| { + assert_eq!(v.0.get(), 100, "exe TLS not corrupted by lib TLS ops"); + }); + println!("PASS: TLS isolation between exe and lib"); + + println!("all TLS tests passed"); +} diff --git a/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs b/tests/guest/rust-tests/src/bin/std_tls_multi_crate.rs index 0b4f3f17ac9..cd9be194620 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 @@ -1,15 +1,21 @@ +use std::ffi::{c_char, c_void, CStr}; use std::sync::{Arc, Barrier}; -use toyos_abi::syscall; +// The C library's loader. +#[link(name = "toyos_c", kind = "static")] +unsafe extern "C" { + fn dlopen(path: *const c_char, flags: i32) -> *mut c_void; + fn dlsym(handle: *mut c_void, name: *const c_char) -> *mut c_void; +} /// Three TLS-bearing names no earlier line of this binary holds, so every racer /// runs the whole load rather than returning the dedup lookup's answer. The /// third is the one the rest of this binary exercises: the checks below read /// the first two, and every test after them reads that one. -const RACED: [&[u8]; 3] = [ - b"/system/lib/libtls_lib.so", - b"/system/lib/libtls_dlopen_lib.so", - b"/system/lib/libtls_multi_crate.so", +const RACED: [&CStr; 3] = [ + c"/system/lib/libtls_lib.so", + c"/system/lib/libtls_dlopen_lib.so", + c"/system/lib/libtls_multi_crate.so", ]; fn main() { @@ -87,10 +93,11 @@ fn main() { println!("all multi-crate TLS tests passed"); } -fn sym(handle: u64, name: &[u8]) -> usize { - let addr = unsafe { syscall::dl_sym(handle, name) } - .unwrap_or_else(|e| panic!("dl_sym {}: {e:?}", String::from_utf8_lossy(name))); - addr as usize +fn sym(handle: usize, name: &CStr) -> usize { + // SAFETY: a handle `dlopen` answered, and a NUL-terminated name. + let at = unsafe { dlsym(handle as *mut c_void, name.as_ptr()) }; + assert!(!at.is_null(), "dlsym {name:?}"); + at as usize } /// Two threads loading two *different* TLS-bearing names at once hold two @@ -101,34 +108,37 @@ fn two_names_at_once_are_two_modules() { let racers: Vec<_> = RACED .iter() .map(|name| { - let name: &'static [u8] = name; + let name: &'static CStr = name; let line = Arc::clone(&line); std::thread::spawn(move || { // A first load is a disk read, so the barrier alone would race // the block queue rather than the two loads. - let path = core::str::from_utf8(name).expect("a library path"); + let path = name.to_str().expect("a library path"); std::fs::read(path).unwrap_or_else(|e| panic!("warm {path}: {e}")); line.wait(); - toyos::files::dl_open(name).unwrap_or_else(|e| panic!("dlopen {path}: {e:?}")) + // SAFETY: a NUL-terminated path. + let handle = unsafe { dlopen(name.as_ptr(), 2) }; + assert!(!handle.is_null(), "dlopen {path}"); + handle as usize }) }) .collect(); - let handles: Vec = racers.into_iter().map(|t| t.join().expect("a racer")).collect(); + let handles: Vec = racers.into_iter().map(|t| t.join().expect("a racer")).collect(); let a_label: extern "C" fn() -> u64 = - unsafe { core::mem::transmute(sym(handles[0], b"tls_get_label")) }; + unsafe { core::mem::transmute(sym(handles[0], c"tls_get_label")) }; let a_set_label: extern "C" fn(u64) = - unsafe { core::mem::transmute(sym(handles[0], b"tls_set_label")) }; + unsafe { core::mem::transmute(sym(handles[0], c"tls_set_label")) }; let a_inc: extern "C" fn() -> u64 = - unsafe { core::mem::transmute(sym(handles[0], b"tls_increment")) }; + unsafe { core::mem::transmute(sym(handles[0], c"tls_increment")) }; let b_label: extern "C" fn() -> u64 = - unsafe { core::mem::transmute(sym(handles[1], b"dl_tls_get_label")) }; + unsafe { core::mem::transmute(sym(handles[1], c"dl_tls_get_label")) }; let b_set_label: extern "C" fn(u64) = - unsafe { core::mem::transmute(sym(handles[1], b"dl_tls_set_label")) }; + unsafe { core::mem::transmute(sym(handles[1], c"dl_tls_set_label")) }; let b_counter: extern "C" fn() -> u64 = - unsafe { core::mem::transmute(sym(handles[1], b"dl_tls_get_a")) }; + unsafe { core::mem::transmute(sym(handles[1], c"dl_tls_get_a")) }; let b_buffer: extern "C" fn() -> u64 = - unsafe { core::mem::transmute(sym(handles[1], b"dl_tls_check_buffer")) }; + unsafe { core::mem::transmute(sym(handles[1], c"dl_tls_check_buffer")) }; // Read before anything writes: a block sized and templated from the other // module answers with the other module's initialisers. diff --git a/tests/guest/rust-tests/src/bin/timing_protection.rs b/tests/guest/rust-tests/src/bin/timing_protection.rs index 9c432086cee..cd673fc6fe0 100644 --- a/tests/guest/rust-tests/src/bin/timing_protection.rs +++ b/tests/guest/rust-tests/src/bin/timing_protection.rs @@ -143,7 +143,7 @@ fn each_threads_own(supported: bool) -> (usize, usize) { let protected = Arc::clone(&protected); std::thread::spawn(move || { let wants = i % 2 == 0; - protected.lock().unwrap().insert(toyos_abi::current_tid().raw(), wants); + protected.lock().unwrap().insert(toyos::tls::current_tid().raw(), wants); let protection = wants.then(toyos::timing::protect); for round in 0..ROUNDS { let begun = Instant::now(); diff --git a/tests/guest/rust-tests/src/bin/wake_onto_busy.rs b/tests/guest/rust-tests/src/bin/wake_onto_busy.rs index 33153b43daf..4b4d9587974 100644 --- a/tests/guest/rust-tests/src/bin/wake_onto_busy.rs +++ b/tests/guest/rust-tests/src/bin/wake_onto_busy.rs @@ -116,7 +116,7 @@ fn partner(busy: u32) -> (JoinHandle<()>, Arc) { let thread = { let seat = seat.clone(); thread::spawn(move || { - let me = toyos_abi::current_tid().0; + let me = toyos::tls::current_tid().0; pin(u64::from(me), busy); assert_eq!(syscall::current_cpu(), busy, "a partner pinned to CPU {busy}"); seat.tid.store(me, Ordering::Release); @@ -143,7 +143,7 @@ impl Spinner { let thread = { let (stop, started) = (stop.clone(), started.clone()); thread::spawn(move || { - pin(u64::from(toyos_abi::current_tid().0), busy); + pin(u64::from(toyos::tls::current_tid().0), busy); assert_eq!(syscall::current_cpu(), busy, "a spinner pinned to CPU {busy}"); started.store(true, Ordering::Release); let mut x = 1u64; diff --git a/tests/guest/rust-tests/tls-lib/Cargo.toml b/tests/guest/rust-tests/tls-lib/Cargo.toml index 5cf1f69e83d..54335112643 100644 --- a/tests/guest/rust-tests/tls-lib/Cargo.toml +++ b/tests/guest/rust-tests/tls-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tls-lib" -description = "A guest test fixture: a cdylib of thread-locals that std_tls_multi_crate and the C corpus load with dlopen." +description = "A guest test fixture: a cdylib of thread-locals that std_tls links through DT_NEEDED, and std_tls_multi_crate and the C corpus load with dlopen." version = "0.1.0" edition = "2021" diff --git a/tests/toyos.rs b/tests/toyos.rs index 41e6e9c5553..aa7e07041a9 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -319,13 +319,13 @@ const RUST_SKIP: &[&str] = &[ ]; /// The shared boot's last members, in this order: each fills a bound of its -/// own process — thousands of threads, a thousand library images — and its +/// own process — thousands of threads — and its /// exit gives all of it back. Behind every /// discovered member and every corpus case, so nothing but the reboot runs /// after one of these. **This is their one declaration**: /// [`discover_rust_tests`] leaves them to it, so a name taken off this list is /// a discovered member again. -const LAST_MEMBERS: &[&str] = &["abuse_thread_table", "abuse_dlopen_ledger"]; +const LAST_MEMBERS: &[&str] = &["abuse_thread_table"]; /// Binaries a metal row or a guest test drives that the shared boot also runs /// on purpose. From 96cbf3838e8e6d1baa6a2d5023f1d3351fc6d4ee Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 11:49:28 +0100 Subject: [PATCH 02/13] Host CI's findings: no mem::forget, a stale auto-trait row, libloading unbundled toyos::tls's start publishes the executable through the list's one writer rather than forgetting a Loading guard; libc's loader marks a library's memory published rather than forgetting its reservation. sourcegate's Send/Sync registry loses kernel/src/mm/region.rs, which is gone. The layout tests read alignment with is_multiple_of. libloading's fork names libtoyos_c unbundled (fbfc60c): a library build does not search the sysroot the final link does. Co-Authored-By: Claude Opus 5.5 --- build/sourcegate.rs | 1 - sdk/libc/src/dl.rs | 13 +++++++----- sdk/toyos/src/tls/layout.rs | 6 +++--- sdk/toyos/src/tls/mod.rs | 34 +++++++++++++++++-------------- tests/guest/rust-tests/Cargo.lock | 2 +- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/build/sourcegate.rs b/build/sourcegate.rs index c360993b3af..31670a31916 100644 --- a/build/sourcegate.rs +++ b/build/sourcegate.rs @@ -138,7 +138,6 @@ const AUTO_TRAIT_IMPLS: &[(&str, usize)] = &[ ("kernel/src/drivers/panic_console/mod.rs", 3), ("kernel/src/arch/x86_64/hw.rs", 1), ("kernel/src/mm/mmio.rs", 2), - ("kernel/src/mm/region.rs", 2), ("kernel/src/object/paged.rs", 1), ("kernel/src/process.rs", 1), ("kernel/src/sched/driver.rs", 2), diff --git a/sdk/libc/src/dl.rs b/sdk/libc/src/dl.rs index 09703d57f72..582623b4c57 100644 --- a/sdk/libc/src/dl.rs +++ b/sdk/libc/src/dl.rs @@ -115,7 +115,7 @@ fn open(loading: &mut Loading, published: &mut Published, name: &str, inits: &mu // its own protection once nothing more is written. // SAFETY: placed anywhere, so it replaces nothing. let base = unsafe { syscall::vm_reserve(plan.span, VmProt::ReadWrite, VmAt::ANYWHERE) }.map_err(Refused::Memory)?; - let reservation = Reservation { at: base, len: plan.span }; + let mut reservation = Reservation { at: base, len: plan.span, published: false }; // SAFETY: the fresh, zeroed reservation, this function's alone. plan.lay(bytes, unsafe { core::slice::from_raw_parts_mut(base, plan.span) })?; // SAFETY: laid out whole and writable, and reached by nothing else until published. @@ -146,7 +146,7 @@ fn open(loading: &mut Loading, published: &mut Published, name: &str, inits: &mu }; let name = alloc::ffi::CString::new(name).map_err(|_| "dlopen: a library path holds a NUL")?; let name: &'static core::ffi::CStr = alloc::boxed::Box::leak(name.into_boxed_c_str()); - core::mem::forget(reservation); + reservation.published = true; let module = loading.add(Module::new(name, image, tls)); if let Some((at, count)) = tables.init_array { inits.push((start + at as usize, count)); @@ -155,16 +155,19 @@ fn open(loading: &mut Loading, published: &mut Published, name: &str, inits: &mu Ok(module) } -/// A reservation released unless it is published. +/// A library's memory, released unless it was published. struct Reservation { at: *mut u8, len: usize, + published: bool, } impl Drop for Reservation { fn drop(&mut self) { - // SAFETY: a reservation nothing was published over. - let _ = unsafe { syscall::vm_release(self.at, self.len) }; + if !self.published { + // SAFETY: a reservation nothing was published over. + let _ = unsafe { syscall::vm_release(self.at, self.len) }; + } } } diff --git a/sdk/toyos/src/tls/layout.rs b/sdk/toyos/src/tls/layout.rs index 2553bdf2c1c..7865e913c22 100644 --- a/sdk/toyos/src/tls/layout.rs +++ b/sdk/toyos/src/tls/layout.rs @@ -84,11 +84,11 @@ mod tests { for p_align in [0, 1, 2, 4, 8, 16, 32, 64, 4096, 1 << 21] { let a = area(variant, memsz, p_align, DTV, tcb).unwrap(); let p = p_align.max(1); - assert_eq!(a.data % p, 0, "{variant:?} memsz {memsz} align {p_align}: data off its alignment"); - assert!(a.align.is_power_of_two() && a.align >= 16 && a.align % p == 0); + assert!(a.data.is_multiple_of(p), "{variant:?} memsz {memsz} align {p_align}: data off its alignment"); + assert!(a.align.is_power_of_two() && a.align >= 16 && a.align.is_multiple_of(p)); assert!(a.data >= DTV, "the data meets the DTV"); assert!(a.data + memsz <= a.size && a.tp + tcb <= a.size, "past the area's end"); - assert_eq!(a.tp % 16, 0, "a thread pointer off 16"); + assert!(a.tp.is_multiple_of(16), "a thread pointer off 16"); match variant { Variant::II => { assert_eq!(a.data + memsz.next_multiple_of(p), a.tp, "variant II's data ends at TP"); diff --git a/sdk/toyos/src/tls/mod.rs b/sdk/toyos/src/tls/mod.rs index a87fda81437..c9f527abc3a 100644 --- a/sdk/toyos/src/tls/mod.rs +++ b/sdk/toyos/src/tls/mod.rs @@ -163,19 +163,25 @@ impl Loading { } /// Publish `module` under [`next_id`](Self::next_id). - pub fn add(&mut self, mut module: Module) -> &'static Module { - module.id = self.next_id(); - let module: &'static Module = alloc::boxed::Box::leak(alloc::boxed::Box::new(module)); - let at = (module as *const Module).cast_mut(); - // SAFETY: the last module published, never freed; the list is held. - match unsafe { self.0.last.load(Ordering::Relaxed).as_ref() } { - Some(last) => last.next.store(at, Ordering::Release), - None => self.0.first.store(at, Ordering::Release), - } - self.0.last.store(at, Ordering::Relaxed); - self.0.count.store(module.id, Ordering::Release); - module + pub fn add(&mut self, module: Module) -> &'static Module { + publish(self.0, module) + } +} + +/// Publish `module` in `list` under the next id: the list's one writer, which +/// holds it or is the program's start, before any other thread exists. +fn publish(list: &'static Modules, mut module: Module) -> &'static Module { + module.id = list.count.load(Ordering::Relaxed) + 1; + let module: &'static Module = alloc::boxed::Box::leak(alloc::boxed::Box::new(module)); + let at = (module as *const Module).cast_mut(); + // SAFETY: the last module published, never freed. + match unsafe { list.last.load(Ordering::Relaxed).as_ref() } { + Some(last) => last.next.store(at, Ordering::Release), + None => list.first.store(at, Ordering::Release), } + list.last.store(at, Ordering::Relaxed); + list.count.store(module.id, Ordering::Release); + module } impl Drop for Loading { @@ -260,9 +266,7 @@ pub unsafe fn start(header: *const u8, name: &[u8]) { // No panic here, which reads a thread-local: an `argv[0]` holds no NUL. let name = alloc::ffi::CString::new(name).unwrap_or_default(); let name: &'static CStr = alloc::boxed::Box::leak(name.into_boxed_c_str()); - let mut first = Loading(list); - first.add(Module::new(name, image, tls)); - core::mem::forget(first); + publish(list, Module::new(name, image, tls)); let Ok(at) = reserve(area.size, area.align) else { refuse(b"no memory for this program's thread-local storage: it cannot start\n"); }; diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 12f8eafb46a..53ae040a010 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -421,7 +421,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" version = "0.8.9" -source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#d38a45d2cb833ef562304723801f4e4aeb21361c" +source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#fbfc60c73509f321abab280b6f2d74785664e338" dependencies = [ "cfg-if", "windows-link", From c6c2c3dd2f3db7c757c7e9e222871704c7ffe4fa Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 12:11:04 +0100 Subject: [PATCH 03/13] abuse_elf_loader: drop the builders only its library cases used Co-Authored-By: Claude Opus 5.5 --- tests/guest/rust-tests/src/bin/abuse_elf_loader.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs index 7448929e026..dfe0c1c99ad 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs @@ -77,11 +77,8 @@ const DT_JMPREL: i64 = 23; const DT_PLTRELSZ: i64 = 2; 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; const STT_TLS: u8 = 6; @@ -189,14 +186,6 @@ impl Elf { .poke(off + 8, &st_value.to_le_bytes()) } - /// One Elf64_Shdr at file offset `off`. - fn shdr(self, off: usize, sh_type: u32, sh_offset: u64, sh_size: u64, sh_entsize: u64) -> Self { - self.poke(off + 4, &sh_type.to_le_bytes()) - .poke(off + 24, &sh_offset.to_le_bytes()) - .poke(off + 32, &sh_size.to_le_bytes()) - .poke(off + 56, &sh_entsize.to_le_bytes()) - } - fn build(mut self) -> Vec { let phnum = self.phdrs.len(); assert!(PH_OFF + phnum * PH_SIZE <= self.bytes.len(), "phdrs past the buffer"); @@ -579,8 +568,6 @@ fn values_are_bounded_by_the_image() { 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; From f66dd9a22327609856695ea2a5a82bb63dfdf612 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 13:30:36 +0100 Subject: [PATCH 04/13] Record the tty stall sighting and the kernel's size after the loader left Co-Authored-By: Claude Opus 5.5 --- ...-tty-interrupt-can-stall-at-the-nested-shells-line.md | 9 +++++++++ ...e-kernel-is-small-interrupts-post-and-threads-wait.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/issues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md b/issues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md index a38fa06710a..226bfeda286 100644 --- a/issues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md +++ b/issues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md @@ -38,6 +38,15 @@ the same stall. Nothing else in that run was red; it did not wait for host CPUs; host load was not recorded. Evidence: https://github.com/ToyOSOrg/evidence/tree/a95c798b496d939b4bdb3c7fe0a3cfc2e6d9bfb6/pr-1166/r1 +Another sighting, 2026-11-05, at #1189 (the guest runner, run 42018194698, and +local QEMU), with main `15c26bfbc` stalling the same way in 1 of 7 local runs +beside it. The kernel's exit record of the killed nested shell names its side: +its main thread made one `SYS_READ` (`1=1`) and spent the 119 s in it, after +the prompt's one write, while the probe had written `cat` (`0=4`). So the +nested shell's stdin read sat blocked with the line written: the read or its +wake lost it, not the surface. Evidence: +https://github.com/ToyOSOrg/evidence/tree/43e52cb13749d79b5ca967e0688065fb2eefd103/pr-1189/tty + **Exit:** find what the nested shell waits on when it stalls, fix it, and show `libc_children` green across repeated runs. `signal_quit`'s stall may be the same defect. 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 395c4b6c7cd..013696d1ded 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 @@ -18,7 +18,8 @@ kernel's part of a restart, and named module tests hold each program's 1. **What step 9's file half left** (stage 4): its windows, by stage 6 step 2's rule. Its lines are measured: `kernel/src` held 230 files and - 59,012 lines before #1152 and 223 and 56,800 after #1182. + 59,012 lines before #1152 and 223 and 56,800 after #1182, and 218 and + 55,286 once the loader left it (#1189). 2. **P1 of the Wi-Fi track** (`issues/there-is-no-wifi.md`): a BAR stays where firmware put it, handed over in 4 KiB pages with its MSI-X table's page, under the owner's Q2 ruling. The orchestrator ruled on #900 that From 42ed99c1446ab7465c27d1b038abb864b3c88369 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 13:36:06 +0100 Subject: [PATCH 05/13] Crafted-ELF cases about the kernel's TLS allocation go with it The T14 testcases boot at f66dd9a22 red abuse_elf_loader's tls_template_2m and abuse_elf_segments's tls_huge: each asserted the kernel refuses a PT_TLS its TLS block allocation could not hold, and the kernel lays out no TLS now, so it spawns them and the child dies. tls_crowds_dtv went with them, a survival case over the kernel's DTV. The PT_TLS rules the program-header parse keeps (p_align, filesz > memsz, vaddr) stay asserted at the spawn. Co-Authored-By: Claude Opus 5.5 --- .../rust-tests/src/bin/abuse_elf_loader.rs | 27 ++----------------- .../rust-tests/src/bin/abuse_elf_segments.rs | 24 +++++------------ 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs index dfe0c1c99ad..28c1f10ed83 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs @@ -369,9 +369,8 @@ fn main() { .build(), ); - // 5. PT_TLS p_align is a file-chosen addend to the TLS block's size. - // u64::MAX wrapped the size computation, and the layout that fell out - // of it tripped the block's own "DTV overlaps TLS data" assert. + // 5. A PT_TLS p_align that is no power of two, which the program-header + // parse refuses: u64::MAX once wrapped the kernel's TLS block size. spawn_refused( "tls_align_wrap", &base_exe(0x4000) @@ -379,28 +378,6 @@ fn main() { .build(), ); - // 6. A TLS size that leaves no room for the DTV in front of it. The DTV - // is a fixed 528-byte header the kernel writes at the start of the - // same allocation, and nothing counted it when sizing that allocation. - // Legal to load once it is counted, so the assertion is survival. - let _ = spawn_result( - "tls_crowds_dtv", - &base_exe(0x4000) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x2000, vaddr: 0x2000, filesz: 0, memsz: 0x1F_FF00, align: 8 }) - .build(), - ); - - // 7. A TLS template just under 2 MiB. The kernel heap's page source - // asserts above 2 MiB, and dlmalloc rounds a request up to a whole - // 2 MiB granule *plus* its own bookkeeping — so the guard on the - // allocation was short by exactly that bookkeeping. - spawn_refused( - "tls_template_2m", - &base_exe(0x4000) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x2000, vaddr: 0x2000, filesz: 0, memsz: 0x1F_FFF0, align: 8 }) - .build(), - ); - // 8. A section header table of 2.4 MiB, in a file big enough to hold it, // which the kernel read into one `Vec` past `mm/alloc.rs`'s assert when // it looked for an executable's `.rela.dyn` and `.symtab` there. 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 5c8060ad514..82c7ca099f5 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs @@ -9,10 +9,10 @@ //! kernel laid out thread-local storage; it is the program's now, and the //! header is still refused at the spawn. //! -//! A `p_memsz` no allocator can satisfy, and program-header vaddrs outside the -//! loaded image, are the other two ways in. Every case must be an error return, -//! with the kernel intact afterwards. A library's headers are the program's -//! own loader's to refuse (libc's `dlopen`). +//! Program-header vaddrs outside the loaded image are the other way in. Every +//! case must be an error return, with the kernel intact afterwards. A +//! library's headers are the program's own loader's to refuse (libc's +//! `dlopen`). use std::fs; @@ -122,25 +122,15 @@ fn spawn_as(path: &str, image: &[u8]) -> SyscallError { fn main() { fs::create_dir_all(DIR).expect("create /tmp/abuse_elf"); - // 1. PT_TLS filesz > memsz: 16 KiB copied into a 16-byte kernel heap - // allocation. The overflow that made this test necessary. + // 1. PT_TLS filesz > memsz, which the program-header parse refuses: 16 KiB + // copied into a 16-byte kernel heap allocation while the kernel laid + // out thread-local storage. let err = spawn_err("tls_filesz", &elf(Phdr { kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x2000, filesz: 0x4000, memsz: 16, align: 8, })); assert_eq!(err, SyscallError::InvalidArgument, "PT_TLS filesz > memsz"); - // 2. PT_TLS memsz no allocator can satisfy — must be an error, not an - // `.expect` panic in syscall context. - let err = spawn_err("tls_huge", &elf(Phdr { - kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x2000, - filesz: 0, memsz: u64::MAX / 2, align: 8, - })); - assert!( - matches!(err, SyscallError::InvalidArgument | SyscallError::ResourceExhausted), - "PT_TLS memsz = u64::MAX/2 gave {err:?}", - ); - // 3. PT_TLS vaddr far outside every PT_LOAD. let err = spawn_err("tls_vaddr", &elf(Phdr { kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x8000_0000, From 774465867ada9779bd6d3401f1669a8e041772cc Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 14:53:23 +0100 Subject: [PATCH 06/13] The loader is the SDK's, and refuses what it cannot honour The loader moves out of libc into `toyos::dl`, beside the module list it publishes into (`toyos::tls`) and the self-relocation that precedes it. std's and libc's starts both call `toyos::dl::start`, so a Rust program that names a library no longer has to link libtoyos_c: the weak symbol, std's "links no loader" refusal and `std_tls`'s link workaround go. libc keeps `dlopen`, `dlsym`, `dlclose` and `dlerror` as wrappers over it, with errno and a per-thread `dlerror` text that names the reason. The libloading fork calls `toyos::dl` again; `toyos` depends on `toyos-elf`, which joins std's patched crates and the published crates ahead of `toyos`, and the rust fork's library lock carries it. A load is one set: the libraries named and every library they name not yet published, found breadth first, so a cycle of `DT_NEEDED` names loads once instead of recursing off the stack. Each is relocated against the published modules and the set, all are protected, and only then is the set published, so a refused load leaves nothing half-loaded; constructors run afterwards, each library's after those of the libraries it names. Nothing is skipped silently any more. Every relocation type but NONE that the loader does not apply (COPY, IRELATIVE, any unknown number) is refused by number; R_X86_64_64 and R_AARCH64_ABS64 are applied as S + A. A symbol nothing defines is refused by name, but a weak one, which is 0, and a call slot in an image not bound now, which is bound to a trap that fails at the call as a lazy loader would (a Rust library's `main`, which its std's `_start` names). A DT_NEEDED naming nothing, and a library segment both writable and executable, are refused by name. The executable relocates itself before its main thread's thread-local storage is copied from its template, so `toyos::relocate` no longer refuses a write into the template; the start's executable pass, which runs after that copy, writes what lands in the template into the main thread's block too. `RelaCounts`, whose only user was the kernel loader, goes with its comment about shared read-only pages. `abuse_kernel_addr` holds SYS_SET_THREAD_POINTER's refusal of a non-canonical, a kernel and the last address. The crafted-ELF cases' comments lose their history. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 + build/sdkversion.rs | 1 + build/sysroot.rs | 15 +- build/toolchain.rs | 41 +- ...ry-whose-constructors-are-still-running.md | 23 + ...-never-binds-to-the-executables-exports.md | 6 +- ...a-package-cannot-ship-its-own-libraries.md | 6 +- issues/an-executables-relro-stays-writable.md | 4 +- ...bc-refuses-what-toyos-cannot-yet-answer.md | 2 +- ...b-pages-still-copies-each-programs-text.md | 3 +- ...-capability-end-state-is-twelve-answers.md | 6 +- ...-small-interrupts-post-and-threads-wait.md | 4 +- ...ernel-still-parses-what-userland-writes.md | 26 +- issues/toyos-runs-on-arm64.md | 2 +- issues/what-is-owed-on-file-size.md | 4 +- lib/elf/Cargo.toml | 11 +- lib/elf/src/dynamic.rs | 13 + lib/elf/src/lib.rs | 2 +- lib/elf/src/rela.rs | 148 ++---- lib/elf/tests/fuzz.rs | 25 +- lib/elf/tests/tables.rs | 90 ++-- rust | 2 +- sdk/libc/Cargo.lock | 1 + sdk/libc/arch/src/dlimage_relocations.rs | 222 --------- sdk/libc/arch/src/lib.rs | 7 - sdk/libc/arch/src/prototypes.rs | 1 - sdk/libc/src/dl.rs | 356 +++----------- sdk/libc/src/lib.rs | 6 +- sdk/std/sys/pal/mod.rs | 23 +- sdk/toyos/Cargo.toml | 4 +- .../dlimage.rs => toyos/src/dl/image/mod.rs} | 167 ++++--- sdk/toyos/src/dl/image/tests.rs | 334 +++++++++++++ sdk/toyos/src/dl/mod.rs | 458 ++++++++++++++++++ sdk/toyos/src/dl/tests.rs | 89 ++++ sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/relocate/mod.rs | 18 +- sdk/toyos/src/relocate/tests.rs | 20 +- .../rust-tests/src/bin/abuse_elf_loader.rs | 18 +- .../rust-tests/src/bin/abuse_elf_segments.rs | 25 +- .../rust-tests/src/bin/abuse_kernel_addr.rs | 24 +- tests/guest/rust-tests/src/bin/std_tls.rs | 15 +- 41 files changed, 1331 insertions(+), 894 deletions(-) create mode 100644 issues/a-concurrent-dlopen-can-answer-a-library-whose-constructors-are-still-running.md delete mode 100644 sdk/libc/arch/src/dlimage_relocations.rs rename sdk/{libc/src/dlimage.rs => toyos/src/dl/image/mod.rs} (69%) create mode 100644 sdk/toyos/src/dl/image/tests.rs create mode 100644 sdk/toyos/src/dl/mod.rs create mode 100644 sdk/toyos/src/dl/tests.rs diff --git a/Cargo.lock b/Cargo.lock index e9c0b2b76f6..3eef5415eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3539,6 +3539,7 @@ dependencies = [ "rustc-std-workspace-alloc", "rustc-std-workspace-core", "toyos-abi", + "toyos-elf", ] [[package]] @@ -3739,6 +3740,7 @@ name = "toyos-elf" version = "0.1.0" dependencies = [ "object", + "rustc-std-workspace-core", ] [[package]] diff --git a/build/sdkversion.rs b/build/sdkversion.rs index 64499f2b100..ef9faaae72e 100644 --- a/build/sdkversion.rs +++ b/build/sdkversion.rs @@ -27,6 +27,7 @@ pub const PUBLISHED: &[Crate] = &[ Crate { name: "toyos-abi", dir: "sdk/abi" }, Crate { name: "toyos-keymap", dir: "sdk/keymap" }, Crate { name: "toyos-font", dir: "sdk/font" }, + Crate { name: "toyos-elf", dir: "lib/elf" }, Crate { name: "toyos", dir: "sdk/toyos" }, Crate { name: "toyos-window", dir: "sdk/window" }, Crate { name: "toyos-osrelease", dir: "sdk/osrelease" }, diff --git a/build/sysroot.rs b/build/sysroot.rs index 81a583a54fb..87d730fe716 100644 --- a/build/sysroot.rs +++ b/build/sysroot.rs @@ -23,8 +23,9 @@ //! moved to the commit its tree pins; //! a linked worktree in its own `rust/`, made on first need as a git worktree of //! the primary's fork repository at the commit this tree pins and never moved ([`fork_checkout`]). -//! `library/std` names `toyos-abi` and `toyos` by version, and every bootstrap -//! run is handed this worktree's by one `[patch]` ([`std_patch`]); each file of +//! `library/std` names `toyos-abi` and `toyos` by version, `toyos-elf` through +//! `toyos`, and every bootstrap run is handed this worktree's by one `[patch]` +//! ([`std_patch`]); each file of //! its ToyOS backend, `sdk/std` and the allocator files of `sdk/libc/src` it //! includes (`malloc.rs`, `mapped.rs`), it names by a `#[path]` up out of the //! fork. So each checkout's std compiles against its own worktree's ABI and @@ -66,7 +67,7 @@ use crate::toolchain::{self, host_triple, GuestTarget, Owner, Role, GUEST_TARGET use whole_toolchain::{whole, Whole}; /// The per-worktree sources that end up inside a sysroot: std links the -/// `toyos-abi` and `toyos` [`std_patch`] hands it and compiles its ToyOS +/// `toyos-abi`, `toyos-elf` and `toyos` [`std_patch`] hands it and compiles its ToyOS /// backend from `sdk/std`, C's allocator among it from `sdk/libc/src`, and /// `libtoyos_c.a` is `sdk/libc` with `toyos-elf` and `toyos-osrelease`. pub const SYSROOT_SOURCES: [&str; 7] = [ @@ -94,7 +95,7 @@ pub(crate) const SYSROOT_MANIFESTS: [&str; 7] = [ /// Of [`SYSROOT_MANIFESTS`], the crates std names by version, which std's /// lockfile resolves with the fork's own through [`std_patch`]: what of a /// worktree can move a freestanding target's dependency versions. -const STD_MANIFESTS: [&str; 2] = ["sdk/abi/Cargo.toml", "sdk/toyos/Cargo.toml"]; +const STD_MANIFESTS: [&str; 3] = ["sdk/abi/Cargo.toml", "lib/elf/Cargo.toml", "sdk/toyos/Cargo.toml"]; /// One of the crates std names by version, in a worktree. pub(crate) struct StdCrate { @@ -169,7 +170,7 @@ const RECIPE: &str = "bootstrap stage-0 local rebuild, libraries from the stamp, #[derive(Clone, Copy, PartialEq, Eq)] enum Libraries { /// The fork's and [`SYSROOT_SOURCES`]: ToyOS userland's std, which names - /// `toyos-abi` and `toyos`. + /// `toyos-abi` and `toyos`, and through it `toyos-elf`. Worktree, /// The fork's alone: the kernel's `core` and `alloc` and the loader's std, /// which is what lets them be a key of their own. @@ -1276,7 +1277,7 @@ pub(crate) mod tests { /// The manifests [`std_crates`] reads, in a worktree at `root` that has /// no other source: what a bootstrap run is handed std's patch from. pub(crate) fn std_manifests(root: &Path) { - for (manifest, name) in STD_MANIFESTS.into_iter().zip(["toyos-abi", "toyos"]) { + for (manifest, name) in STD_MANIFESTS.into_iter().zip(["toyos-abi", "toyos-elf", "toyos"]) { write(&root.join(manifest), &format!("[package]\nname = \"{name}\"\n")); } } @@ -1316,6 +1317,8 @@ pub(crate) mod tests { "--config".to_string(), format!("patch.crates-io.toyos-abi.path=\"{at}/sdk/abi\""), "--config".to_string(), + format!("patch.crates-io.toyos-elf.path=\"{at}/lib/elf\""), + "--config".to_string(), format!("patch.crates-io.toyos.path=\"{at}/sdk/toyos\""), ], ); diff --git a/build/toolchain.rs b/build/toolchain.rs index 5ba8ae2900f..8bad280dcbc 100644 --- a/build/toolchain.rs +++ b/build/toolchain.rs @@ -154,8 +154,9 @@ fn toyos_sources_in_dep_info(crates: &[StdCrate], text: &str) -> Vec { paths_in_dep_info(text).filter(|path| std_crate_of(crates, path).is_some()).map(str::to_string).collect() } -/// Refuse a std whose dep-info under `dep_info` names a `toyos-abi` or `toyos` -/// that is not this worktree's: a registry's, or another checkout's. +/// Refuse a std whose dep-info under `dep_info` names a `toyos-abi`, +/// `toyos-elf` or `toyos` that is not this worktree's: a registry's, or +/// another checkout's. /// /// What the builder believed is not evidence; this reads what the compiler was /// handed. A std built against another `toyos-abi` still builds, links and @@ -180,8 +181,8 @@ pub(crate) fn assert_std_built_from(root: &Path, dep_info: &Path) { sources.iter().filter(|p| !Path::new(p).starts_with(&root)).collect(); assert!( foreign.is_empty(), - "std was compiled against {} toyos-abi or toyos sources that are not this worktree's {}:\n {}\n\ - std names both by version and takes this worktree's only through the [patch] bootstrap is \ + "std was compiled against {} toyos-abi, toyos-elf or toyos sources that are not this worktree's {}:\n {}\n\ + std names them by version and takes this worktree's only through the [patch] bootstrap is \ handed (`sysroot::std_patch`): a registry's copy is a resolve that patch did not reach, and \ another checkout's a patch that named another tree.", foreign.len(), @@ -861,36 +862,36 @@ mod tests { ); } - /// **A std that took either crate from a registry is refused, naming the - /// registry's source**: what a std build the patch did not reach compiles, - /// as its dep-info spells it. Only this worktree's copies of both pass. + /// **A std that took any of its crates from a registry is refused, naming + /// the registry's source**: what a std build the patch did not reach + /// compiles, as its dep-info spells it. Only this worktree's copies pass. #[test] - fn a_std_built_from_a_registry_s_copy_of_either_crate_is_refused() { + fn a_std_built_from_a_registry_s_copy_of_any_crate_is_refused() { let temp = TempDir::new("std-registry"); let root = fs::canonicalize(&*temp).unwrap(); let built = root.join("rust/build/toyos-std/host/stage0-std/x86_64-unknown-toyos"); fs::create_dir_all(built.join("dist")).unwrap(); crate::sysroot::tests::std_manifests(&root); - let ours = |dir: &str| format!("{}/{dir}/src/lib.rs", root.display()); - let registry = |name: &str, version: &str| { - format!("/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/{name}-{version}/src/lib.rs") - }; - let built_from = |abi: &str, toyos: &str| { - fs::write(built.join("dist/libstd.d"), format!("{}/dist/libstd.rlib: {abi} {toyos}\n", built.display())) + let ours = ["sdk/abi", "lib/elf", "sdk/toyos"].map(|dir| format!("{}/{dir}/src/lib.rs", root.display())); + let registry = [("toyos-abi", "0.63.0+267ff3ef79dccdcb"), ("toyos-elf", "0.2.0+0a1b2c3d4e5f6a7b"), ("toyos", "0.75.0+0ea8e9e321a9f71c")] + .map(|(name, version)| format!("/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/{name}-{version}/src/lib.rs")); + let built_from = |sources: &[String; 3]| { + fs::write(built.join("dist/libstd.d"), format!("{}/dist/libstd.rlib: {}\n", built.display(), sources.join(" "))) .unwrap(); std::panic::catch_unwind(|| assert_std_built_from(&root, &built)) .err() .map(|refused| *refused.downcast::().expect("a formatted refusal")) }; - assert_eq!(built_from(&ours("sdk/abi"), &ours("sdk/toyos")), None); + assert_eq!(built_from(&ours), None); - let (abi, toyos) = (registry("toyos-abi", "0.63.0+267ff3ef79dccdcb"), registry("toyos", "0.75.0+0ea8e9e321a9f71c")); - for (taken, from) in [(&abi, built_from(&abi, &ours("sdk/toyos"))), (&toyos, built_from(&ours("sdk/abi"), &toyos))] { - let said = from.unwrap_or_else(|| panic!("a std built from {taken} was taken")); + for (i, taken) in registry.iter().enumerate() { + let mut sources = ours.clone(); + sources[i] = taken.clone(); + let said = built_from(&sources).unwrap_or_else(|| panic!("a std built from {taken} was taken")); assert!(said.contains(taken.as_str()) && said.contains("a registry's copy"), "{said}"); } - let said = built_from(&abi, &toyos).expect("a std built from the registry's copies of both was taken"); - assert!(said.contains(&abi) && said.contains(&toyos), "{said}"); + let said = built_from(®istry).expect("a std built from the registry's copies of all was taken"); + assert!(registry.iter().all(|taken| said.contains(taken.as_str())), "{said}"); } /// **Freestanding libraries whose dep-info names the worktree outside its diff --git a/issues/a-concurrent-dlopen-can-answer-a-library-whose-constructors-are-still-running.md b/issues/a-concurrent-dlopen-can-answer-a-library-whose-constructors-are-still-running.md new file mode 100644 index 00000000000..7edec1b51ef --- /dev/null +++ b/issues/a-concurrent-dlopen-can-answer-a-library-whose-constructors-are-still-running.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-11-05 +--- + +# A concurrent dlopen can answer a library whose constructors are still running + +`toyos::dl::open` publishes a set of libraries while it holds the module +list (`tls::loading`), then lets the list go and runs their constructors, so a +constructor may itself load a library. A second thread that opens the same +path meanwhile finds it published and returns it at once, before the first +thread's constructors for it have finished, or begun. glibc holds its loader +lock, which is recursive, across the constructors. + +Nothing shows it: no guest case opens one library from two threads while its +constructor is slow. + +**Exit**: an open that finds a library its constructors have not finished +waits for them, while a constructor's own load in the constructing thread +still proceeds. A host test of the waiting rule holds it, and a guest case +opens one library from two threads, the first's constructor slow, and the +second reads the constructor's effect. diff --git a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md index 48ffb00810d..1fd36995dc2 100644 --- a/issues/a-dlopened-library-never-binds-to-the-executables-exports.md +++ b/issues/a-dlopened-library-never-binds-to-the-executables-exports.md @@ -6,9 +6,9 @@ opened: 2026-09-29 # A dlopen'ed library never binds to the executable's exports -libc's loader (`sdk/libc/src/dl.rs`, `Published`) binds a library's -`GLOB_DAT`/`JUMP_SLOT` slots against every published module, the executable -first, as glibc's `dlopen` searches the global scope. Nothing shows it: no +The SDK's loader (`sdk/toyos/src/dl/mod.rs`, `Global`) binds a library's +`GLOB_DAT`, `JUMP_SLOT` and `ABS64` slots against every published module, the +executable first, as glibc's `dlopen` searches the global scope. Nothing shows it: no guest case loads a plugin that calls back into a symbol its host exports, and no ToyOS executable is linked to export one. diff --git a/issues/a-package-cannot-ship-its-own-libraries.md b/issues/a-package-cannot-ship-its-own-libraries.md index 135b3658be5..c59084a9dbe 100644 --- a/issues/a-package-cannot-ship-its-own-libraries.md +++ b/issues/a-package-cannot-ship-its-own-libraries.md @@ -6,9 +6,9 @@ opened: 2026-09-27 # A package cannot ship its own libraries -A program's own start loads the `DT_NEEDED` libraries it names (libc's -`__toyos_load_needed`), but a name without a `/` is looked for in -`/system/lib` alone (`sdk/libc/src/dl.rs`, `LIBRARY_DIR`). A package under +A program's own start loads the `DT_NEEDED` libraries it names +(`toyos::dl::start`), but a name without a `/` is looked for in +`/system/lib` alone (`sdk/toyos/src/dl/mod.rs`, `LIBRARY_DIR`). A package under `/apps//` whose binary needs a library it carries beside it fails to start. diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md index c138cd05e86..59c103284ba 100644 --- a/issues/an-executables-relro-stays-writable.md +++ b/issues/an-executables-relro-stays-writable.md @@ -12,8 +12,8 @@ of function pointers. The kernel maps it as the writable `PT_LOAD` that holds it, reads no `PT_GNU_RELRO` (`rg -i relro kernel/src lib/elf/src` matches nothing), and gives a program no way to take the write away once `toyos::relocate` has run. A stray or hostile write in a running program can -rewrite a vtable. A library's is read-only once libc's loader has relocated -it (`sdk/libc/src/dl.rs`, `protect`); an executable's is not, and the same +rewrite a vtable. A library's is read-only once the SDK's loader has relocated +it (`sdk/toyos/src/dl/mod.rs`, `protect`); an executable's is not, and the same `SYS_VM_PROTECT` is what its start would call. Owner: the userland loader of diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index fa1dc31aabf..5c6014790db 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -29,7 +29,7 @@ call replacing a process's image; `setsid` and `getsid`, no POSIX session; Refused by design, and owed nothing: `mmap` or `mprotect` of memory writable and executable at once, `ENOTSUP`. A program writes its code and then -protects it read and execute, as libc's loader does; no page is both +protects it read and execute, as the SDK's loader does; no page is both (orchestrator, with the userland loader, which made read and execute a protection a program may ask for). diff --git a/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md b/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md index 213c78e807a..9d8a5c84e01 100644 --- a/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md +++ b/issues/process-memory-in-4-kib-pages-still-copies-each-programs-text.md @@ -24,7 +24,8 @@ makes of 4 KiB frames in any order (`kernel/src/object/shm.rs`), so the hosted clang's 148 MB image no longer needs one physical run; `hosted_clang_hello`, which was refused `ENOMEM` linking, is the check. What is still one physical run is a device-lendable object (`SHM_DEVICE`, `SharedMemObject::create_run`); -a library is laid out in the process's own private memory by libc's loader. +a library is laid out in the process's own private memory by the SDK's +loader (`toyos::dl`). **The constraint that makes this a track, measured on the T14** (run 29 readback, `PMM: 100/16038MB used` at 11 s with four user processes and three diff --git a/issues/the-capability-end-state-is-twelve-answers.md b/issues/the-capability-end-state-is-twelve-answers.md index 4ccfd294e70..4acb099a4e0 100644 --- a/issues/the-capability-end-state-is-twelve-answers.md +++ b/issues/the-capability-end-state-is-twelve-answers.md @@ -156,11 +156,11 @@ table** can still reach: (`SYS_OPEN`, `SYS_READDIR`, `SYS_DELETE`, `SYS_MKDIR`, `SYS_RMDIR`, `SYS_RENAME`, `SYS_SYMLINK`, `SYS_READLINK`), `SYS_SPAWN`, and `SYS_SHUTDOWN` — plus `SYS_SYSINFO`'s census. `SYS_DLOPEN` was among them - and is gone: a program loads its own libraries (libc's loader), reading + and is gone: a program loads its own libraries (`toyos::dl`), reading each through its own namespace. -**Smallest decision:** the last group. **Recommendation:** keep the filesystem, -`SYS_DLOPEN` and `SYS_SPAWN` ambient under question 2's ruling; make +**Smallest decision:** the last group. **Recommendation:** keep the filesystem +and `SYS_SPAWN` ambient under question 2's ruling; make `SYS_SHUTDOWN` and `SYS_SYSINFO` rights-bearing, because neither is path authority and both are machine-wide. Then this list, minus those two, is the committed answer. **Both are done** — `Rights::POWER` and `Rights::ROSTER` — so 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 013696d1ded..3404660c79c 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 @@ -122,8 +122,8 @@ Beside that order, each waiting on something outside it: 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; and `SYS_DLOPEN` takes no path. - **Done**: spawn (#536, `SpawnArgs::image`), and dlopen: libc's loader - reads the library through `toyos::files::read_image` wherever it is + **Done**: spawn (#536, `SpawnArgs::image`), and dlopen: the SDK's + loader (`toyos::dl`) reads the library through `toyos::files::read_image` wherever it is served and loads it itself, each process holding its own library pages (orchestrator, under the memory design, whose later stages bring sharing back through memory objects); `SYS_DLOPEN` is gone with the kernel's diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index 9de298846a5..e7371ac9b73 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -31,24 +31,18 @@ executable's dynamic section, symbol tables or relocations. thread-local storage (`toyos::tls`): its module list, every thread's area and DTV, which grows in the process's own memory, and `__tls_get_addr`'s blocks. The kernel's only part is the thread pointer a thread names -(`SYS_SET_THREAD_POINTER`). libc's loader (`sdk/libc/src/dl.rs`) lays a -library out in private memory, relocates it, protects its pages, its RELRO -read-only, and publishes it in the module list; `dlopen`, `dlsym`, `dladdr` and -`dl_iterate_phdr` read that list, and so do std's unwinder and the backtrace -crate. `SYS_DLOPEN`, `SYS_DLSYM`, `SYS_DLCLOSE`, `SYS_TLS_ALLOC_BLOCK` and +(`SYS_SET_THREAD_POINTER`). The SDK's loader (`toyos::dl`) lays a +library out in private memory with every library it names, relocates them, +protects their pages, their RELRO read-only, and publishes them in the module +list together or not at all; libc's `dlopen`, `dlsym`, `dladdr` and +`dl_iterate_phdr` read that list, and so do libloading, std's unwinder and the +backtrace crate. `SYS_DLOPEN`, `SYS_DLSYM`, `SYS_DLCLOSE`, `SYS_TLS_ALLOC_BLOCK` and `SYS_QUERY_MODULES` are deleted, and a program may protect its own memory read -and execute, never write and execute at once. An executable's start loads the +and execute, never write and execute at once. An executable's start, std's or libc's, loads the libraries it names (`DT_NEEDED`) through the same loader before `main` -(`__toyos_load_needed`), and `std_tls` holds it. What is left in Ring 0 is the -program-header parse, which the end state keeps. - -**The interval it leaves.** libtoyos_c is built from std, so std cannot link it: -std reaches the loader through a weak symbol, and a Rust executable that names -a library links libtoyos_c itself (`std_tls` does), or its start refuses it by -name and exits 127. A C program always has it. The exit: every Rust executable -links the loader as a C program does, through the target's link of its C -runtime. AArch64 shared objects reach their thread-locals through TLS -descriptors, which the loader refuses. +(`toyos::dl::start`), and `std_tls` holds it. What is left in Ring 0 is the +program-header parse, which the end state keeps. AArch64 shared objects reach +their thread-locals through TLS descriptors, which the loader refuses. The evidence that it is worth doing: nine dated commits over eleven days fixed userland-reachable kernel defects in this code, six of them machine-wide panics; diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 794d21e2581..75bd57da0f6 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -161,7 +161,7 @@ bound). Heaviest: `arch::cpu` (57 references), `arch::percpu` (28), AP[2:1], UXN/PXN, and ASIDs in the TTBR. - **TLS.** Each program lays its own out (`sdk/toyos/src/tls`), variant II on x86-64 and variant I on AArch64; std's `__tls_get_addr` uses `fs:`. - LLVM emits TLSDESC by default on AArch64, which libc's loader refuses, so + LLVM emits TLSDESC by default on AArch64, which `toyos::dl` refuses, so no AArch64 shared object reaches its thread-locals yet. - **Calling conventions.** `extern "sysv64"` at `main.rs:205`, `deadline.rs:194`, `sched/driver.rs:892`, `i8042/mod.rs:490`, diff --git a/issues/what-is-owed-on-file-size.md b/issues/what-is-owed-on-file-size.md index 1cecea7d741..022b3e97218 100644 --- a/issues/what-is-owed-on-file-size.md +++ b/issues/what-is-owed-on-file-size.md @@ -13,8 +13,8 @@ what survives it is a different question. **Answered:** - `elf.rs` → `lib/elf/` (host-tested, crafted-input corpus), `e2c6a06`; the - mapping half stayed as `kernel/src/elf/`, `42b29c9`, until libc's loader - took a library's loading out of the kernel. + mapping half stayed as `kernel/src/elf/`, `42b29c9`, until the program's + loader took a library's loading out of the kernel. - `compositor/main.rs` → `toyos-desktop/` (pure, since moved to `system/compositor/desktop/`), `763712b`; the effects shell is `system/compositor/` over five files with a 71-line `main.rs`, `72705d9`. diff --git a/lib/elf/Cargo.toml b/lib/elf/Cargo.toml index 4ea9f3f706b..e03058be52c 100644 --- a/lib/elf/Cargo.toml +++ b/lib/elf/Cargo.toml @@ -1,8 +1,10 @@ # A member of the host workspace (root `Cargo.toml`), like toyos-gpt: the # kernel depends on it by path and its tests run on the host, # with no target-triple juggling. An ELF is bytes somebody else wrote, and every -# one the kernel opens crossed a trust boundary — so the parser has to be +# one a loader opens crossed a trust boundary — so the parser has to be # exercised against hostile bytes far more often than a QEMU boot allows. +# `toyos`'s loader (`toyos::dl`) depends on it, so std does, and it is +# published before `toyos` (`build/sdkversion.rs`). [package] name = "toyos-elf" @@ -10,6 +12,13 @@ description = "ELF64 decoding of untrusted program images (no_std, no alloc, for version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" +repository = "https://github.com/ToyOSOrg/ToyOS" + +[features] +rustc-dep-of-std = ["core"] + +[dependencies] +core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } [dev-dependencies] # unaligned: an include_bytes! fixture is aligned to 1, and object reads a header in place. diff --git a/lib/elf/src/dynamic.rs b/lib/elf/src/dynamic.rs index 091449f0f56..259f378ad4c 100644 --- a/lib/elf/src/dynamic.rs +++ b/lib/elf/src/dynamic.rs @@ -30,6 +30,12 @@ pub const DT_STRSZ: i64 = 10; pub const DT_INIT_ARRAY: i64 = 25; pub const DT_JMPREL: i64 = 23; pub const DT_INIT_ARRAYSZ: i64 = 27; +pub const DT_BIND_NOW: i64 = 24; +pub const DT_FLAGS: i64 = 30; +pub const DT_FLAGS_1: i64 = 0x6fff_fffb; +/// `DT_FLAGS`' and `DT_FLAGS_1`'s bind-now bits. +pub const DF_BIND_NOW: u64 = 8; +pub const DF_1_NOW: u64 = 1; pub const DT_GNU_HASH: i64 = 0x6fff_fef5u32 as i32 as i64; /// Bytes in one `Elf64_Dyn`. @@ -44,6 +50,10 @@ pub struct Dynamic { pub strsz: Option, pub symtab: Option, pub gnu_hash: Option, + /// `DT_BIND_NOW`, `DF_BIND_NOW` or `DF_1_NOW`: every symbol is bound as + /// the image is loaded, a function's too, so one nothing defines refuses + /// the image rather than its first call. + pub bind_now: bool, } impl Dynamic { @@ -70,6 +80,9 @@ impl Dynamic { DT_STRSZ => out.strsz = Some(val), DT_SYMTAB => out.symtab = Some(val), DT_GNU_HASH => out.gnu_hash = Some(val), + DT_BIND_NOW => out.bind_now = true, + DT_FLAGS if val & DF_BIND_NOW != 0 => out.bind_now = true, + DT_FLAGS_1 if val & DF_1_NOW != 0 => out.bind_now = true, _ => {} } } diff --git a/lib/elf/src/lib.rs b/lib/elf/src/lib.rs index 31acb62c37e..6fb23d11169 100644 --- a/lib/elf/src/lib.rs +++ b/lib/elf/src/lib.rs @@ -50,7 +50,7 @@ pub use layout::{ DynamicSegment, Extent, ImageOffset, ImageRange, Layout, Segment, SegmentFlags, ProgramHeaderTable, SectionTableRef, StackedImage, TlsSegment, }; -pub use rela::{Op, Rela, RelaCounts, RelaTable, Reloc, RelocError, RelocKind, Rules, TlsRef}; +pub use rela::{Op, Rela, RelaTable, Reloc, RelocError, RelocKind, Rules, TlsRef}; pub use section::{SectionHeader, SectionTable}; pub use sym::{Sym, SymIndex, SymTab}; pub use tls::TlsOffset; diff --git a/lib/elf/src/rela.rs b/lib/elf/src/rela.rs index e0d2bc2ed01..fdd520878cd 100644 --- a/lib/elf/src/rela.rs +++ b/lib/elf/src/rela.rs @@ -9,9 +9,8 @@ //! 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. +//! Every type [`Op`] has no variant for, but `NONE`, is refused: a slot left +//! with its link-time value is a wrong pointer, not a skipped relocation. use crate::header::Machine; use crate::layout::{Extent, ImageOffset, TlsSegment}; @@ -26,10 +25,13 @@ pub const ENTRY_SIZE: usize = 24; /// rather than by any one machine's number: [`RelocKind::from_raw`] is the /// only place a number is read. /// -/// `Other` carries the raw type rather than dropping it, so a log line can name -/// what it skipped. +/// `Other` carries the raw type, so its refusal can name it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RelocKind { + /// `R_X86_64_NONE`, `R_AARCH64_NONE`: nothing to write. + None, + /// `R_X86_64_64`, `R_AARCH64_ABS64` + Abs64, /// `R_X86_64_GLOB_DAT`, `R_AARCH64_GLOB_DAT` GlobDat, /// `R_X86_64_JUMP_SLOT`, `R_AARCH64_JUMP_SLOT` @@ -53,6 +55,8 @@ pub enum RelocKind { impl RelocKind { pub const fn from_raw(machine: Machine, r_type: u32) -> RelocKind { match (machine, r_type) { + (_, 0) => RelocKind::None, + (Machine::X86_64, 1) | (Machine::Aarch64, 257) => RelocKind::Abs64, (Machine::X86_64, 6) | (Machine::Aarch64, 1025) => RelocKind::GlobDat, (Machine::X86_64, 7) | (Machine::Aarch64, 1026) => RelocKind::JumpSlot, (Machine::X86_64, 8) | (Machine::Aarch64, 1027) => RelocKind::Relative, @@ -64,21 +68,6 @@ impl RelocKind { (_, other) => RelocKind::Other(other), } } - - /// How many bytes the loader writes for this type, or `None` for one it - /// never writes. - pub const fn write_width(self) -> Option { - match self { - RelocKind::GlobDat - | RelocKind::JumpSlot - | RelocKind::Relative - | RelocKind::DtpMod64 - | RelocKind::DtpOff64 - | RelocKind::Tpoff64 => Some(8), - RelocKind::Tpoff32 => Some(4), - RelocKind::TlsDesc | RelocKind::Other(_) => None, - } - } } /// One `Elf64_Rela` as the file wrote it. Only its kind is readable: the @@ -172,6 +161,16 @@ impl Reloc { } } +impl Op { + /// The bytes it writes. + pub const fn width(&self) -> u64 { + match self { + Op::Tpoff32(_) => 4, + _ => 8, + } + } +} + /// What a [`Reloc`] writes, per psABI, with every file-chosen number already /// bounded. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -179,8 +178,13 @@ 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. + /// `S`, 8 bytes: `GLOB_DAT`, bound to a symbol by name. Bind(SymIndex), + /// `S`, 8 bytes: `JUMP_SLOT`, a call's slot, which a lazy loader binds at + /// the first call. + Call(SymIndex), + /// `S + A`, 8 bytes; `None` is `r_sym == 0`, whose `S` is 0. + Absolute(Option, i64), /// `S + A - tp`, 8 bytes. Tpoff64(TlsRef), /// `S + A - tp`, 4 bytes, sign-extended by the instruction that reads it. @@ -221,26 +225,11 @@ impl TlsSymRef { } } -/// Parse one entry against the module's [`Rules`]: `Ok(None)` for a type this -/// loader does not write, the reason for one it must not. +/// Parse one entry against the module's [`Rules`]: `Ok(None)` for `NONE`, +/// the reason for every type this loader does not write. /// /// 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); - } let sym = || SymIndex::below(rela.sym, symbols.count()).ok_or(RelocError::SymbolPastTable); let tls = || -> Result { if rela.sym != 0 { @@ -253,81 +242,29 @@ pub fn parse(rela: Rela, rules: &Rules, symbols: SymTab<'_>) -> Result return Ok(None), + RelocKind::TlsDesc => return Err(RelocError::TlsDescriptor), + RelocKind::Other(raw) => return Err(RelocError::Unsupported(raw)), 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::Abs64 => Op::Absolute(if rela.sym == 0 { None } else { Some(sym()?) }, rela.addend), + RelocKind::GlobDat => Op::Bind(sym()?), + RelocKind::JumpSlot => Op::Call(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: -/// `DT_RELASZ` and `DT_PLTRELSZ` are bounded separately, so two individually -/// acceptable tables sum to a collection no bound on either input can catch, -/// and growth-by-doubling then overshoots that sum as well. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct RelaCounts { - pub relative: usize, - pub bind: usize, - pub tpoff64: usize, - pub tpoff32: usize, - pub dtpmod64: usize, - pub dtpoff64: usize, - pub tlsdesc: usize, -} - -impl RelaCounts { - pub fn of(entries: impl Iterator) -> RelaCounts { - let mut counts = RelaCounts::default(); - for rela in entries { - let slot = match rela.kind { - RelocKind::Relative => &mut counts.relative, - RelocKind::GlobDat | RelocKind::JumpSlot => &mut counts.bind, - RelocKind::Tpoff64 => &mut counts.tpoff64, - RelocKind::Tpoff32 => &mut counts.tpoff32, - RelocKind::DtpMod64 => &mut counts.dtpmod64, - RelocKind::DtpOff64 => &mut counts.dtpoff64, - RelocKind::TlsDesc => &mut counts.tlsdesc, - RelocKind::Other(_) => continue, - }; - *slot += 1; - } - counts - } - - /// The largest of the counts a caller actually reserves for. - /// - /// Deliberately not "the largest count": a ceiling on a kind nothing - /// stores refuses a file over a collection that does not exist, and a - /// library is about 99.5 % `RELATIVE` — so a cache that bounded itself on - /// `relative`, which it keeps none of, would refuse to cache every real - /// library in the tree. - pub fn max_of(&self, kinds: &[RelocKind]) -> usize { - kinds.iter().map(|&k| self.count_of(k)).max().unwrap_or(0) - } - - pub fn count_of(&self, kind: RelocKind) -> usize { - match kind { - RelocKind::Relative => self.relative, - RelocKind::GlobDat | RelocKind::JumpSlot => self.bind, - RelocKind::Tpoff64 => self.tpoff64, - RelocKind::Tpoff32 => self.tpoff32, - RelocKind::DtpMod64 => self.dtpmod64, - RelocKind::DtpOff64 => self.dtpoff64, - RelocKind::TlsDesc => self.tlsdesc, - RelocKind::Other(_) => 0, - } + let (lo, hi) = rules.window; + let end = rela.offset.checked_add(op.width()).ok_or(RelocError::OffsetOverflows)?; + if rela.offset < lo || end > hi { + return Err(RelocError::OutsideWindow); } + Ok(Some(Reloc { offset: rela.offset, op })) } /// Why a relocation cannot be applied. @@ -342,6 +279,9 @@ pub enum RelocError { /// A TLS descriptor, which only a resolver this loader does not have can /// fill. TlsDescriptor, + /// A type this loader does not apply, by its number: `COPY` and + /// `IRELATIVE` among them. + Unsupported(u32), /// 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, @@ -360,6 +300,7 @@ impl RelocError { RelocError::OutsideWindow => "ELF: relocation r_offset outside the writable image", RelocError::SymbolPastTable => "ELF: relocation r_sym past .dynsym", RelocError::TlsDescriptor => "ELF: R_AARCH64_TLSDESC has no resolver in this loader", + RelocError::Unsupported(_) => "ELF: a relocation type this loader does not apply", 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", @@ -369,7 +310,10 @@ impl RelocError { impl core::fmt::Display for RelocError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(self.as_str()) + match self { + RelocError::Unsupported(raw) => write!(f, "ELF: relocation type {raw}, which this loader does not apply"), + _ => f.write_str(self.as_str()), + } } } diff --git a/lib/elf/tests/fuzz.rs b/lib/elf/tests/fuzz.rs index d15571f274c..422590a7288 100644 --- a/lib/elf/tests/fuzz.rs +++ b/lib/elf/tests/fuzz.rs @@ -117,9 +117,9 @@ struct Case { } 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]; + // RELATIVE, GLOB_DAT, JUMP_SLOT, DTPMOD64, DTPOFF64, TPOFF64, TPOFF32, ABS64. + const X86: [u32; 8] = [8, 6, 7, 16, 17, 18, 23, 1]; + const A64: [u32; 8] = [1027, 1025, 1026, 1028, 1029, 1030, 1027, 257]; match machine { Machine::X86_64 => X86[which], Machine::Aarch64 => A64[which], @@ -192,14 +192,15 @@ fn generate(rng: &mut Rng) -> Case { } 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 which = rng.below(8) as usize; + // NONE, COPY, IRELATIVE, TLSDESC and an unknown AArch64 number. + let r_type = rng.value(u64::from(reloc_number(machine, which)), &[0, 5, 37, 1031, 1032]) as u32; 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), + 0 | 7 => vmin + rng.below(span + 1), 3..=6 if sym == 0 => rng.below(tls_memsz.min(0x400) + 1), _ => rng.below(17).wrapping_sub(8), }; @@ -233,6 +234,7 @@ struct Reached { accepted: u64, relative: u64, bind: u64, + absolute: u64, tpoff: u64, dtpoff: u64, init_arrays: u64, @@ -245,6 +247,7 @@ impl Reached { self.accepted += 1; self.relative += image.relative; self.bind += image.bind; + self.absolute += image.absolute; self.tpoff += image.tpoff; self.dtpoff += image.dtpoff; self.init_arrays += image.init_arrays; @@ -358,17 +361,13 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), }; 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?}"); + assert!(r.offset() >= window.0 && r.offset() + r.op().width() <= window.1, "{r:?} outside {window:x?}"); match r.op() { Op::Relative(off) => { place(off.get()); reached.relative += 1; } - Op::Bind(idx) => { + Op::Bind(idx) | Op::Call(idx) => { if let Some(s) = symbols.get(idx.get()) { if let Some(off) = s.address(extent) { place(off.get()); @@ -376,6 +375,7 @@ fn load(case: &Case, placement: Placement, reached: &mut Reached) -> Result<(), } reached.bind += 1; } + Op::Absolute(..) => reached.absolute += 1, Op::Tpoff64(t) | Op::Tpoff32(t) => { if resolve(t)?.is_some() { reached.tpoff += 1; @@ -426,6 +426,7 @@ fn every_derived_address_lies_inside_the_image_or_the_image_is_refused() { for (what, n) in [ ("RELATIVE", reached.relative), ("GLOB_DAT/JUMP_SLOT", reached.bind), + ("ABS64", reached.absolute), ("TPOFF", reached.tpoff), ("DTPOFF", reached.dtpoff), ("DT_INIT_ARRAY", reached.init_arrays), diff --git a/lib/elf/tests/tables.rs b/lib/elf/tests/tables.rs index db34919df26..cdec089a003 100644 --- a/lib/elf/tests/tables.rs +++ b/lib/elf/tests/tables.rs @@ -15,7 +15,7 @@ mod common; use common::*; use toyos_elf::dynamic::{self, Dynamic}; use toyos_elf::gnu_hash::{self, GnuHash}; -use toyos_elf::rela::{self, Op, Rela, RelaCounts, RelaTable, RelocError, RelocKind, Rules}; +use toyos_elf::rela::{self, Op, Rela, 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; @@ -60,6 +60,18 @@ fn a_tag_is_absent_or_present_never_zero_meaning_absent() { /// A table needs both of its tags. `DT_RELA` alone names a table of unknown /// length, which nothing can read. +/// An image binds now if any of the three ways says so, and only the flag +/// bit that means it counts. +#[test] +fn an_image_binds_now_by_any_of_its_three_tags() { + assert!(!Dynamic::parse(&dynamic(&[])).bind_now); + assert!(Dynamic::parse(&dynamic(&[(dynamic::DT_BIND_NOW, 0)])).bind_now); + assert!(Dynamic::parse(&dynamic(&[(dynamic::DT_FLAGS, dynamic::DF_BIND_NOW)])).bind_now); + assert!(Dynamic::parse(&dynamic(&[(dynamic::DT_FLAGS_1, dynamic::DF_1_NOW)])).bind_now); + // `DF_SYMBOLIC`, which lld writes for a Rust library, and `DF_1_PIE`. + assert!(!Dynamic::parse(&dynamic(&[(dynamic::DT_FLAGS, 2), (dynamic::DT_FLAGS_1, 0x0800_0000)])).bind_now); +} + #[test] fn a_table_needs_both_of_its_tags_and_a_non_zero_size() { assert_eq!(Dynamic::parse(&dynamic(&[(dynamic::DT_RELA, 0x1000)])).rela, None); @@ -103,12 +115,13 @@ fn a_trailing_partial_relocation_is_not_a_relocation() { assert_eq!(table.iter().count(), 1); } +/// `TPOFF32` writes four bytes, so it may end where an eight-byte write may not. #[test] -fn relocation_types_map_to_the_width_the_writers_use() { - assert_eq!(RelocKind::from_raw(Machine::X86_64, 8).write_width(), Some(8)); - 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); +fn each_type_is_bounded_by_the_bytes_it_writes() { + for (raw, fits) in [(1u32, false), (8, false), (23, true)] { + let bytes = rela(0x10, 0, raw, 0); + assert_eq!(validate(RelaTable::new(&bytes, Machine::X86_64).iter(), (0, 0x14), 4).is_ok(), fits, "type {raw}"); + } } #[test] @@ -208,9 +221,9 @@ fn a_read_table_on_a_page_of_the_write_window_is_refused() { /// to cover every type the loader writes and not just the ones it applies at /// load time. #[test] -fn every_written_type_is_validated_and_no_other_is() { +fn every_written_type_is_validated() { let window = (0u64, 0x100u64); - for raw in [6u32, 7, 8, 16, 17, 18, 23] { + for raw in [1u32, 6, 7, 8, 16, 17, 18, 23] { let bytes = [rela(0x1000, 0, raw, 0)].concat(); assert_eq!( validate(RelaTable::new(&bytes, Machine::X86_64).iter(), window, 4), @@ -218,9 +231,39 @@ fn every_written_type_is_validated_and_no_other_is() { "type {raw} was not validated", ); } - // A type nobody patches may name any offset at all. - let ignored = [rela(u64::MAX, 0, 42, 0)].concat(); - assert_eq!(validate(RelaTable::new(&ignored, Machine::X86_64).iter(), window, 0), Ok(())); + // `NONE` writes nothing, so it may name any offset at all. + for machine in [Machine::X86_64, Machine::Aarch64] { + let none = [rela(u64::MAX, 0, 0, 0)].concat(); + assert_eq!(validate(RelaTable::new(&none, machine).iter(), window, 0), Ok(())); + } +} + +/// A slot whose type this loader does not apply would keep its link-time +/// value, a wrong pointer: `COPY`, `IRELATIVE` and every unknown number are +/// refused, naming the number. +#[test] +fn a_type_this_loader_does_not_apply_is_refused_by_number() { + let window = (0u64, 0x100u64); + for (machine, raw) in [(Machine::X86_64, 5u32), (Machine::X86_64, 37), (Machine::X86_64, 42), (Machine::Aarch64, 1024), (Machine::Aarch64, 1032)] { + let bytes = [rela(0x10, 0, raw, 0)].concat(); + assert_eq!(validate(RelaTable::new(&bytes, machine).iter(), window, 0), Err(RelocError::Unsupported(raw))); + } + assert_eq!(RelocError::Unsupported(37).to_string(), "ELF: relocation type 37, which this loader does not apply"); +} + +/// `R_X86_64_64` and `R_AARCH64_ABS64` are `S + A`, with `S` 0 for `r_sym == 0`. +#[test] +fn an_absolute_relocation_carries_its_symbol_and_addend() { + let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), tls: None }; + let syms = [0u8; 2 * 24]; + for (machine, raw) in [(Machine::X86_64, 1u32), (Machine::Aarch64, 257)] { + let parse = |r_sym| { + let entry = RelaTable::new(&rela(0x10, r_sym, raw, -8), machine).get(0).unwrap(); + rela::parse(entry, &rules, SymTab::new(&syms, &[])).unwrap().unwrap().op() + }; + assert_eq!(parse(0), Op::Absolute(None, -8)); + assert!(matches!(parse(1), Op::Absolute(Some(sym), -8) if sym.get() == 1)); + } } /// A TLS descriptor is filled by a resolver this loader does not have, so an @@ -236,9 +279,7 @@ fn an_aarch64_tls_descriptor_is_refused_by_name() { Err(RelocError::TlsDescriptor), ); assert!(RelocError::TlsDescriptor.as_str().contains("R_AARCH64_TLSDESC")); - assert_eq!(validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4), Ok(())); - let counts = RelaCounts::of(RelaTable::new(&desc, Machine::Aarch64).iter()); - assert_eq!(counts.count_of(RelocKind::TlsDesc), 1); + assert_eq!(validate(RelaTable::new(&desc, Machine::X86_64).iter(), window, 4), Err(RelocError::Unsupported(1031))); } #[test] @@ -247,7 +288,7 @@ fn a_symbol_index_past_the_table_is_refused_except_for_relative() { // 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] { + for raw in [1u32, 6, 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), @@ -267,7 +308,7 @@ fn a_symbol_index_past_the_table_is_refused_except_for_relative() { fn a_symbol_index_is_bounded_by_the_whole_entries_of_its_table() { let rules = Rules { extent: extent(0, 0x100), window: (0, 0x100), tls: None }; let syms = [0u8; 4 * 24 + 8]; - for raw in [6u32, 7, 16, 17, 18, 23] { + for raw in [1u32, 6, 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, &[])) @@ -277,23 +318,6 @@ fn a_symbol_index_is_bounded_by_the_whole_entries_of_its_table() { } } -#[test] -fn counts_are_per_kind_over_every_table() { - let a = [rela(0, 0, 8, 0), rela(8, 1, 6, 0), rela(16, 2, 18, 0)].concat(); - let b = [rela(24, 3, 7, 0), rela(32, 0, 23, 0), rela(40, 0, 99, 0)].concat(); - let counts = RelaCounts::of(RelaTable::new(&a, Machine::X86_64).iter().chain(RelaTable::new(&b, Machine::X86_64).iter())); - assert_eq!( - counts, - RelaCounts { relative: 1, bind: 2, tpoff64: 1, tpoff32: 1, dtpmod64: 0, dtpoff64: 0, tlsdesc: 0 }, - ); - // The ceiling is over the kinds a caller reserves for, never over every - // kind: a bound on one nothing stores refuses a file for a collection that - // does not exist. - assert_eq!(counts.max_of(&[RelocKind::GlobDat, RelocKind::Tpoff32]), 2); - assert_eq!(counts.max_of(&[RelocKind::DtpMod64]), 0); - assert_eq!(counts.max_of(&[]), 0); -} - // ── Symbol tables ─────────────────────────────────────────────────────── /// The shape this replaces sliced from `index * 24` and read 24 bytes through a diff --git a/rust b/rust index fd00d661c36..7b390729d73 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit fd00d661c36d8c1d2e2920ebe2e02d4c20c01142 +Subproject commit 7b390729d73b5e997afb18cb824fe9a2239cb285 diff --git a/sdk/libc/Cargo.lock b/sdk/libc/Cargo.lock index 2c169500537..4149f9284f6 100644 --- a/sdk/libc/Cargo.lock +++ b/sdk/libc/Cargo.lock @@ -30,6 +30,7 @@ name = "toyos" version = "0.18.0" dependencies = [ "toyos-abi", + "toyos-elf", ] [[package]] diff --git a/sdk/libc/arch/src/dlimage_relocations.rs b/sdk/libc/arch/src/dlimage_relocations.rs deleted file mode 100644 index 09638cefccf..00000000000 --- a/sdk/libc/arch/src/dlimage_relocations.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! The loader's image work on a shared object laid out here: what each -//! relocation it writes is worth, which ones it refuses, and the images it -//! refuses before any is laid out. The image is the host's machine's, so each -//! host checks its own architecture's relocation numbers. - -use crate::dlimage::{self, Mapped, Pass, Scope}; -use toyos_elf::RelocError; - -const SIZE: usize = 0x2000; -const PHDRS: usize = 0x40; -const SYMTAB: usize = 0x200; -const STRTAB: usize = 0x400; -const RELA: usize = 0x600; -const DYNAMIC: usize = 0x800; -/// The writable segment, `[0x1000, 0x2000)` in memory, its first 0x100 bytes file bytes. -const DATA: u64 = 0x1000; -const TLS: u64 = 0x1080; - -#[cfg(target_arch = "x86_64")] -mod machine { - pub const EM: u16 = 62; - pub const RELATIVE: u32 = 8; - pub const GLOB_DAT: u32 = 6; - pub const DTPMOD: u32 = 16; - pub const DTPOFF: u32 = 17; - pub const TPOFF: u32 = 18; -} -#[cfg(target_arch = "aarch64")] -mod machine { - pub const EM: u16 = 183; - pub const RELATIVE: u32 = 1027; - pub const GLOB_DAT: u32 = 1025; - pub const DTPMOD: u32 = 1028; - pub const DTPOFF: u32 = 1029; - pub const TPOFF: u32 = 1030; -} -use machine::*; - -const GLOBAL_FUNC: u8 = (1 << 4) | 2; -const GLOBAL_TLS: u8 = (1 << 4) | 6; - -/// Name, `st_info`, defined, value: 1 an import, 2 this object's -/// thread-local, 3 a function it defines, 4 another's thread-local. -const SYMBOLS: &[(&str, u8, bool, u64)] = &[ - ("", 0, false, 0), - ("imported", GLOBAL_FUNC, false, 0), - ("counter", GLOBAL_TLS, true, 8), - ("exported", GLOBAL_FUNC, true, 0x500), - ("other_tls", GLOBAL_TLS, false, 0), -]; - -/// One relocation: where, its type, its symbol and its addend. -type Entry = (u64, u32, u32, i64); - -/// A shared object, its file bytes its image: text and tables in a read-only -/// page, a writable page whose start the relocations write and whose -/// `PT_TLS` is 0x20 bytes at 0x1080, and `relocs` in a `DT_RELA` at -/// `rela_at`. `edit` changes the bytes last. -fn object(relocs: &[Entry], rela_at: usize, edit: impl FnOnce(&mut [u8])) -> Vec { - let mut bytes = vec![0u8; SIZE]; - let mut put = |at: usize, value: &[u8]| bytes[at..at + value.len()].copy_from_slice(value); - put(0, &[0x7f, b'E', b'L', b'F', 2, 1, 1]); - put(16, &3u16.to_le_bytes()); - put(18, &EM.to_le_bytes()); - put(20, &1u32.to_le_bytes()); - put(24, &0x500u64.to_le_bytes()); - put(32, &(PHDRS as u64).to_le_bytes()); - put(52, &64u16.to_le_bytes()); - put(54, &56u16.to_le_bytes()); - put(56, &4u16.to_le_bytes()); - // Type, flags, offset and vaddr, file size, memory size, alignment. - let phdrs: [(u32, u32, u64, u64, u64, u64); 4] = [ - (1, 5, 0, 0x1000, 0x1000, 0x1000), - (1, 6, DATA, 0x100, 0x1000, 0x1000), - (2, 4, DYNAMIC as u64, 0x100, 0x100, 8), - (7, 4, TLS, 0x10, 0x20, 8), - ]; - for (i, (kind, flags, at, filesz, memsz, align)) in phdrs.into_iter().enumerate() { - let p = PHDRS + i * 56; - put(p, &kind.to_le_bytes()); - put(p + 4, &flags.to_le_bytes()); - for (field, value) in [(8, at), (16, at), (24, at), (32, filesz), (40, memsz), (48, align)] { - put(p + field, &value.to_le_bytes()); - } - } - let mut names = vec![0u8]; - for (i, (name, info, defined, value)) in SYMBOLS.iter().enumerate() { - let at = SYMTAB + i * 24; - if !name.is_empty() { - put(at, &(names.len() as u32).to_le_bytes()); - names.extend_from_slice(name.as_bytes()); - names.push(0); - } - put(at + 4, &[*info]); - put(at + 6, &u16::from(*defined).to_le_bytes()); - put(at + 8, &value.to_le_bytes()); - } - put(STRTAB, &names); - for (i, &(offset, kind, sym, addend)) in relocs.iter().enumerate() { - let at = rela_at + i * 24; - put(at, &offset.to_le_bytes()); - put(at + 8, &(u64::from(sym) << 32 | u64::from(kind)).to_le_bytes()); - put(at + 16, &addend.to_le_bytes()); - } - let tags = [ - (6u64, SYMTAB as u64), - (5, STRTAB as u64), - (10, names.len() as u64), - (7, rela_at as u64), - (8, (relocs.len() * 24) as u64), - (0, 0), - ]; - for (i, (tag, value)) in tags.into_iter().enumerate() { - put(DYNAMIC + i * 16, &tag.to_le_bytes()); - put(DYNAMIC + i * 16 + 8, &value.to_le_bytes()); - } - edit(&mut bytes); - bytes -} - -/// Defines `imported` and module 7's `other_tls` at 0x10 in its block. -struct Loaded; - -impl Scope for Loaded { - fn address(&self, name: &str) -> Option { - (name == "imported").then_some(0xdead_0000) - } - - fn tls(&self, name: &str, addend: i64) -> Option> { - (name == "other_tls").then(|| Ok((7, 0x10u64.checked_add_signed(addend).unwrap()))) - } -} - -/// Defines nothing. -struct Nothing; - -impl Scope for Nothing { - fn address(&self, _: &str) -> Option { - None - } - - fn tls(&self, _: &str, _: i64) -> Option> { - None - } -} - -/// `file` laid out and relocated as module 3 against `scope`: the image's -/// words from `DATA` and how many symbols nothing defined, and the image's base. -fn load(file: &[u8], scope: &dyn Scope, pass: Pass) -> Result<(Vec, usize, u64), &'static str> { - let plan = dlimage::plan(&file[..0x1000])?; - // Whole words, so the image is 8-aligned as a mapping is page-aligned. - let mut words = vec![0u64; plan.span / 8]; - // SAFETY: the buffer's own bytes. - let image = unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), plan.span) }; - plan.lay(file, image)?; - let base = image.as_ptr() as u64; - // SAFETY: the buffer, laid out whole and writable, which nothing else reaches. - let mapped = unsafe { Mapped::new(image.as_mut_ptr(), &plan) }; - let tables = mapped.tables(true)?.expect("a PT_DYNAMIC"); - let unresolved = dlimage::relocate(&mapped, &tables, 3, scope, pass)?; - Ok((words[DATA as usize / 8..DATA as usize / 8 + 8].to_vec(), unresolved, base)) -} - -#[test] -fn each_relocation_is_worth_what_its_psabi_says() { - let relocs = [ - (DATA, RELATIVE, 0, 0x500), - (DATA + 8, GLOB_DAT, 1, 0), - (DATA + 16, GLOB_DAT, 3, 0), - (DATA + 24, DTPMOD, 0, 0), - (DATA + 32, DTPOFF, 2, 4), - (DATA + 40, DTPMOD, 4, 0), - (DATA + 48, DTPOFF, 4, 2), - (DATA + 56, DTPOFF, 0, 0x18), - ]; - let file = object(&relocs, RELA, |_| ()); - let (words, unresolved, base) = load(&file, &Loaded, Pass::Library).unwrap(); - assert_eq!(words, [base + 0x500, 0xdead_0000, base + 0x500, 3, 8 + 4, 7, 0x12, 0x18]); - assert_eq!(unresolved, 0); - - // With nothing loaded before it: an import is zero, another module's - // thread-local names this one and offset zero, each counted. - let (words, unresolved, _) = load(&file, &Nothing, Pass::Library).unwrap(); - assert_eq!((words[1], words[5], words[6]), (0, 3, 0)); - assert_eq!(unresolved, 3); -} - -#[test] -fn an_executables_pass_leaves_the_relatives_it_applied_itself() { - let file = object(&[(DATA, RELATIVE, 0, 0x500), (DATA + 8, GLOB_DAT, 1, 0)], RELA, |_| ()); - let (words, _, _) = load(&file, &Loaded, Pass::Executable).unwrap(); - assert_eq!(&words[..2], [0, 0xdead_0000]); -} - -#[test] -fn what_it_cannot_honour_is_refused_before_any_write() { - // Initial-exec TLS outside the executable's own block. - let file = object(&[(DATA, RELATIVE, 0, 0x500), (DATA + 8, TPOFF, 2, 0)], RELA, |_| ()); - assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err("ELF: initial-exec TLS outside the executable's own block")); - // A write outside the writable segment. - let file = object(&[(0x100, RELATIVE, 0, 0)], RELA, |_| ()); - assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err(RelocError::OutsideWindow.as_str())); - // An offset past this object's own thread-local storage. - let file = object(&[(DATA, DTPOFF, 2, 0x20)], RELA, |_| ()); - assert_eq!(load(&file, &Loaded, Pass::Library).map(|_| ()), Err(RelocError::TlsOutsideSegment.as_str())); -} - -#[test] -fn a_relocation_table_a_relocation_could_write_is_refused() { - let file = object(&[(DATA + 8, RELATIVE, 0, 0)], DATA as usize + 0x40, |_| ()); - assert!(load(&file, &Loaded, Pass::Library).is_err_and(|why| why.contains(".rela.dyn"))); -} - -#[test] -fn an_image_off_zero_or_with_segments_sharing_a_page_is_refused() { - // The read-only segment's vaddr moved to 0x400. - let shifted = object(&[], RELA, |b| b[PHDRS + 16..PHDRS + 24].copy_from_slice(&0x400u64.to_le_bytes())); - assert!(dlimage::plan(&shifted[..0x1000]).is_err()); - // The writable segment moved into the read-only one's last page. - let shared = object(&[], RELA, |b| b[PHDRS + 56 + 16..PHDRS + 56 + 24].copy_from_slice(&0xf00u64.to_le_bytes())); - assert_eq!(dlimage::plan(&shared[..0x1000]).map(|_| ()), Err("ELF: two PT_LOAD segments share a page")); -} diff --git a/sdk/libc/arch/src/lib.rs b/sdk/libc/arch/src/lib.rs index 99197bbde0b..9450715b9ee 100644 --- a/sdk/libc/arch/src/lib.rs +++ b/sdk/libc/arch/src/lib.rs @@ -47,11 +47,6 @@ mod errcode; #[path = "../../src/dgramreq.rs"] mod dgramreq; #[cfg(test)] -#[path = "../../src/dlimage.rs"] -// The loader's effects (`dl.rs`) use what the tests here do not. -#[allow(dead_code)] -mod dlimage; -#[cfg(test)] #[path = "../../src/elfsym.rs"] mod elfsym; #[cfg(test)] @@ -118,8 +113,6 @@ mod descriptor_requests; #[cfg(test)] mod dladdr_symbols; #[cfg(test)] -mod dlimage_relocations; -#[cfg(test)] mod errno_codes; #[cfg(test)] mod exact_hex; diff --git a/sdk/libc/arch/src/prototypes.rs b/sdk/libc/arch/src/prototypes.rs index e8369f17ce8..3dfcec7988a 100644 --- a/sdk/libc/arch/src/prototypes.rs +++ b/sdk/libc/arch/src/prototypes.rs @@ -21,7 +21,6 @@ const UNDECLARED: &[(&str, &str)] = &[ ("_Unwind_Resume", "what the precompiled `alloc`'s landing pads name"), ("__cxa_atexit", "the C++ ABI's, which `cxxabi.h` declares"), ("__cxa_thread_atexit_impl", "what libc++abi's `__cxa_thread_atexit` calls, which it declares"), - ("__toyos_load_needed", "what a program's start calls to load the libraries it names"), ]; fn libc() -> PathBuf { diff --git a/sdk/libc/src/dl.rs b/sdk/libc/src/dl.rs index 582623b4c57..7df3f341d6d 100644 --- a/sdk/libc/src/dl.rs +++ b/sdk/libc/src/dl.rs @@ -1,274 +1,20 @@ -//! The dynamic loader: `dlopen` and its kin, and the libraries an executable -//! names (`DT_NEEDED`). A library is read wherever its path is served -//! (`toyos::files::read_image`), laid out in memory of this process's own, -//! relocated ([`dlimage`]), given its protections and published in the -//! process's one module list (`toyos::tls`), which every copy of std and libc -//! in the process reads. Its constructors run once it is published, with the -//! list let go, so one may load another. -//! -//! A library is known by the absolute path it was loaded from: a second load -//! of that name answers the module the first published, and nothing leaves -//! the list, so `dlclose` releases nothing. +//! `dlopen`, `dlsym`, `dlclose` and `dlerror` over the SDK's loader +//! (`toyos::dl`): a handle is a module's id, never null, and nothing leaves +//! the module list, so `dlclose` releases nothing. -use alloc::string::String; -use alloc::vec::Vec; use core::cell::Cell; +use core::fmt::Write; use core::ptr; -use toyos::process::ImageRefused; -use toyos::tls::{self, Loading, Module}; -use toyos_abi::syscall::{self, SyscallError, VmAt, VmProt}; +use toyos::dl::{self, Refused}; -use crate::dlimage::{self, Mapped, Pass, Plan, Scope, Tables, PAGE}; - -/// Where a library a `DT_NEEDED` names by file name alone is looked for. -const LIBRARY_DIR: &str = "/system/lib/"; - -/// The bytes of an image's header a plan is read from: the page its first -/// `PT_LOAD` maps. -const HEADER: usize = PAGE as usize; - -/// Why a library was not loaded. -enum Refused { - Image(ImageRefused), - Memory(SyscallError), - Elf(&'static str), -} - -impl From<&'static str> for Refused { - fn from(why: &'static str) -> Self { - Refused::Elf(why) - } -} - -impl Refused { - /// What `dlerror` says of it. - fn text(&self) -> &'static str { - match self { - Refused::Image(_) => "dlopen: the library could not be read", - Refused::Memory(_) => "dlopen: no memory for the library", - Refused::Elf(why) => why, - } - } - - fn errno(self) -> i32 { - match self { - Refused::Image(refused) => crate::errcode::of_image(refused), - Refused::Memory(e) => crate::errcode::of(e), - Refused::Elf(_) => crate::errcode::ENOEXEC, - } - } -} - -/// A published module and its tables, which a new image's symbols bind to. -struct Known { - module: &'static Module, - tables: Tables<'static>, -} - -/// Every module published, executable first: what a load binds against. -struct Published(Vec); - -impl Published { - /// What the list `_loading` holds: a module whose tables cannot be read - /// binds nothing. - fn of(_loading: &Loading) -> Self { - Published(tls::modules().filter_map(known).collect()) - } -} - -/// `module`'s tables, `None` for one with none to read. -fn known(module: &'static Module) -> Option { - let base = module.image.base; - // SAFETY: every published module's first page is its header, mapped for - // the process's life. - let header = unsafe { core::slice::from_raw_parts(base as *const u8, HEADER) }; - let plan = dlimage::plan(header).ok()?; - // SAFETY: a published module stays mapped as its loader laid it out, and - // nothing writes its tables. - let mapped = unsafe { Mapped::new(base as *mut u8, &plan) }; - let tables = mapped.tables(false).ok()??; - Some(Known { module, tables }) -} - -impl Scope for Published { - fn address(&self, name: &str) -> Option { - self.0.iter().find_map(|k| k.tables.address(name, k.module.image.base as u64)) - } - - fn tls(&self, name: &str, addend: i64) -> Option> { - self.0.iter().filter(|k| k.module.tls.memsz != 0).find_map(|k| Some(k.tables.tls(name, addend)?.map(|at| (k.module.id(), at)))) - } -} - -/// Load the library named `name` and every one it names, depth first, into -/// the list `loading` holds: the module, and each newly loaded one's -/// constructors in `inits`, a library's after those of the ones it names. -fn open(loading: &mut Loading, published: &mut Published, name: &str, inits: &mut Vec<(usize, usize)>) -> Result<&'static Module, Refused> { - if let Some(module) = loading.find(name.as_bytes()) { - return Ok(module); - } - let file = toyos::files::read_image(name.as_bytes()).map_err(Refused::Image)?; - let bytes = file.bytes(); - let plan = dlimage::plan(&bytes[..bytes.len().min(HEADER)])?; - // Committed whole while it is laid out and relocated; each page is given - // its own protection once nothing more is written. - // SAFETY: placed anywhere, so it replaces nothing. - let base = unsafe { syscall::vm_reserve(plan.span, VmProt::ReadWrite, VmAt::ANYWHERE) }.map_err(Refused::Memory)?; - let mut reservation = Reservation { at: base, len: plan.span, published: false }; - // SAFETY: the fresh, zeroed reservation, this function's alone. - plan.lay(bytes, unsafe { core::slice::from_raw_parts_mut(base, plan.span) })?; - // SAFETY: laid out whole and writable, and reached by nothing else until published. - let mapped = unsafe { Mapped::new(base, &plan) }; - let tables = mapped.tables(true)?.ok_or("ELF: a shared object with no PT_DYNAMIC")?; - for needed in tables.needed() { - let path = if needed.contains('/') { String::from(needed) } else { [LIBRARY_DIR, needed].concat() }; - open(loading, published, &path, inits)?; - } - let id = loading.next_id(); - dlimage::relocate(&mapped, &tables, id, published, Pass::Library)?; - protect(&plan, base)?; - let start = base as usize; - let tls = plan.tls().map_or(tls::Tls::default(), |segment| tls::Tls { - template: start + segment.template().start().get() as usize, - filesz: segment.template().len() as usize, - memsz: segment.memsz() as usize, - align: segment.align() as usize, - }); - let (phdr, phnum) = plan.phdrs(); - let image = tls::Image { - base: start, - start, - end: start + plan.span, - phdr: start + phdr as usize, - phnum, - eh_frame_hdr: plan.eh_frame_hdr().map_or(0, |at| start + at as usize), - }; - let name = alloc::ffi::CString::new(name).map_err(|_| "dlopen: a library path holds a NUL")?; - let name: &'static core::ffi::CStr = alloc::boxed::Box::leak(name.into_boxed_c_str()); - reservation.published = true; - let module = loading.add(Module::new(name, image, tls)); - if let Some((at, count)) = tables.init_array { - inits.push((start + at as usize, count)); - } - published.0.push(Known { module, tables }); - Ok(module) -} - -/// A library's memory, released unless it was published. -struct Reservation { - at: *mut u8, - len: usize, - published: bool, -} - -impl Drop for Reservation { - fn drop(&mut self) { - if !self.published { - // SAFETY: a reservation nothing was published over. - let _ = unsafe { syscall::vm_release(self.at, self.len) }; - } - } -} - -/// Give each of `plan`'s segments at `base` its pages' protection, the pages -/// between them none, and `PT_GNU_RELRO`'s pages read-only: nothing writes -/// what its relocations wrote, and no page is both writable and executable. -fn protect(plan: &Plan, base: *mut u8) -> Result<(), Refused> { - let mut segments: Vec<_> = plan.segments().collect(); - segments.sort_unstable_by_key(|&(lo, _, _)| lo); - let mut covered = 0; - let give = |lo: u64, hi: u64, prot: VmProt| { - if lo >= hi { - return Ok(()); - } - // SAFETY: whole pages of the reservation `base` begins. - unsafe { syscall::vm_protect(base.add(lo as usize), (hi - lo) as usize, prot) }.map_err(Refused::Memory) - }; - for (lo, hi, flags) in segments { - give(covered, lo, VmProt::None)?; - let prot = if flags.executable() { - VmProt::ReadExec - } else if flags.writable() { - VmProt::ReadWrite - } else { - VmProt::Read - }; - give(lo, hi, prot)?; - covered = hi; - } - give(covered, plan.span as u64, VmProt::None)?; - if let Some((lo, hi)) = plan.relro() { - give(lo, hi, VmProt::Read)?; - } - Ok(()) -} - -/// Call each `(address, count)` `.init_array`'s constructors in order. -/// -/// # Safety -/// Each names a published module's `.init_array`. -unsafe fn construct(inits: &[(usize, usize)]) { - for &(at, count) in inits { - // SAFETY: the caller's; the array is `count` pointers inside the image. - let entries = unsafe { core::slice::from_raw_parts(at as *const usize, count) }; - for &entry in entries { - // The psABI's `0` and `-1` are no constructor. - if entry != 0 && entry != usize::MAX { - // SAFETY: a relocated `.init_array` entry is a constructor. - let f: extern "C" fn() = unsafe { core::mem::transmute(entry) }; - f(); - } - } - } -} - -/// Load the libraries the executable at `header` names and bind its symbols -/// to them, then run their constructors: std's and libc's start call this -/// before anything that could name such a symbol. An executable that names -/// none costs a look at its dynamic section; one whose libraries cannot be -/// had exits 127 with a line on its stderr, as `toyos::relocate` does. -/// -/// # Safety -/// `header` is the running executable's ELF header, and its thread-local -/// storage is laid out (`toyos::tls::start`). -#[no_mangle] -pub unsafe extern "C" fn __toyos_load_needed(header: *const u8) { - // SAFETY: the caller's; the page the header begins is mapped. - let page = unsafe { core::slice::from_raw_parts(header, HEADER) }; - let Ok(plan) = dlimage::plan(page) else { return }; - // SAFETY: the executable as the kernel mapped it, relocated by itself, - // whose writable window only this writes until it returns. - let mapped = unsafe { Mapped::new(header.cast_mut(), &plan) }; - let Ok(Some(tables)) = mapped.tables(false) else { return }; - if tables.needed().next().is_none() { - return; - } - let mut inits = Vec::new(); - let bound = (|| -> Result<(), Refused> { - let mut loading = tls::loading(); - let mut published = Published::of(&loading); - let tables = mapped.tables(true)?.ok_or("ELF: no PT_DYNAMIC")?; - for needed in tables.needed() { - let path = if needed.contains('/') { String::from(needed) } else { [LIBRARY_DIR, needed].concat() }; - open(&mut loading, &mut published, &path, &mut inits)?; - } - dlimage::relocate(&mapped, &tables, 1, &published, Pass::Executable)?; - Ok(()) - })(); - if bound.is_err() { - let line = b"this program's libraries could not be loaded: it cannot start\n"; - // SAFETY: a buffer of `line.len()` bytes. - let _ = unsafe { crate::posix_io::write(2, line.as_ptr(), line.len()) }; - syscall::exit(127); +/// The errno a refusal sets. +fn errno_of(refused: Refused) -> i32 { + match refused { + Refused::Read(_, why) => crate::errcode::of_image(why), + Refused::Memory(e) => crate::errcode::of(e), + Refused::Elf(_) | Refused::Relocation(_) | Refused::Undefined(_) => crate::errcode::ENOEXEC, } - // SAFETY: each a module just published. - unsafe { construct(&inits) }; -} - -/// A C handle is a module's id: never null, which is `dlopen`'s failure. -fn module_of(handle: *mut u8) -> Option<&'static Module> { - tls::modules().find(|m| m.id() == handle as usize) } #[no_mangle] @@ -280,24 +26,11 @@ pub unsafe extern "C" fn dlopen(path: *const u8, _flags: i32) -> *mut u8 { crate::errno::set(crate::errcode::ENOENT); return ptr::null_mut(); }; - let mut cwd = alloc::vec![0u8; syscall::MAX_PATH]; - let n = toyos::files::getcwd(&mut cwd); - let name = toyos::files::absolute(&String::from_utf8_lossy(&cwd[..n.min(cwd.len())]), path); - let mut inits = Vec::new(); - let opened = { - let mut loading = tls::loading(); - let mut published = Published::of(&loading); - open(&mut loading, &mut published, &name, &mut inits) - }; - match opened { - Ok(module) => { - // SAFETY: each a module just published. - unsafe { construct(&inits) }; - module.id() as *mut u8 - } + match dl::open(path) { + Ok(module) => module.id() as *mut u8, Err(refused) => { - ERROR.set(Some(refused.text())); - crate::errno::set(refused.errno()); + refuse(format_args!("dlopen: {refused}")); + crate::errno::set(errno_of(refused)); ptr::null_mut() } } @@ -313,14 +46,13 @@ pub unsafe extern "C" fn dlsym(handle: *mut u8, symbol: *const u8) -> *mut u8 { let Ok(name) = core::str::from_utf8(crate::posix_io::c_str_to_bytes(symbol)) else { return ptr::null_mut(); }; - let found = if handle.is_null() { - tls::modules().filter_map(known).find_map(|k| k.tables.address(name, k.module.image.base as u64)) - } else { - module_of(handle).and_then(known).and_then(|k| k.tables.defined(name, k.module.image.base as u64)) + let found = match handle.is_null() { + true => dl::symbol(None, name), + false => dl::module(handle as usize).and_then(|module| dl::symbol(Some(module), name)), }; found.map_or_else( || { - ERROR.set(Some("dlsym: no such symbol")); + refuse(format_args!("dlsym: no symbol `{name}`")); ptr::null_mut() }, |at| at as *mut u8, @@ -330,25 +62,49 @@ pub unsafe extern "C" fn dlsym(handle: *mut u8, symbol: *const u8) -> *mut u8 { /// Nothing leaves the module list: `0` for a handle `dlopen` answered. #[no_mangle] pub unsafe extern "C" fn dlclose(handle: *mut u8) -> i32 { - if module_of(handle).is_some() { 0 } else { -1 } + if dl::module(handle as usize).is_some() { 0 } else { -1 } } -/// The calling thread's last refusal by `dlopen` or `dlsym` that `dlerror` has not said yet. +/// Bytes of the calling thread's last refusal `dlerror` has not said yet, +/// NUL-terminated, cut to fit; 0 for none. #[thread_local] -static ERROR: Cell> = Cell::new(None); +static UNSAID: Cell = Cell::new(0); -/// Where `dlerror` writes it, NUL-terminated: valid until the thread's next `dlerror`. +/// Where the refusal is written: valid until the thread's next `dlopen` or +/// `dlsym` refusal. #[thread_local] -static mut TEXT: [u8; 128] = [0; 128]; +static mut TEXT: [u8; 256] = [0; 256]; + +/// The calling thread's own text, written once a refusal is formatted. +struct Text(usize); + +impl Write for Text { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + // SAFETY: the calling thread's own buffer; one byte is kept for the NUL. + let buf = unsafe { &mut *(&raw mut TEXT) }; + let room = buf.len() - 1 - self.0; + let n = s.len().min(room); + buf[self.0..self.0 + n].copy_from_slice(&s.as_bytes()[..n]); + self.0 += n; + Ok(()) + } +} + +/// Record `why` as the calling thread's refusal for `dlerror`. +fn refuse(why: core::fmt::Arguments<'_>) { + let mut text = Text(0); + let _ = text.write_fmt(why); + // SAFETY: the calling thread's own buffer, `text.0` short of its end. + unsafe { (*(&raw mut TEXT))[text.0] = 0 }; + UNSAID.set(text.0 + 1); +} -/// The last refusal not said yet, cut to what [`TEXT`] holds; null for none. +/// The last refusal not said yet; null for none. #[no_mangle] pub unsafe extern "C" fn dlerror() -> *const u8 { - let Some(text) = ERROR.take() else { return ptr::null() }; - // SAFETY: the calling thread's own buffer. - let buf = unsafe { &mut *(&raw mut TEXT) }; - let len = text.len().min(buf.len() - 1); - buf[..len].copy_from_slice(&text.as_bytes()[..len]); - buf[len] = 0; - buf.as_ptr() + match UNSAID.replace(0) { + 0 => ptr::null(), + // SAFETY: the calling thread's own buffer, which only it writes. + _ => unsafe { (*(&raw const TEXT)).as_ptr() }, + } } diff --git a/sdk/libc/src/lib.rs b/sdk/libc/src/lib.rs index 0d77036e407..41022a9866e 100644 --- a/sdk/libc/src/lib.rs +++ b/sdk/libc/src/lib.rs @@ -15,7 +15,6 @@ mod conversion; mod ctype; mod dgramreq; mod dl; -mod dlimage; mod elfsym; mod env; mod errcode; @@ -81,8 +80,9 @@ mod runtime { // Before the constructors and `main`, either of which may read // `environ` before any call that would make it. crate::env::init(); - // SAFETY: the executable's own header, its thread-local storage laid out. - unsafe { crate::dl::__toyos_load_needed(&raw const __ehdr_start) }; + // SAFETY: the executable's own header, its thread-local storage laid + // out, and this its main thread. + unsafe { toyos::dl::start(&raw const __ehdr_start) }; // SAFETY: the linker's bounds of the program's constructor array. unsafe { for hook in hooks(&raw const __init_array_start, &raw const __init_array_end) { diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index 5ddb29ca49c..a964c7282d8 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -224,12 +224,6 @@ extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { fn main(argc: i32, argv: *const *const u8) -> i32; static __ehdr_start: u8; } - // The C library's loader (`sdk/libc/src/dl.rs`), where the program links - // it: libtoyos_c is built from this std, so std cannot name it itself. - unsafe extern "C" { - #[linkage = "extern_weak"] - static __toyos_load_needed: Option; - } // The program goes by its `argv[0]`. let name = match argc { 0 => &[][..], @@ -243,26 +237,15 @@ extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { eh_frame::init(); crate::sys::env::init(); // Before anything that names a symbol of a library it links. - // SAFETY: a weak symbol's value, read once. - match unsafe { __toyos_load_needed } { - // SAFETY: the executable's own header, its thread-local storage laid out. - Some(load) => unsafe { load(&raw const __ehdr_start) }, - None if toyos::relocate::names_a_library() => { - // As `toyos::relocate` refuses its program: a line and 127. - let _ = crate::io::Write::write_all(&mut crate::io::stderr(), NO_LOADER); - toyos_abi::syscall::exit(127) - } - None => {} - } + // SAFETY: the executable's own header, its thread-local storage laid out, + // and this its main thread. + unsafe { toyos::dl::start(&raw const __ehdr_start) }; let code = unsafe { main(argc as i32, argv) }; crate::sys::stdio::finish(); toyos_abi::syscall::exit(code) } -/// What a program that names libraries and links no loader says as it ends. -const NO_LOADER: &[u8] = b"this program names libraries and links no loader: it cannot start\n"; - pub fn abort_internal() -> ! { toyos_abi::syscall::exit(128 + 6) // SIGABRT-like — kill entire process } diff --git a/sdk/toyos/Cargo.toml b/sdk/toyos/Cargo.toml index ac4288d067e..681280654c4 100644 --- a/sdk/toyos/Cargo.toml +++ b/sdk/toyos/Cargo.toml @@ -7,7 +7,7 @@ description = "The ToyOS userland SDK: typed handles, IPC, ports, namespaces, su repository = "https://github.com/ToyOSOrg/ToyOS" [features] -rustc-dep-of-std = ["core", "alloc", "toyos-abi/rustc-dep-of-std", "fd-table"] +rustc-dep-of-std = ["core", "alloc", "toyos-abi/rustc-dep-of-std", "toyos-elf/rustc-dep-of-std", "fd-table"] # This copy owns the process's descriptor table (`src/fd.rs`): on in std's # build and in libc's C-only build, and in no other. fd-table = [] @@ -104,6 +104,8 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("toyos") core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } alloc = { version = "1.0.0", optional = true, package = "rustc-std-workspace-alloc" } toyos-abi = { path = "../abi", default-features = false } +# The loader's ELF decoding (`dl`). +toyos-elf = { path = "../../lib/elf" } [dev-dependencies] loom = "0.7" diff --git a/sdk/libc/src/dlimage.rs b/sdk/toyos/src/dl/image/mod.rs similarity index 69% rename from sdk/libc/src/dlimage.rs rename to sdk/toyos/src/dl/image/mod.rs index 8586fd1c27f..2bdcce71de0 100644 --- a/sdk/libc/src/dlimage.rs +++ b/sdk/toyos/src/dl/image/mod.rs @@ -1,25 +1,32 @@ -//! A shared object's image, as the loader lays it out and relocates it: every -//! table it reads, through `toyos-elf`, out of the image's own mapped bytes. -//! It touches nothing but the memory it is handed, so the host tests it on -//! images it lays out (`toyos-libc-copies`). +//! An image, as the loader lays it out and relocates it: every table it +//! reads, through `toyos-elf`, out of the image's own mapped bytes. It touches +//! nothing but the memory it is handed, so the host tests it on images it lays +//! out. //! //! An image is the process's own input: a crafted one corrupts only the //! process that loads it, and is refused by name wherever `toyos-elf` or the //! rules here can tell. +#[cfg(test)] +mod tests; + +use alloc::string::String; use alloc::vec::Vec; use core::marker::PhantomData; use toyos_elf::dynamic::ENTRY_SIZE as DYN_SIZE; +use toyos_elf::sym::{STB_WEAK, STT_TLS}; use toyos_elf::{ rela, Dynamic, GnuHash, InitArray, Layout, Machine, Op, RelaTable, RelocError, SegmentFlags, Sym, SymTab, TlsRef, TlsSegment, }; +use super::Refused; + /// The page an image's protections are given in. pub(crate) const PAGE: u64 = 4096; -/// The machine this library runs on, whose images it loads. +/// The machine this program runs on, whose images it loads. #[cfg(target_arch = "x86_64")] pub(crate) const MACHINE: Machine = Machine::X86_64; #[cfg(target_arch = "aarch64")] @@ -37,18 +44,18 @@ pub(crate) struct Plan { /// The image `header` begins, `header` reaching at least past its program /// header table: one whose addresses start at zero, whose program header table /// a `PT_LOAD` maps and whose segments share no page. -pub(crate) fn plan(header: &[u8]) -> Result { +pub(crate) fn plan(header: &[u8]) -> Result { let layout = Layout::parse(header, MACHINE).map_err(|e| e.as_str())?; // Every bound below is image-relative and every `r_offset` a vaddr, so a // non-zero `vaddr_min` would shift the two against each other. if layout.extent().min() != 0 { - return Err("ELF: a shared object must begin at vaddr 0"); + return Err(Refused::Elf("ELF: an image must begin at vaddr 0")); } if layout.program_headers().is_none() { - return Err("ELF: no PT_LOAD maps the program header table"); + return Err(Refused::Elf("ELF: no PT_LOAD maps the program header table")); } if layout.overlapping_load_pages(PAGE).is_some() { - return Err("ELF: two PT_LOAD segments share a page"); + return Err(Refused::Elf("ELF: two PT_LOAD segments share a page")); } let span = layout.span().checked_next_multiple_of(PAGE).and_then(|s| usize::try_from(s).ok()); let span = span.ok_or("ELF: an image span no address space holds")?; @@ -56,9 +63,18 @@ pub(crate) fn plan(header: &[u8]) -> Result { } impl Plan { + /// A library's segments, which the loader protects as their flags say: + /// one both writable and executable is refused rather than given either. + pub(crate) fn library(&self) -> Result<(), Refused> { + match self.layout.segments().iter().any(|seg| seg.flags().writable() && seg.flags().executable()) { + true => Err(Refused::Elf("ELF: a library's PT_LOAD is both writable and executable")), + false => Ok(()), + } + } + /// Copy every `PT_LOAD`'s file bytes from `file` into `image`, its first /// `span` bytes zeroed and writable. - pub(crate) fn lay(&self, file: &[u8], image: &mut [u8]) -> Result<(), &'static str> { + pub(crate) fn lay(&self, file: &[u8], image: &mut [u8]) -> Result<(), Refused> { for seg in self.layout.segments() { let at = seg.image().start().get() as usize; let len = seg.filesz() as usize; @@ -141,8 +157,8 @@ impl<'p, 'm> Mapped<'p, 'm> { /// image with no `PT_DYNAMIC`. `check`ed, every symbol it defines lies in /// it and no table on a page a relocation writes, as [`relocate`] needs: a /// module already published was checked as it was loaded. - pub(crate) fn tables(&self, check: bool) -> Result>, &'static str> { - const OUTSIDE: &str = "ELF: a dynamic table outside the image's segments"; + pub(crate) fn tables(&self, check: bool) -> Result>, Refused> { + const OUTSIDE: Refused = Refused::Elf("ELF: a dynamic table outside the image's segments"); let Some(dynamic) = self.plan.layout.dynamic() else { return Ok(None) }; let range = dynamic.image(); let dynamic = self.view(range.start().get(), range.len()).ok_or(OUTSIDE)?; @@ -187,6 +203,7 @@ impl<'p, 'm> Mapped<'p, 'm> { rela, jmprel, rules: rela::Rules { extent, window, tls: self.plan.layout.tls() }, + bind_now: parsed.bind_now, init_array: init_array.map(|a| (a.range().start().get(), a.count() as usize)), })) } @@ -203,6 +220,7 @@ impl<'p, 'm> Mapped<'p, 'm> { } /// An image's dynamic tables, each a view of its mapped bytes. +#[derive(Clone)] pub(crate) struct Tables<'a> { dynamic: &'a [u8], symbols: SymTab<'a>, @@ -210,6 +228,8 @@ pub(crate) struct Tables<'a> { rela: Option<&'a [u8]>, jmprel: Option<&'a [u8]>, rules: rela::Rules, + /// Whether a call's slot is bound as the image is loaded, not at the call. + bind_now: bool, /// `DT_INIT_ARRAY`'s image offset and entries. pub(crate) init_array: Option<(u64, usize)>, } @@ -239,8 +259,9 @@ impl<'a> Tables<'a> { sym.is_exported().then_some(sym) } - /// The address this image, mapped at `base`, gives the non-TLS symbol it - /// defines as `name`: any defined one, as `dlsym` reads it. + /// The address this image, mapped at `base`, gives the symbol it defines + /// as `name`: any defined one, as `dlsym` reads it, but a thread-local, + /// which has no one address. pub(crate) fn defined(&self, name: &str, base: u64) -> Option { let (_, sym) = self.symbols.find(name)?; Some(base + sym.address(self.rules.extent)?.get()) @@ -253,7 +274,7 @@ impl<'a> Tables<'a> { /// `S + A` of the TLS symbol this image exports as `name`, in its block. pub(crate) fn tls(&self, name: &str, addend: i64) -> Option> { - let sym = self.exported(name).filter(|s| s.kind() == toyos_elf::sym::STT_TLS)?; + let sym = self.exported(name).filter(|s| s.kind() == STT_TLS)?; let segment = self.rules.tls?; Some(sym.tls_offset(addend, segment).map(|at| at.get()).ok_or(RelocError::TlsOutsideSegment)) } @@ -276,75 +297,91 @@ pub(crate) trait Scope { /// Which of an image's relocations this pass writes: a library's, all; an /// executable's, all but the `RELATIVE`s it applied to itself. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum Pass { +pub(crate) enum Pass<'b> { Library, - Executable, + /// `block` is the main thread's block of the executable's thread-locals, + /// copied from its template before this pass: what the pass writes into + /// the template it writes there too. No other thread exists yet. + Executable { block: &'b mut [u8] }, } /// Apply `tables`' relocations to `mapped`, module `id` of the process, /// against `scope` and then the image's own definitions. Every value is /// computed before the first write, so a refused image is left as laid. -/// Answers how many name a symbol nothing defines, each written as zero, or -/// for a `DTPMOD`, as the image's own id. -pub(crate) fn relocate(mapped: &Mapped<'_, '_>, tables: &Tables<'_>, id: usize, scope: &dyn Scope, pass: Pass) -> Result { +/// +/// A symbol nothing defines is refused by name, but one the image names +/// weakly, whose address is 0, and a call's slot (`JUMP_SLOT`) in an image +/// not bound now: a lazy loader would bind that at the first call and fail +/// there, so it is bound to [`super::undefined_function`], which fails there +/// too. A Rust library's slot for `main` is one, which its std's `_start` +/// names and only an executable defines. +pub(crate) fn relocate(mapped: &Mapped<'_, '_>, tables: &Tables<'_>, id: usize, scope: &dyn Scope, pass: Pass<'_>) -> Result<(), Refused> { let base = mapped.base(); let symbol = |i: toyos_elf::SymIndex| -> (Sym, &str) { - let sym = tables.symbols.get(i.get()).expect("dlimage: a parse bounded the index by the table"); + let sym = tables.symbols.get(i.get()).expect("dl: a parse bounded the index by the table"); (sym, sym.name_in(tables.symbols.strings())) }; - let own_tls = |at: toyos_elf::TlsOffset| at.get(); - let mut unresolved = 0; + let undefined = |name: &str| Refused::Undefined(String::from(name)); + let address = |i: toyos_elf::SymIndex| -> Result { + let (sym, name) = symbol(i); + let own = sym.address(tables.rules.extent).map(|at| base + at.get()); + match scope.address(name).or(own) { + Some(at) => Ok(at), + None if sym.bind() == STB_WEAK => Ok(0), + None => Err(undefined(name)), + } + }; + let elsewhere = |name: &str, addend: i64| -> Result<(usize, u64), Refused> { + scope.tls(name, addend).ok_or_else(|| undefined(name))?.map_err(Refused::Relocation) + }; + let own_block = || tables.rules.tls.ok_or(Refused::Relocation(RelocError::TlsOutsideSegment)); + let executable = matches!(pass, Pass::Executable { .. }); let mut writes = Vec::new(); for raw in tables.relocations() { - let Some(r) = rela::parse(raw, &tables.rules, tables.symbols).map_err(|e| e.as_str())? else { continue }; + let Some(r) = rela::parse(raw, &tables.rules, tables.symbols).map_err(Refused::Relocation)? else { continue }; let value: u64 = match r.op() { - Op::Relative(_) if pass == Pass::Executable => continue, + Op::Relative(_) if executable => continue, Op::Relative(target) => base + target.get(), - Op::Bind(i) => { - let (sym, name) = symbol(i); - let own = sym.address(tables.rules.extent).map(|at| base + at.get()); - match scope.address(name).or(own) { - Some(at) => at, - None => { - unresolved += 1; - 0 - } - } - } - Op::Tpoff64(_) | Op::Tpoff32(_) => return Err("ELF: initial-exec TLS outside the executable's own block"), - Op::DtpMod64(i) => match i.map(symbol) { - None => id as u64, - Some((sym, _)) if sym.is_defined() => id as u64, - Some((_, name)) => match scope.tls(name, 0) { - Some(found) => found.map_err(|e| e.as_str())?.0 as u64, - None => { - unresolved += 1; - id as u64 - } - }, + Op::Bind(i) => address(i)?, + Op::Call(i) => match address(i) { + Err(Refused::Undefined(_)) if !tables.bind_now => (super::undefined_function as *const ()).addr() as u64, + bound => bound?, + }, + Op::Absolute(i, addend) => i.map_or(Ok(0), address)?.wrapping_add_signed(addend), + Op::Tpoff64(_) | Op::Tpoff32(_) => return Err(Refused::Elf("ELF: initial-exec TLS outside the executable's own block")), + Op::DtpMod64(None) => id as u64, + Op::DtpMod64(Some(i)) => match symbol(i) { + (sym, _) if sym.is_defined() => id as u64, + (_, name) => elsewhere(name, 0)?.0 as u64, }, - Op::DtpOff64(TlsRef::Own(at)) => own_tls(at), - Op::DtpOff64(TlsRef::Symbol(s)) => { - let (sym, name) = symbol(s.sym()); - if sym.is_defined() { - let segment = tables.rules.tls.ok_or(RelocError::TlsOutsideSegment.as_str())?; - sym.tls_offset(s.addend(), segment).ok_or(RelocError::TlsOutsideSegment.as_str())?.get() - } else { - match scope.tls(name, s.addend()) { - Some(found) => found.map_err(|e| e.as_str())?.1, - None => { - unresolved += 1; - 0 - } - } + Op::DtpOff64(TlsRef::Own(at)) => at.get(), + Op::DtpOff64(TlsRef::Symbol(s)) => match symbol(s.sym()) { + (sym, _) if sym.is_defined() => { + let at = sym.tls_offset(s.addend(), own_block()?); + at.ok_or(Refused::Relocation(RelocError::TlsOutsideSegment))?.get() } - } + (_, name) => elsewhere(name, s.addend())?.1, + }, }; writes.push((r.offset(), value)); } + let template = mapped.plan.tls().map(|segment| segment.template()); + let mut block = match pass { + Pass::Executable { block } => Some(block), + Pass::Library => None, + }; for (at, value) in writes { - mapped.write(at, &value.to_le_bytes()); + let bytes = value.to_le_bytes(); + mapped.write(at, &bytes); + if let (Some(block), Some(template)) = (block.as_deref_mut(), template) { + let (lo, hi) = (template.start().get(), template.end().get()); + for (i, byte) in bytes.iter().enumerate() { + let at = at + i as u64; + if (lo..hi).contains(&at) { + block[(at - lo) as usize] = *byte; + } + } + } } - Ok(unresolved) + Ok(()) } diff --git a/sdk/toyos/src/dl/image/tests.rs b/sdk/toyos/src/dl/image/tests.rs new file mode 100644 index 00000000000..b42f7bd8e8f --- /dev/null +++ b/sdk/toyos/src/dl/image/tests.rs @@ -0,0 +1,334 @@ +//! The loader's image work on a shared object laid out here: what each +//! relocation it writes is worth, which ones it refuses, and the images it +//! refuses before any is laid out. The image is the host's machine's, so each +//! host checks its own architecture's relocation numbers. + +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::arithmetic_side_effects)] + +use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; + +use super::{plan, relocate, Mapped, Pass, Scope}; +use crate::dl::Refused; +use toyos_elf::RelocError; + +const SIZE: usize = 0x2000; +const PHDRS: usize = 0x40; +const SYMTAB: usize = 0x200; +const STRTAB: usize = 0x400; +const RELA: usize = 0x600; +const DYNAMIC: usize = 0x800; +/// The writable segment, `[0x1000, 0x2000)` in memory, its first 0x100 bytes file bytes. +const DATA: u64 = 0x1000; +const TLS: u64 = 0x1080; + +#[cfg(target_arch = "x86_64")] +mod machine { + pub const EM: u16 = 62; + pub const ABS64: u32 = 1; + pub const JUMP_SLOT: u32 = 7; + pub const IRELATIVE: u32 = 37; + pub const RELATIVE: u32 = 8; + pub const GLOB_DAT: u32 = 6; + pub const DTPMOD: u32 = 16; + pub const DTPOFF: u32 = 17; + pub const TPOFF: u32 = 18; +} +#[cfg(target_arch = "aarch64")] +mod machine { + pub const EM: u16 = 183; + pub const ABS64: u32 = 257; + pub const JUMP_SLOT: u32 = 1026; + pub const IRELATIVE: u32 = 1032; + pub const RELATIVE: u32 = 1027; + pub const GLOB_DAT: u32 = 1025; + pub const DTPMOD: u32 = 1028; + pub const DTPOFF: u32 = 1029; + pub const TPOFF: u32 = 1030; +} +use machine::*; + +const GLOBAL_FUNC: u8 = (1 << 4) | 2; +const WEAK_FUNC: u8 = (2 << 4) | 2; +const GLOBAL_TLS: u8 = (1 << 4) | 6; + +/// Name, `st_info`, defined, value: 1 an import, 2 this object's +/// thread-local, 3 a function it defines, 4 another's thread-local, 5 a weak +/// import. +const SYMBOLS: &[(&str, u8, bool, u64)] = &[ + ("", 0, false, 0), + ("imported", GLOBAL_FUNC, false, 0), + ("counter", GLOBAL_TLS, true, 8), + ("exported", GLOBAL_FUNC, true, 0x500), + ("other_tls", GLOBAL_TLS, false, 0), + ("maybe", WEAK_FUNC, false, 0), +]; + +/// One relocation: where, its type, its symbol and its addend. +type Entry = (u64, u32, u32, i64); + +/// A shared object, its file bytes its image: text and tables in a read-only +/// page, a writable page whose start the relocations write and whose +/// `PT_TLS` is 0x20 bytes at 0x1080, 0x10 of them its template, and `relocs` +/// in a `DT_RELA` at `rela_at`. `edit` changes the bytes last. +fn object(relocs: &[Entry], rela_at: usize, edit: impl FnOnce(&mut [u8])) -> Vec { + let mut bytes = vec![0u8; SIZE]; + let mut put = |at: usize, value: &[u8]| bytes[at..at + value.len()].copy_from_slice(value); + put(0, &[0x7f, b'E', b'L', b'F', 2, 1, 1]); + put(16, &3u16.to_le_bytes()); + put(18, &EM.to_le_bytes()); + put(20, &1u32.to_le_bytes()); + put(24, &0x500u64.to_le_bytes()); + put(32, &(PHDRS as u64).to_le_bytes()); + put(52, &64u16.to_le_bytes()); + put(54, &56u16.to_le_bytes()); + put(56, &4u16.to_le_bytes()); + // Type, flags, offset and vaddr, file size, memory size, alignment. + let phdrs: [(u32, u32, u64, u64, u64, u64); 4] = [ + (1, 5, 0, 0x1000, 0x1000, 0x1000), + (1, 6, DATA, 0x100, 0x1000, 0x1000), + (2, 4, DYNAMIC as u64, 0x100, 0x100, 8), + (7, 4, TLS, 0x10, 0x20, 8), + ]; + for (i, (kind, flags, at, filesz, memsz, align)) in phdrs.into_iter().enumerate() { + let p = PHDRS + i * 56; + put(p, &kind.to_le_bytes()); + put(p + 4, &flags.to_le_bytes()); + for (field, value) in [(8, at), (16, at), (24, at), (32, filesz), (40, memsz), (48, align)] { + put(p + field, &value.to_le_bytes()); + } + } + let mut names = vec![0u8]; + for (i, (name, info, defined, value)) in SYMBOLS.iter().enumerate() { + let at = SYMTAB + i * 24; + if !name.is_empty() { + put(at, &(names.len() as u32).to_le_bytes()); + names.extend_from_slice(name.as_bytes()); + names.push(0); + } + put(at + 4, &[*info]); + put(at + 6, &u16::from(*defined).to_le_bytes()); + put(at + 8, &value.to_le_bytes()); + } + put(STRTAB, &names); + for (i, &(offset, kind, sym, addend)) in relocs.iter().enumerate() { + let at = rela_at + i * 24; + put(at, &offset.to_le_bytes()); + put(at + 8, &(u64::from(sym) << 32 | u64::from(kind)).to_le_bytes()); + put(at + 16, &addend.to_le_bytes()); + } + let tags = [ + (6u64, SYMTAB as u64), + (5, STRTAB as u64), + (10, names.len() as u64), + (7, rela_at as u64), + (8, (relocs.len() * 24) as u64), + (0, 0), + ]; + for (i, (tag, value)) in tags.into_iter().enumerate() { + put(DYNAMIC + i * 16, &tag.to_le_bytes()); + put(DYNAMIC + i * 16 + 8, &value.to_le_bytes()); + } + edit(&mut bytes); + bytes +} + +/// Defines `imported` and module 7's `other_tls` at 0x10 in its block. +struct Loaded; + +impl Scope for Loaded { + fn address(&self, name: &str) -> Option { + (name == "imported").then_some(0xdead_0000) + } + + fn tls(&self, name: &str, addend: i64) -> Option> { + (name == "other_tls").then(|| Ok((7, 0x10u64.checked_add_signed(addend).unwrap()))) + } +} + +/// Defines nothing. +struct Nothing; + +impl Scope for Nothing { + fn address(&self, _: &str) -> Option { + None + } + + fn tls(&self, _: &str, _: i64) -> Option> { + None + } +} + +/// The words a load leaves from `DATA`, as many as the relocations write. +const WORDS: usize = 20; + +/// `file` laid out and relocated as module 3 against `scope`: the image's +/// words from `DATA`, whether or not it was refused, the refusal, and the +/// image's base. +fn load(file: &[u8], scope: &dyn Scope, pass: Pass<'_>) -> (Vec, Result<(), Refused>, u64) { + let plan = plan(&file[..0x1000]).unwrap(); + // Whole words, so the image is 8-aligned as a mapping is page-aligned. + let mut words = vec![0u64; plan.span / 8]; + // SAFETY: the buffer's own bytes. + let image = unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), plan.span) }; + plan.lay(file, image).unwrap(); + let base = image.as_ptr() as u64; + // SAFETY: the buffer, laid out whole and writable, which nothing else reaches. + let mapped = unsafe { Mapped::new(image.as_mut_ptr(), &plan) }; + let relocated = mapped.tables(true).and_then(|tables| relocate(&mapped, &tables.expect("a PT_DYNAMIC"), 3, scope, pass)); + (words[DATA as usize / 8..DATA as usize / 8 + WORDS].to_vec(), relocated, base) +} + +/// The words `file` relocates to as a library against `scope`, or why it was refused. +fn library(file: &[u8], scope: &dyn Scope) -> Result<(Vec, u64), String> { + let (words, relocated, base) = load(file, scope, Pass::Library); + relocated.map(|()| (words, base)).map_err(|why| why.to_string()) +} + +#[test] +fn each_relocation_is_worth_what_its_psabi_says() { + let relocs = [ + (DATA, RELATIVE, 0, 0x500), + (DATA + 8, GLOB_DAT, 1, 0), + (DATA + 16, GLOB_DAT, 3, 0), + (DATA + 24, DTPMOD, 0, 0), + (DATA + 32, DTPOFF, 2, 4), + (DATA + 40, DTPMOD, 4, 0), + (DATA + 48, DTPOFF, 4, 2), + (DATA + 56, DTPOFF, 0, 0x18), + (DATA + 64, ABS64, 1, 8), + (DATA + 72, ABS64, 0, 0x1234), + (DATA + 80, ABS64, 3, -0x10), + ]; + let file = object(&relocs, RELA, |_| ()); + let (words, base) = library(&file, &Loaded).unwrap(); + assert_eq!( + words[..11], + [base + 0x500, 0xdead_0000, base + 0x500, 3, 8 + 4, 7, 0x12, 0x18, 0xdead_0008, 0x1234, base + 0x4f0] + ); +} + +/// A strong name nothing defines would leave a wrong pointer, or alias +/// another module's thread-local onto this one's: each is refused, naming +/// the symbol, before any write. A weak one is 0. +#[test] +fn a_symbol_nothing_defines_is_refused_by_name_unless_it_is_weak() { + for (entry, name) in [ + ((DATA + 8, GLOB_DAT, 1, 0), "imported"), + ((DATA + 8, ABS64, 1, 0), "imported"), + ((DATA + 8, DTPMOD, 4, 0), "other_tls"), + ((DATA + 8, DTPOFF, 4, 0), "other_tls"), + ] { + let file = object(&[(DATA, RELATIVE, 0, 0x500), entry], RELA, |_| ()); + let (words, relocated, _) = load(&file, &Nothing, Pass::Library); + assert!(matches!(&relocated, Err(Refused::Undefined(n)) if n == name), "{entry:?}: {relocated:?}"); + assert_eq!(relocated.unwrap_err().to_string(), alloc::format!("symbol `{name}` is defined by no loaded module")); + assert_eq!(words[0], 0, "{entry:?} wrote before its refusal"); + } + let file = object(&[(DATA, GLOB_DAT, 5, 0), (DATA + 8, ABS64, 5, 4)], RELA, |_| ()); + assert_eq!(library(&file, &Nothing).unwrap().0[..2], [0, 4]); +} + +/// A call's slot nothing defines is bound lazily, as the psABI lets a loader: +/// it fails at the call, not the load. An image that binds now +/// (`DT_BIND_NOW`) is refused at the load, naming it. +#[test] +fn a_call_nothing_defines_fails_at_the_call_unless_the_image_binds_now() { + let file = object(&[(DATA, JUMP_SLOT, 1, 0), (DATA + 8, JUMP_SLOT, 3, 0)], RELA, |_| ()); + let (words, base) = library(&file, &Nothing).unwrap(); + assert_eq!(words[..2], [(crate::dl::undefined_function as *const ()).addr() as u64, base + 0x500]); + // `DT_BIND_NOW` where `DT_NULL` was, and `DT_NULL` after it. + let now = object(&[(DATA, JUMP_SLOT, 1, 0)], RELA, |b| { + b[DYNAMIC + 5 * 16..DYNAMIC + 6 * 16].copy_from_slice(&[24u64.to_le_bytes(), [0; 8]].concat()); + b[DYNAMIC + 6 * 16..DYNAMIC + 7 * 16].fill(0); + }); + assert_eq!(library(&now, &Nothing).err().as_deref(), Some("symbol `imported` is defined by no loaded module")); +} + +#[test] +fn an_executables_pass_leaves_the_relatives_it_applied_itself() { + let file = object(&[(DATA, RELATIVE, 0, 0x500), (DATA + 8, GLOB_DAT, 1, 0)], RELA, |_| ()); + let (words, relocated, _) = load(&file, &Loaded, Pass::Executable { block: &mut [] }); + relocated.unwrap(); + assert_eq!(words[..2], [0, 0xdead_0000]); +} + +/// The main thread's block was copied from the template before the +/// executable's pass, so a value the pass writes into the template is written +/// into that block too, at the same offset; one beside the template is not. +#[test] +fn an_executables_pass_writes_into_the_main_threads_block_what_it_writes_into_the_template() { + let file = object(&[(TLS, GLOB_DAT, 1, 0), (TLS + 8, ABS64, 1, 4), (TLS + 0x10, GLOB_DAT, 1, 0)], RELA, |_| ()); + let mut block = [0u8; 0x10]; + let (words, relocated, _) = load(&file, &Loaded, Pass::Executable { block: &mut block }); + relocated.unwrap(); + let image_at = (TLS - DATA) as usize / 8; + assert_eq!(words[image_at..image_at + 3], [0xdead_0000, 0xdead_0004, 0xdead_0000]); + assert_eq!(block[..8], 0xdead_0000u64.to_le_bytes()); + assert_eq!(block[8..], 0xdead_0004u64.to_le_bytes()); +} + +#[test] +fn what_it_cannot_honour_is_refused_before_any_write() { + for (entry, why) in [ + // Initial-exec TLS outside the executable's own block. + ((DATA + 8, TPOFF, 2, 0), "ELF: initial-exec TLS outside the executable's own block".to_string()), + // A write outside the writable segment. + ((0x100, RELATIVE, 0, 0), RelocError::OutsideWindow.to_string()), + // An offset past this object's own thread-local storage. + ((DATA + 8, DTPOFF, 2, 0x20), RelocError::TlsOutsideSegment.to_string()), + // A type this loader does not apply. + ((DATA + 8, IRELATIVE, 0, 0x500), RelocError::Unsupported(IRELATIVE).to_string()), + ] { + let file = object(&[(DATA, RELATIVE, 0, 0x500), entry], RELA, |_| ()); + let (words, relocated, _) = load(&file, &Loaded, Pass::Library); + assert_eq!(relocated.map_err(|e| e.to_string()), Err(why)); + assert_eq!(words[0], 0, "{entry:?} wrote before its refusal"); + } +} + +#[test] +fn a_relocation_table_a_relocation_could_write_is_refused() { + let file = object(&[(DATA + 8, RELATIVE, 0, 0)], DATA as usize + 0x40, |_| ()); + assert!(library(&file, &Loaded).is_err_and(|why| why.contains(".rela.dyn"))); +} + +#[test] +fn an_image_off_zero_or_with_segments_sharing_a_page_is_refused() { + // The read-only segment's vaddr moved to 0x400. + let shifted = object(&[], RELA, |b| b[PHDRS + 16..PHDRS + 24].copy_from_slice(&0x400u64.to_le_bytes())); + assert!(plan(&shifted[..0x1000]).is_err()); + // The writable segment moved into the read-only one's last page. + let shared = object(&[], RELA, |b| b[PHDRS + 56 + 16..PHDRS + 56 + 24].copy_from_slice(&0xf00u64.to_le_bytes())); + assert_eq!(plan(&shared[..0x1000]).map(|_| ()).map_err(|e| e.to_string()), Err("ELF: two PT_LOAD segments share a page".to_string())); +} + +/// A library's segment both writable and executable is refused rather than +/// given either protection. +#[test] +fn a_library_segment_both_writable_and_executable_is_refused() { + assert!(plan(&object(&[], RELA, |_| ())[..0x1000]).unwrap().library().is_ok()); + // The writable segment's flags, `PF_R | PF_W | PF_X`. + let wx = object(&[], RELA, |b| b[PHDRS + 56 + 4..PHDRS + 56 + 8].copy_from_slice(&7u32.to_le_bytes())); + let refused = plan(&wx[..0x1000]).unwrap().library().map_err(|e| e.to_string()); + assert_eq!(refused, Err("ELF: a library's PT_LOAD is both writable and executable".to_string())); +} + +/// `dlsym` with a handle answers any symbol the module defines, but a +/// thread-local, whose address differs per thread. +#[test] +fn a_modules_thread_local_has_no_one_address() { + let file = object(&[], RELA, |_| ()); + let plan = plan(&file[..0x1000]).unwrap(); + let mut words = vec![0u64; plan.span / 8]; + // SAFETY: the buffer's own bytes. + let image = unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), plan.span) }; + plan.lay(&file, image).unwrap(); + // SAFETY: the buffer, laid out whole, which nothing else reaches. + let mapped = unsafe { Mapped::new(image.as_mut_ptr(), &plan) }; + let tables = mapped.tables(true).unwrap().unwrap(); + assert_eq!(tables.defined("exported", 0x10_0000), Some(0x10_0500)); + assert_eq!(tables.defined("counter", 0x10_0000), None); + assert_eq!(tables.defined("imported", 0x10_0000), None); +} diff --git a/sdk/toyos/src/dl/mod.rs b/sdk/toyos/src/dl/mod.rs new file mode 100644 index 00000000000..3e9352572bc --- /dev/null +++ b/sdk/toyos/src/dl/mod.rs @@ -0,0 +1,458 @@ +//! The dynamic loader: a library a program opens ([`open`]), and those its +//! executable names (`DT_NEEDED`, [`start`]), laid out in memory of the +//! process's own, relocated ([`image`]), given their protections and +//! published in the process's one module list ([`crate::tls`]), which every +//! copy of std and libc in the process reads. +//! +//! **A load is one set**: the libraries named and every library they name that +//! is not yet published, found breadth first, so a cycle of names is one set +//! and loads once. Each is relocated against the published modules and then +//! the set's, in load order. Nothing of the set is published until all of it +//! is relocated and protected, and a refusal releases all of it. Constructors +//! run once the set is published and the list let go, so one may load +//! another: a library's after those of the libraries it names. +//! +//! A library is known by the absolute path it was loaded from: a second load +//! of that name answers the module the first published, and nothing leaves +//! the list. + +mod image; +#[cfg(test)] +mod tests; + +use alloc::collections::VecDeque; +use alloc::ffi::CString; +use alloc::string::String; +use alloc::vec::Vec; +use core::ffi::CStr; +use core::fmt; + +use toyos_abi::syscall::{self, SyscallError, VmAt, VmProt}; +use toyos_elf::RelocError; + +use crate::files; +use crate::process::ImageRefused; +use crate::tls::{self, Loading, Module}; +use image::{Mapped, Pass, Plan, Scope, Tables, PAGE}; + +/// Where a library named by file name alone is looked for. +const LIBRARY_DIR: &str = "/system/lib/"; + +/// The bytes of an image's header a plan is read from: the page its first +/// `PT_LOAD` maps. +const HEADER: usize = PAGE as usize; + +/// Why a load was refused, as `dlerror` says it. +#[derive(Debug)] +pub enum Refused { + /// The library at this path could not be read. + Read(String, ImageRefused), + /// No memory to lay a library out in. + Memory(SyscallError), + /// An image `toyos-elf` or the loader's own rules refuse. + Elf(&'static str), + /// A relocation the loader does not apply. + Relocation(RelocError), + /// A symbol an image binds to that no module defines, and that it + /// neither names weakly nor calls lazily. + Undefined(String), +} + +impl From<&'static str> for Refused { + fn from(why: &'static str) -> Self { + Refused::Elf(why) + } +} + +impl fmt::Display for Refused { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Refused::Read(path, why) => write!(f, "{path}: could not be read: {why:?}"), + Refused::Memory(e) => write!(f, "no memory for a library: {e}"), + Refused::Elf(why) => f.write_str(why), + Refused::Relocation(e) => write!(f, "{e}"), + Refused::Undefined(name) => write!(f, "symbol `{name}` is defined by no loaded module"), + } + } +} + +/// What a lazily bound call nothing defines lands in: the call fails, as a +/// lazy loader's first call to it would, with a line on stderr and 127. +extern "C" fn undefined_function() -> ! { + let line = b"a library called a function no loaded module defines\n"; + if let Some(stderr) = crate::relocate::stderr() { + let _ = syscall::write(stderr, line); + } + syscall::exit(127) +} + +/// A module whose definitions a load binds to: published, or in the set. +struct Bound<'m> { + id: usize, + base: u64, + tables: Tables<'m>, + tls: bool, +} + +/// Every module a load binds against, the executable first, in load order. +struct Global<'m>(Vec>); + +impl Scope for Global<'_> { + fn address(&self, name: &str) -> Option { + self.0.iter().find_map(|m| m.tables.address(name, m.base)) + } + + fn tls(&self, name: &str, addend: i64) -> Option> { + self.0.iter().filter(|m| m.tls).find_map(|m| Some(m.tables.tls(name, addend)?.map(|at| (m.id, at)))) + } +} + +/// `module`'s tables, `None` for one with none to read. +fn bound(module: &'static Module) -> Option> { + let base = module.image.base; + // SAFETY: every published module's first page is its header, mapped for + // the process's life. + let header = unsafe { core::slice::from_raw_parts(base as *const u8, HEADER) }; + let plan = image::plan(header).ok()?; + // SAFETY: a published module stays mapped as its loader laid it out, and + // nothing writes its tables. + let mapped = unsafe { Mapped::new(base as *mut u8, &plan) }; + let tables = mapped.tables(false).ok()??; + Some(Bound { id: module.id(), base: base as u64, tables, tls: module.tls.memsz != 0 }) +} + +/// A library of a set, by the path it is loaded from, and the paths it +/// names, in its order. +struct Node { + name: String, + needed: Vec, + laid: T, +} + +/// A library laid out and not yet published. +struct Laid { + plan: Plan, + memory: Reservation, + tables: Tables<'static>, +} + +/// The set a load lays out with `lay`: `roots` and every library they name, +/// breadth first, each once, but those already `published`. +fn discover( + roots: impl IntoIterator, + published: impl Fn(&str) -> bool, + mut lay: impl FnMut(&str) -> Result<(T, Vec), Refused>, +) -> Result>, Refused> { + let mut set: Vec> = Vec::new(); + let mut queue: VecDeque = roots.into_iter().collect(); + while let Some(name) = queue.pop_front() { + if published(&name) || set.iter().any(|node| node.name == name) { + continue; + } + let (laid, needed) = lay(&name)?; + queue.extend(needed.iter().cloned()); + set.push(Node { name, needed, laid }); + } + Ok(set) +} + +/// The order `set`'s constructors run in, by index: each library after the +/// set's libraries it names, depth first in the order it names them, each +/// once however many name it. +fn order(set: &[Node]) -> Vec { + let index = |name: &str| set.iter().position(|node| node.name == name); + let mut seen = alloc::vec![false; set.len()]; + let mut order = Vec::with_capacity(set.len()); + // Each frame a library and how many of its names were visited. + let mut stack: Vec<(usize, usize)> = Vec::new(); + for root in 0..set.len() { + if seen[root] { + continue; + } + seen[root] = true; + stack.push((root, 0)); + while let Some(&(at, next)) = stack.last() { + match set[at].needed.get(next) { + Some(name) => { + if let Some(top) = stack.last_mut() { + top.1 += 1; + } + if let Some(dep) = index(name).filter(|&dep| !seen[dep]) { + seen[dep] = true; + stack.push((dep, 0)); + } + } + None => { + stack.pop(); + order.push(at); + } + } + } + } + order +} + +/// The path a `DT_NEEDED` name is loaded from. +fn path_of(needed: &str) -> Result { + match needed { + "" => Err(Refused::Elf("ELF: a DT_NEEDED names no library")), + _ if needed.contains('/') => Ok(String::from(needed)), + _ => Ok([LIBRARY_DIR, needed].concat()), + } +} + +/// Read the library at `name` and lay it out in memory of its own, its +/// tables checked: it, and the paths it names. +fn lay(name: &str) -> Result<(Laid, Vec), Refused> { + if name.contains('\0') { + return Err(Refused::Elf("dlopen: a library path holds a NUL")); + } + let file = files::read_image(name.as_bytes()).map_err(|why| Refused::Read(String::from(name), why))?; + let bytes = file.bytes(); + let plan = image::plan(&bytes[..bytes.len().min(HEADER)])?; + plan.library()?; + // Committed whole while it is laid out and relocated; each page is given + // its own protection once nothing more is written. + // SAFETY: placed anywhere, so it replaces nothing. + let at = unsafe { syscall::vm_reserve(plan.span, VmProt::ReadWrite, VmAt::ANYWHERE) }.map_err(Refused::Memory)?; + let memory = Reservation { at, len: plan.span, kept: false }; + // SAFETY: the fresh, zeroed reservation, this function's alone. + plan.lay(bytes, unsafe { core::slice::from_raw_parts_mut(at, plan.span) })?; + // SAFETY: laid out whole and writable, reached by nothing else until + // published, and released only after every view of it is gone. + let mapped = unsafe { Mapped::new(at, &plan) }; + let tables = mapped.tables(true)?.ok_or("ELF: a library with no PT_DYNAMIC")?; + let needed = tables.needed().map(path_of).collect::>()?; + Ok((Laid { plan, memory, tables }, needed)) +} + +/// A library's memory, released unless it is [kept](Self::keep). +struct Reservation { + at: *mut u8, + len: usize, + kept: bool, +} + +impl Reservation { + /// Keep it for the process's life: its module is published. + fn keep(mut self) { + self.kept = true; + } +} + +impl Drop for Reservation { + fn drop(&mut self) { + if !self.kept { + // SAFETY: a reservation nothing was published over. + let _ = unsafe { syscall::vm_release(self.at, self.len) }; + } + } +} + +/// A set published: each library's constructors to run, `(address, count)`, +/// a library's after those of the libraries it names. +struct Published { + inits: Vec<(usize, usize)>, +} + +/// Load `roots` and every library they name that is not yet published, into +/// the list `loading` holds. `executable`, the running executable's image and +/// tables, is relocated against the set before it is published. +fn load(loading: &mut Loading, roots: impl IntoIterator, executable: Option<(&Mapped<'_, '_>, &Tables<'_>)>) -> Result { + let set = discover(roots, |name| loading.find(name.as_bytes()).is_some(), lay)?; + let first = loading.next_id(); + let mut global = Global(tls::modules().filter_map(bound).collect()); + global.0.extend(set.iter().enumerate().map(|(i, node)| Bound { + id: first + i, + base: node.laid.memory.at as u64, + tables: node.laid.tables.clone(), + tls: node.laid.plan.tls().is_some(), + })); + for (i, node) in set.iter().enumerate() { + // SAFETY: `lay`'s, and only this writes it until it is published. + let mapped = unsafe { Mapped::new(node.laid.memory.at, &node.laid.plan) }; + image::relocate(&mapped, &node.laid.tables, first + i, &global, Pass::Library)?; + } + if let Some((mapped, tables)) = executable { + let block = tls::block(1); + let filesz = tls::modules().next().map_or(0, |exe| exe.tls.filesz); + // SAFETY: the main thread's block of the executable, `filesz` bytes of + // it the template's, and the main thread is the only one. + let block = unsafe { core::slice::from_raw_parts_mut(block, filesz) }; + image::relocate(mapped, tables, 1, &global, Pass::Executable { block })?; + } + for node in &set { + protect(&node.laid.plan, node.laid.memory.at)?; + } + drop(global); + let inits = order(&set) + .into_iter() + .filter_map(|i| { + let laid = &set[i].laid; + laid.tables.init_array.map(|(offset, count)| (laid.memory.at as usize + offset as usize, count)) + }) + .collect(); + for (i, node) in set.into_iter().enumerate() { + let start = node.laid.memory.at as usize; + let laid = node.laid; + let module = loading.add(Module::new(leak(&node.name), image_of(&laid.plan, start), tls_of(&laid.plan, start))); + assert_eq!(module.id(), first + i, "dl: a set's ids are taken in order while the list is held"); + laid.memory.keep(); + } + Ok(Published { inits }) +} + +/// `name` for the module list, which keeps it for the process's life. +fn leak(name: &str) -> &'static CStr { + let name = CString::new(name).expect("dl: `lay` refused a path holding a NUL"); + alloc::boxed::Box::leak(name.into_boxed_c_str()) +} + +fn image_of(plan: &Plan, start: usize) -> tls::Image { + let (phdr, phnum) = plan.phdrs(); + tls::Image { + base: start, + start, + end: start + plan.span, + phdr: start + phdr as usize, + phnum, + eh_frame_hdr: plan.eh_frame_hdr().map_or(0, |at| start + at as usize), + } +} + +fn tls_of(plan: &Plan, start: usize) -> tls::Tls { + plan.tls().map_or(tls::Tls::default(), |segment| tls::Tls { + template: start + segment.template().start().get() as usize, + filesz: segment.template().len() as usize, + memsz: segment.memsz() as usize, + align: segment.align() as usize, + }) +} + +/// Give each of `plan`'s segments at `base` its pages' protection, the pages +/// between them none, and `PT_GNU_RELRO`'s pages read-only: nothing writes +/// what its relocations wrote. No segment is both writable and executable +/// ([`Plan::library`]). +fn protect(plan: &Plan, base: *mut u8) -> Result<(), Refused> { + let mut segments: Vec<_> = plan.segments().collect(); + segments.sort_unstable_by_key(|&(lo, _, _)| lo); + let mut covered = 0; + let give = |lo: u64, hi: u64, prot: VmProt| { + if lo >= hi { + return Ok(()); + } + // SAFETY: whole pages of the reservation `base` begins. + unsafe { syscall::vm_protect(base.add(lo as usize), (hi - lo) as usize, prot) }.map_err(Refused::Memory) + }; + for (lo, hi, flags) in segments { + give(covered, lo, VmProt::None)?; + let prot = if flags.executable() { + VmProt::ReadExec + } else if flags.writable() { + VmProt::ReadWrite + } else { + VmProt::Read + }; + give(lo, hi, prot)?; + covered = hi; + } + give(covered, plan.span as u64, VmProt::None)?; + if let Some((lo, hi)) = plan.relro() { + give(lo, hi, VmProt::Read)?; + } + Ok(()) +} + +/// Call each `(address, count)` `.init_array`'s constructors in order. +/// +/// # Safety +/// Each names a published module's `.init_array`. +unsafe fn construct(inits: &[(usize, usize)]) { + for &(at, count) in inits { + // SAFETY: the caller's; the array is `count` pointers inside the image. + let entries = unsafe { core::slice::from_raw_parts(at as *const usize, count) }; + for &entry in entries { + // The psABI's `0` and `-1` are no constructor. + if entry != 0 && entry != usize::MAX { + // SAFETY: a relocated `.init_array` entry is a constructor. + let f: extern "C" fn() = unsafe { core::mem::transmute(entry) }; + f(); + } + } + } +} + +/// Load the libraries the executable at `header` names and bind its symbols +/// to them, then run their constructors: std's and libc's start call this +/// once its thread-local storage is laid out, before anything that could +/// name such a symbol. One whose libraries cannot be had exits 127 with a +/// line on its stderr naming why, as [`crate::relocate`] does. +/// +/// # Safety +/// `header` is the running executable's ELF header, its thread-local storage +/// is laid out ([`tls::start`]) and this is its main thread, the only one. +pub unsafe fn start(header: *const u8) { + if !crate::relocate::names_a_library() { + return; + } + // SAFETY: the caller's. + match unsafe { load_needed(header) } { + // SAFETY: each a module just published. + Ok(published) => unsafe { construct(&published.inits) }, + Err(why) => { + let line = alloc::format!("this program's libraries could not be loaded: {why}\n"); + if let Some(stderr) = crate::relocate::stderr() { + let _ = syscall::write(stderr, line.as_bytes()); + } + syscall::exit(127) + } + } +} + +/// # Safety +/// [`start`]'s. +unsafe fn load_needed(header: *const u8) -> Result { + // SAFETY: the caller's; the page the header begins is mapped. + let page = unsafe { core::slice::from_raw_parts(header, HEADER) }; + let plan = image::plan(page)?; + // SAFETY: the executable as the kernel mapped it, relocated by itself, + // whose writable window only the load writes until it returns. + let mapped = unsafe { Mapped::new(header.cast_mut(), &plan) }; + let tables = mapped.tables(true)?.ok_or("ELF: an executable that names libraries with no PT_DYNAMIC")?; + let needed: Vec = tables.needed().map(path_of).collect::>()?; + let mut loading = tls::loading(); + load(&mut loading, needed, Some((&mapped, &tables))) +} + +/// Load the library at `path`, relative to the working directory, and every +/// library it names, and run their constructors: the module, which a second +/// load of the same path answers too. +pub fn open(path: &str) -> Result<&'static Module, Refused> { + let mut cwd = alloc::vec![0u8; syscall::MAX_PATH]; + let n = files::getcwd(&mut cwd); + let name = files::absolute(&String::from_utf8_lossy(&cwd[..n.min(cwd.len())]), path); + let (module, published) = { + let mut loading = tls::loading(); + let published = load(&mut loading, [name.clone()], None)?; + (loading.find(name.as_bytes()).expect("dl: a load publishes its root"), published) + }; + // SAFETY: each a module just published. + unsafe { construct(&published.inits) }; + Ok(module) +} + +/// The module whose id is `id`. +pub fn module(id: usize) -> Option<&'static Module> { + tls::modules().find(|m| m.id() == id) +} + +/// The address of the symbol `name`: with a module, any symbol it defines; +/// with none, the first module's that exports it, the executable first. A +/// thread-local has no one address, so it is none. +pub fn symbol(module: Option<&'static Module>, name: &str) -> Option { + let found = match module { + Some(module) => bound(module).and_then(|m| m.tables.defined(name, m.base)), + None => tls::modules().filter_map(bound).find_map(|m| m.tables.address(name, m.base)), + }; + found.map(|at| at as usize) +} diff --git a/sdk/toyos/src/dl/tests.rs b/sdk/toyos/src/dl/tests.rs new file mode 100644 index 00000000000..7d02a4085b8 --- /dev/null +++ b/sdk/toyos/src/dl/tests.rs @@ -0,0 +1,89 @@ +//! A set's shape: which libraries a load lays out, and the order their +//! constructors run in. + +#![allow(clippy::unwrap_used, clippy::indexing_slicing)] + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use super::{discover, order, Refused}; + +/// Who names whom: each library and the libraries it names. +const NAMES: &[(&str, &[&str])] = &[ + ("app", &["a", "b"]), + ("a", &["c", "b"]), + ("b", &["c"]), + ("c", &["a"]), + ("d", &["d"]), +]; + +/// The set `roots` loads with `published` already in the list: each name in +/// the order it was laid, and every name laid in that order. +fn set(roots: &[&str], published: &[&str]) -> (Vec, Vec) { + let mut laid = Vec::new(); + let set = discover(roots.iter().map(|r| r.to_string()), |name| published.contains(&name), |name| { + laid.push(name.to_string()); + let needed = NAMES.iter().find(|(n, _)| *n == name).unwrap().1; + Ok(((), needed.iter().map(|n| n.to_string()).collect())) + }) + .unwrap(); + (set.into_iter().map(|node| node.name).collect(), laid) +} + +/// A cycle of names is one set, each library laid once, breadth first: a +/// recursion over `DT_NEEDED` would never end. +#[test] +fn a_cycle_of_names_is_laid_once_breadth_first() { + let (names, laid) = set(&["app"], &[]); + assert_eq!(names, ["app", "a", "b", "c"]); + assert_eq!(laid, names); + assert_eq!(set(&["d"], &[]).0, ["d"]); +} + +/// A library already in the list is bound to, not laid again. +#[test] +fn a_published_library_is_not_laid_again() { + assert_eq!(set(&["app"], &["b"]).0, ["app", "a", "c"]); + assert!(set(&["b"], &["b"]).0.is_empty()); +} + +/// The first refusal ends the walk: nothing after it is laid. +#[test] +fn a_refusal_ends_the_set() { + let mut laid = Vec::new(); + let refused = discover(["app".to_string()], |_| false, |name| { + laid.push(name.to_string()); + match name { + "a" => Err(Refused::Elf("refused")), + _ => Ok(((), Vec::from(["a".to_string(), "b".to_string()]))), + } + }); + assert!(matches!(refused, Err(Refused::Elf("refused")))); + assert_eq!(laid, ["app", "a"]); +} + +/// A library's constructors run after those of every library of the set it +/// names, transitively, each once; within a cycle, after those it reached +/// first. +#[test] +fn constructors_run_after_those_of_the_libraries_named() { + let set = discover(["app".to_string()], |_| false, |name| { + let needed = NAMES.iter().find(|(n, _)| *n == name).unwrap().1; + Ok(((), needed.iter().map(|n| n.to_string()).collect())) + }) + .unwrap(); + let names: Vec<&str> = order(&set).into_iter().map(|i| set[i].name.as_str()).collect(); + // app names a, which names c, which names a (already seen): c, then a's + // other name b, whose c is done, then a, then app. + assert_eq!(names, ["c", "b", "a", "app"]); +} + +/// A `DT_NEEDED` name is a path, or a file name in the system's library +/// directory; an empty one, which an unreadable name also reads as, names +/// nothing. +#[test] +fn a_needed_name_is_a_path_or_a_system_library() { + assert_eq!(super::path_of("libx.so").unwrap(), "/system/lib/libx.so"); + assert_eq!(super::path_of("/app/lib/libx.so").unwrap(), "/app/lib/libx.so"); + assert!(matches!(super::path_of(""), Err(Refused::Elf("ELF: a DT_NEEDED names no library")))); +} diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 63a64899272..6d04501a7e8 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -20,6 +20,7 @@ pub mod audio; pub mod census; pub mod device; pub mod devset; +pub mod dl; pub mod endow; pub mod fd; pub mod files; diff --git a/sdk/toyos/src/relocate/mod.rs b/sdk/toyos/src/relocate/mod.rs index dd2d2897879..08f6c793661 100644 --- a/sdk/toyos/src/relocate/mod.rs +++ b/sdk/toyos/src/relocate/mod.rs @@ -15,14 +15,12 @@ //! A static PIE carries `RELATIVE` relocations and nothing else, so that is //! all this applies. An image that names a library (`DT_NEEDED`) leaves every //! other relocation in its tables to the loader, which binds them once its -//! libraries are loaded (libc's `__toyos_load_needed`). Every other form is -//! refused by name: in an image that names no library, a symbol it would have -//! to bind (`DT_JMPREL` with entries, any other type); a table form it does not read +//! libraries are loaded ([`crate::dl::start`]). Every other form is refused by +//! name: in an image that names no library, a symbol it would have to bind +//! (`DT_JMPREL` with entries, any other type); a table form it does not read //! (`DT_REL`, every `RELR`, Android's packed tables, and every one's size), -//! and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). -//! A write lands only inside a writable `PT_LOAD` and never in the `PT_TLS` -//! template, which every thread's block is copied from as it was mapped and -//! which a write here would never reach in a block already made. +//! and a write into text (`DT_TEXTREL`, `DF_TEXTREL`). A write lands only +//! inside a writable `PT_LOAD`. #![deny( clippy::indexing_slicing, @@ -82,8 +80,6 @@ enum Refusal { Kind(u32), /// A write outside every writable `PT_LOAD`. OutsideWritable, - /// A write into the `PT_TLS` template. - InTlsTemplate, } /// What a program whose relocations are refused writes to slot 2. @@ -195,7 +191,6 @@ const R_AARCH64_RELATIVE: u32 = 1027; const PT_LOAD: u32 = 1; const PT_DYNAMIC: u32 = 2; -const PT_TLS: u32 = 7; const PF_W: u32 = 2; const DT_NULL: u64 = 0; @@ -433,9 +428,6 @@ impl<'a> Image<'a> { if !self.loads().any(|p| p.writable_load() && p.holds(offset, 8, p.memsz)) { return Err(Refusal::OutsideWritable); } - if self.headers().any(|p| p.kind == PT_TLS && p.meets(offset, 8, p.filesz)) { - return Err(Refusal::InTlsTemplate); - } write(offset, bias.wrapping_add(addend)); } Ok(()) diff --git a/sdk/toyos/src/relocate/tests.rs b/sdk/toyos/src/relocate/tests.rs index f8f08c82a49..23a7d4141ab 100644 --- a/sdk/toyos/src/relocate/tests.rs +++ b/sdk/toyos/src/relocate/tests.rs @@ -119,12 +119,16 @@ fn the_bias_is_where_the_header_is_found_less_its_link_address() { } } +/// Every thread's block is copied from the template after this runs +/// (`toyos::tls::start`), so a pointer in it is relocated like any other. #[test] -fn a_pointer_in_the_tls_template_is_refused() { +fn a_pointer_in_the_tls_template_is_written() { // `readelf -lr x86_64-tls.elf`: `TLS 0x000478 0x2478 … 0x10` and a // `R_X86_64_RELATIVE` at `0x2478`; `aarch64-tls.elf` the same at `0x204e0`. - assert_eq!(writes(X86_64_TLS, BASE), Err(Refusal::InTlsTemplate)); - assert_eq!(writes(AARCH64_TLS, BASE), Err(Refusal::InTlsTemplate)); + for (file, template) in [(X86_64_TLS, 0x2478), (AARCH64_TLS, 0x204e0)] { + let writes = writes(file, BASE).expect("lld's own output applies"); + assert!(writes.iter().any(|&(vaddr, value)| vaddr == template && value > BASE), "{writes:x?}"); + } } /// `tags`, `DT_NULL`-terminated, as a dynamic section's bytes. @@ -236,16 +240,6 @@ fn a_write_outside_a_writable_segment_is_refused() { } } -#[test] -fn a_write_meeting_the_tls_template_is_refused() { - // `x86_64-tls.elf`'s template is `[0x2478, 0x2488)`; an 8-byte write - // starting 4 bytes before its end meets it. - for offset in [0x2478, 0x2480, 0x2484] { - assert_eq!(applied(X86_64_TLS, &[rela(offset, 8, 0)]), Err(Refusal::InTlsTemplate), "{offset:#x}"); - } - assert!(applied(X86_64_TLS, &[rela(0x2488, 8, 0)]).is_ok()); -} - #[test] fn every_type_but_the_machines_relative_is_refused() { // `R_X86_64_JUMP_SLOT`, `GLOB_DAT`, `TPOFF64`, `NONE`, and AArch64's `RELATIVE`. diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs index 28c1f10ed83..5f9bf2a91c3 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs @@ -5,9 +5,9 @@ //! covers what the kernel *derives* from them: the address a segment lands at //! once the image is rebased to `USER_VM_BASE`. Each case is a kernel panic or //! a forbidden mapping before the fix; each must be an error return after it, -//! with the kernel intact. A library is the program's own to load (libc's -//! `dlopen`), so a crafted one is that program's to refuse, and its loader's -//! host tests (`toyos-libc-copies`) hold that. +//! with the kernel intact. A library is the program's own to load +//! (`toyos::dl`), so a crafted one is that program's to refuse, and its +//! loader's host tests hold that. //! //! An executable's dynamic section and thread-local storage are the program's //! own (`toyos::relocate`, `toyos::tls`): the spawn cases crafted in them are @@ -370,7 +370,7 @@ fn main() { ); // 5. A PT_TLS p_align that is no power of two, which the program-header - // parse refuses: u64::MAX once wrapped the kernel's TLS block size. + // parse refuses. spawn_refused( "tls_align_wrap", &base_exe(0x4000) @@ -485,8 +485,8 @@ fn main() { spawn_refused("phdrs_unmapped", &unmapped); // 22. This program's own image, a `RELR` table in one copy, which its - // `_start` refuses before it writes a word, and a library in another, - // which its start refuses before `main`, since it links no loader. + // `_start` refuses before it writes a word, and in another a library + // named by the empty string, which its start refuses before `main`. refuses_itself("carries_relr", DT_RELR, REFUSED_LINE); refuses_itself("needs_a_library", DT_NEEDED as u64, NO_LIBRARY_LINE); @@ -628,9 +628,9 @@ fn exe_tls_ref_past_segment() -> Vec { /// What `toyos::relocate` writes to its stderr, `fd:2`, when it refuses its own program. const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; -/// What std's start writes there for a program that names a library and links -/// no loader, as this one links none. -const NO_LIBRARY_LINE: &str = "this program names libraries and links no loader: it cannot start\n"; +/// What std's start writes there for a program whose `DT_NEEDED` names no +/// library. +const NO_LIBRARY_LINE: &str = "this program's libraries could not be loaded: ELF: a DT_NEEDED names no library\n"; /// The status it exits with then. const REFUSED_STATUS: i32 = 127; /// The argument a refused copy is handed, which its `main` panics on. 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 82c7ca099f5..3c231d8fc57 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs @@ -1,18 +1,11 @@ -//! The kernel must survive an ELF whose program headers lie about their sizes. +//! The kernel must survive an ELF whose program headers lie about their sizes +//! or place a segment outside the loaded image. //! -//! `p_filesz` and `p_memsz` are a (copy length, buffer size) pair to the -//! loader, so the spec's `p_filesz <= p_memsz` is memory safety here. Inverted, -//! the copy overruns its destination with file-chosen length *and* content: -//! -//! `PT_TLS` on the exe path was `OwnedAlloc::new(tls_memsz)` then -//! `copy_nonoverlapping(.., tls_filesz)`, a kernel heap overflow, while the -//! kernel laid out thread-local storage; it is the program's now, and the -//! header is still refused at the spawn. -//! -//! Program-header vaddrs outside the loaded image are the other way in. Every -//! case must be an error return, with the kernel intact afterwards. A -//! library's headers are the program's own loader's to refuse (libc's -//! `dlopen`). +//! `p_filesz` and `p_memsz` are a (copy length, buffer size) pair to a loader, +//! so the spec's `p_filesz <= p_memsz` is memory safety: inverted, a copy +//! overruns its destination with file-chosen length *and* content. Every case +//! must be an error return, with the kernel intact afterwards. A library's +//! headers are the program's own loader's to refuse (`toyos::dl`). use std::fs; @@ -122,9 +115,7 @@ fn spawn_as(path: &str, image: &[u8]) -> SyscallError { fn main() { fs::create_dir_all(DIR).expect("create /tmp/abuse_elf"); - // 1. PT_TLS filesz > memsz, which the program-header parse refuses: 16 KiB - // copied into a 16-byte kernel heap allocation while the kernel laid - // out thread-local storage. + // 1. PT_TLS filesz > memsz, which the program-header parse refuses. let err = spawn_err("tls_filesz", &elf(Phdr { kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x2000, filesz: 0x4000, memsz: 16, align: 8, 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 b0564d817ae..41c8380ad28 100644 --- a/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs +++ b/tests/guest/rust-tests/src/bin/abuse_kernel_addr.rs @@ -12,6 +12,11 @@ //! address space to notice. So the kernel keeps sixteen bytes with a known //! value and answers two questions about them (`SYS_DEBUG` actions 10 and 11): //! where they are, and whether they still say what it put there. +//! +//! `SYS_SET_THREAD_POINTER` writes a userland value into FS base or +//! `TPIDR_EL0`. A non-canonical FS base faults the `wrmsr` in Ring 0, so the +//! kernel takes zero or a user-half address and refuses the rest, leaving the +//! thread's own in place. use toyos_abi::syscall::{self, VmAt, VmProt, SyscallError}; @@ -75,6 +80,23 @@ fn main() { assert!(err(ret).is_none(), "futex_wake refused a word of this process's own: {ret:#x}"); unsafe { syscall::vm_release(region, 2 * PAGE as usize) }.expect("release"); + + // The first address past the user half, which x86-64 holds non-canonical, + // a kernel address and the last address. + for tp in [0x0000_8000_0000_0000, canary, u64::MAX] { + assert_eq!( + syscall::set_thread_pointer(tp).err(), + Some(SyscallError::InvalidArgument), + "SYS_SET_THREAD_POINTER took {tp:#x}", + ); + } + // Read through the thread pointer: still the thread's own. + THREAD_OWN.with(|v| assert_eq!(v.get(), 0x7E11, "a refused thread pointer replaced the thread's own")); + assert!(canary_intact(), "the canary changed during the run"); - println!("futex refuses an address it may not reach, and kernel memory is untouched"); + println!("futex and the thread pointer refuse an address they may not reach, and kernel memory is untouched"); +} + +thread_local! { + static THREAD_OWN: std::cell::Cell = const { std::cell::Cell::new(0x7E11) }; } diff --git a/tests/guest/rust-tests/src/bin/std_tls.rs b/tests/guest/rust-tests/src/bin/std_tls.rs index 0a1b0e4021b..8416c64a628 100644 --- a/tests/guest/rust-tests/src/bin/std_tls.rs +++ b/tests/guest/rust-tests/src/bin/std_tls.rs @@ -1,20 +1,11 @@ -//! An executable that links a library at load time (`DT_NEEDED`): its start -//! loads `libtls_lib.so` and binds its calls to it before `main` -//! (`__toyos_load_needed`), the executable's own thread-locals sit where its +//! An executable that links a library at load time (`DT_NEEDED`): std's +//! start loads `libtls_lib.so` and binds its calls to it before `main` +//! (`toyos::dl::start`), the executable's own thread-locals sit where its //! linker put them, and the library's are a block of their own, reached //! through the thread's DTV. use std::cell::Cell; -// The C library's loader, which this program's start runs: a Rust program -// that links a library at load time links it (`__toyos_load_needed`). -#[link(name = "toyos_c", kind = "static")] -unsafe extern "C" { - fn __toyos_load_needed(header: *const u8); -} -#[used] -static LOADER: unsafe extern "C" fn(*const u8) = __toyos_load_needed; - // libtls_lib.so's, which this executable names in a `DT_NEEDED`. #[link(name = "tls_lib")] extern "C" { From 125b8929ff6e5efac97be88d9f6f2783bd15d3d2 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 15:00:22 +0100 Subject: [PATCH 07/13] rust-tests: libloading through the SDK's loader The fork's ToyOS backend (1eb1665) calls toyos::dl again instead of linking libtoyos_c, and toyos brings toyos-elf. Co-Authored-By: Claude Opus 5.5 --- tests/guest/rust-tests/Cargo.lock | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 53ae040a010..04d2fea641e 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -421,9 +421,10 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" version = "0.8.9" -source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#fbfc60c73509f321abab280b6f2d74785664e338" +source = "git+https://github.com/ToyOSOrg/rust_libloading?branch=toyos-sdk-0.12#1eb166595120d44ccac6b02a67d103707a9246c2" dependencies = [ "cfg-if", + "toyos", "windows-link", ] @@ -938,6 +939,7 @@ name = "toyos" version = "0.18.0" dependencies = [ "toyos-abi", + "toyos-elf", ] [[package]] @@ -955,6 +957,10 @@ dependencies = [ name = "toyos-device-memory" version = "0.1.0" +[[package]] +name = "toyos-elf" +version = "0.1.0" + [[package]] name = "toyos-font" version = "0.2.0" From d40e9b33ba6c1401068a7adf2565b0b778480553 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 18:31:58 +0100 Subject: [PATCH 08/13] Nothing in the kernel parses an ELF The owner's decision: a parent lays out its child's program, the build lays out the kernel and the supervisor, and the ELF decoder is a module of `toyos`, so `toyos-elf` is no crate and is never published. Spawn: `SpawnArgs` carries the child's memory as ranges (`SpawnMap`), the entry, the image base and the build-id, in place of an image the kernel parsed. Each range is checked as an ordinary mapping is, all or nothing: whole pages of the user half, a `VmProt`, and a copy only of ordinary memory the object, held with `MAP`, holds from a page boundary; a range meeting another or the clock page, and an entry in no executable range, are refused. A copied range is the existing private kind whose pages are copied out of the object at their first touch, so pages are shared as before. `toyos::elf::program::lay_out` is the layout, which `Helper::spawn` and the build both use; the kernel's `file_backing` goes. Boot: the signed image's kernel section is `toyos_kimage`: the kernel at its own addresses, its RELATIVE relocations, its ranges and RELRO, a symbol table that resolves as the ELF table does, and the supervisor's program. The build writes it (`build::kimage`); the loader copies the image and applies the relocations; the kernel takes its page protections, its symbols and the supervisor's ranges from it. `toyos::elf` holds `lib/elf`'s source and tests. `lib/symbols` keeps what reads no ELF (frame records, the name budget); finding an image's symbol tables and build-id is `toyos::elf`'s, and `symbolize` names a recorded frame with it. The build's gates read the kernel with it too. `toyos-elf` leaves `PUBLISHED`, std's patched crates and the rust fork's lock (a189b3a6b33). The crafted-ELF guest tests go with the kernel's parse: their headers are the parent's layout, which the host tests hold. `abuse_spawn_maps` holds the kernel's new boundary, `start_refuses_itself` a program's refusal of itself, and `spawn_image_object` the object's. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 29 +- Cargo.toml | 10 +- build/build.rs | 22 +- build/fpgate.rs | 12 +- build/hostws.rs | 6 +- build/kimage.rs | 169 +++++ build/lib.rs | 1 + build/licence.rs | 8 +- build/sdkversion.rs | 1 - build/sysroot.rs | 23 +- build/toolchain.rs | 41 +- issues/an-executables-relro-stays-writable.md | 6 +- issues/elf-domain-lint-line-not-yet-added.md | 12 +- ...o-gate-decodes-kernel-elfs-instructions.md | 2 +- ...ernel-still-parses-what-userland-writes.md | 30 +- ...-binary-whose-relr-table-it-never-reads.md | 8 +- issues/the-tree-says-who-uses-each-thing.md | 4 +- issues/toyos-runs-on-arm64.md | 10 +- kernel/Cargo.toml | 4 +- kernel/src/arch/aarch64/mod.rs | 4 +- kernel/src/arch/x86_64/mod.rs | 4 +- kernel/src/bootimage.rs | 10 +- kernel/src/file_backing.rs | 55 -- kernel/src/loader/mod.rs | 261 +++---- kernel/src/main.rs | 27 +- kernel/src/mm/image.rs | 79 +- kernel/src/mm/space.rs | 6 + kernel/src/symbols.rs | 63 +- kernel/src/syscall/dispatch.rs | 19 +- kernel/src/syscall/mod.rs | 2 +- kernel/src/syscall/proc.rs | 40 +- kernel/src/syscall/vm.rs | 15 +- lib/elf/Cargo.toml | 25 - lib/kimage/Cargo.toml | 24 + lib/kimage/src/lib.rs | 290 ++++++++ lib/kimage/src/write.rs | 92 +++ lib/kimage/tests/round_trip.rs | 83 +++ lib/symbols/Cargo.toml | 18 +- lib/symbols/src/frame.rs | 12 +- lib/symbols/src/lib.rs | 133 +--- lib/userbound/Cargo.toml | 4 +- loader/Cargo.toml | 3 +- loader/src/arch/aarch64.rs | 4 +- loader/src/arch/x86_64.rs | 4 +- loader/src/main.rs | 146 +--- rust | 2 +- sdk/abi/src/boot.rs | 8 +- sdk/abi/src/syscall.rs | 67 +- sdk/libc/Cargo.lock | 6 - sdk/libc/Cargo.toml | 1 - sdk/libc/arch/Cargo.toml | 1 - sdk/libc/src/elfsym.rs | 4 +- sdk/toyos/Cargo.toml | 7 +- sdk/toyos/src/dl/image/mod.rs | 21 +- sdk/toyos/src/dl/image/tests.rs | 2 +- sdk/toyos/src/dl/mod.rs | 2 +- {lib/elf/src => sdk/toyos/src/elf}/dynamic.rs | 6 +- .../elf/src => sdk/toyos/src/elf}/gnu_hash.rs | 4 +- {lib/elf/src => sdk/toyos/src/elf}/header.rs | 12 +- {lib/elf/src => sdk/toyos/src/elf}/layout.rs | 4 +- .../src/lib.rs => sdk/toyos/src/elf/mod.rs | 26 +- {lib/elf/src => sdk/toyos/src/elf}/note.rs | 14 +- sdk/toyos/src/elf/program.rs | 116 +++ {lib/elf/src => sdk/toyos/src/elf}/rela.rs | 12 +- {lib/elf/src => sdk/toyos/src/elf}/section.rs | 21 +- {lib/elf/src => sdk/toyos/src/elf}/sym.rs | 20 +- {lib/elf/src => sdk/toyos/src/elf}/tls.rs | 4 +- sdk/toyos/src/files.rs | 2 +- sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/process.rs | 14 +- sdk/toyos/src/spawn.rs | 18 +- .../toyos/tests/elf}/common/mod.rs | 14 +- .../toyos/tests/elf}/fixtures/input-test.bin | Bin .../toyos/tests/elf}/fixtures/lld-headers.bin | Bin .../toyos/tests/elf_crafted.rs | 7 +- .../fuzz.rs => sdk/toyos/tests/elf_fuzz.rs | 13 +- .../note.rs => sdk/toyos/tests/elf_note.rs | 3 +- sdk/toyos/tests/elf_program.rs | 118 +++ .../real.rs => sdk/toyos/tests/elf_real.rs | 12 +- .../real.rs => sdk/toyos/tests/elf_symbols.rs | 19 +- .../toyos/tests/elf_tables.rs | 15 +- system/symbolize/Cargo.toml | 7 + system/symbolize/src/main.rs | 8 +- system/symbolize/src/name.rs | 71 ++ .../symbolize/src/name_tests.rs | 9 +- tests/guest/rust-tests/Cargo.lock | 5 - .../rust-tests/src/bin/abuse_elf_loader.rs | 680 ------------------ .../rust-tests/src/bin/abuse_elf_segments.rs | 159 ---- .../rust-tests/src/bin/abuse_handle_table.rs | 20 +- .../rust-tests/src/bin/abuse_page_straddle.rs | 10 +- .../rust-tests/src/bin/abuse_spawn_argv.rs | 10 +- .../rust-tests/src/bin/abuse_spawn_maps.rs | 99 +++ .../guest/rust-tests/src/bin/process_tree.rs | 10 +- .../src/bin/spawn_child_ends_first.rs | 10 +- .../rust-tests/src/bin/spawn_image_object.rs | 37 +- .../rust-tests/src/bin/spawn_lands_claimed.rs | 10 +- .../src/bin/start_refuses_itself.rs | 109 +++ tests/guest/rust-tests/src/image.rs | 14 +- 98 files changed, 1894 insertions(+), 1791 deletions(-) create mode 100644 build/kimage.rs delete mode 100644 kernel/src/file_backing.rs delete mode 100644 lib/elf/Cargo.toml create mode 100644 lib/kimage/Cargo.toml create mode 100644 lib/kimage/src/lib.rs create mode 100644 lib/kimage/src/write.rs create mode 100644 lib/kimage/tests/round_trip.rs rename {lib/elf/src => sdk/toyos/src/elf}/dynamic.rs (98%) rename {lib/elf/src => sdk/toyos/src/elf}/gnu_hash.rs (98%) rename {lib/elf/src => sdk/toyos/src/elf}/header.rs (94%) rename {lib/elf/src => sdk/toyos/src/elf}/layout.rs (99%) rename lib/elf/src/lib.rs => sdk/toyos/src/elf/mod.rs (92%) rename {lib/elf/src => sdk/toyos/src/elf}/note.rs (83%) create mode 100644 sdk/toyos/src/elf/program.rs rename {lib/elf/src => sdk/toyos/src/elf}/rela.rs (98%) rename {lib/elf/src => sdk/toyos/src/elf}/section.rs (80%) rename {lib/elf/src => sdk/toyos/src/elf}/sym.rs (96%) rename {lib/elf/src => sdk/toyos/src/elf}/tls.rs (85%) rename {lib/elf/tests => sdk/toyos/tests/elf}/common/mod.rs (94%) rename {lib/symbols/tests => sdk/toyos/tests/elf}/fixtures/input-test.bin (100%) rename {lib/elf/tests => sdk/toyos/tests/elf}/fixtures/lld-headers.bin (100%) rename lib/elf/tests/crafted.rs => sdk/toyos/tests/elf_crafted.rs (98%) rename lib/elf/tests/fuzz.rs => sdk/toyos/tests/elf_fuzz.rs (98%) rename lib/elf/tests/note.rs => sdk/toyos/tests/elf_note.rs (97%) create mode 100644 sdk/toyos/tests/elf_program.rs rename lib/elf/tests/real.rs => sdk/toyos/tests/elf_real.rs (91%) rename lib/symbols/tests/real.rs => sdk/toyos/tests/elf_symbols.rs (70%) rename lib/elf/tests/tables.rs => sdk/toyos/tests/elf_tables.rs (98%) create mode 100644 system/symbolize/src/name.rs rename lib/symbols/tests/name.rs => system/symbolize/src/name_tests.rs (94%) delete mode 100644 tests/guest/rust-tests/src/bin/abuse_elf_loader.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_elf_segments.rs create mode 100644 tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs create mode 100644 tests/guest/rust-tests/src/bin/start_refuses_itself.rs diff --git a/Cargo.lock b/Cargo.lock index 3eef5415eac..fd472cc83b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,8 +253,8 @@ dependencies = [ "toyos-blackbox", "toyos-bootmap", "toyos-devicepath", - "toyos-elf", "toyos-gpt", + "toyos-kimage", "toyos-microcode", "toyos-random", "toyos-rootimage", @@ -1558,9 +1558,9 @@ dependencies = [ "toyos-blackbox", "toyos-bootmap", "toyos-devicepath", - "toyos-elf", "toyos-gicv3", "toyos-its", + "toyos-kimage", "toyos-microcode", "toyos-pci", "toyos-phys", @@ -3269,6 +3269,8 @@ dependencies = [ name = "symbolize" version = "0.1.0" dependencies = [ + "object", + "toyos", "toyos-symbols", ] @@ -3536,10 +3538,10 @@ name = "toyos" version = "0.18.0" dependencies = [ "loom", + "object", "rustc-std-workspace-alloc", "rustc-std-workspace-core", "toyos-abi", - "toyos-elf", ] [[package]] @@ -3634,11 +3636,11 @@ dependencies = [ "serde_json", "tar", "toml", + "toyos", "toyos-abi", "toyos-blackbox", "toyos-blockdev", "toyos-crypto", - "toyos-elf", "toyos-fat32", "toyos-fat32-check", "toyos-font", @@ -3646,6 +3648,7 @@ dependencies = [ "toyos-icon", "toyos-input", "toyos-keymap", + "toyos-kimage", "toyos-logstream", "toyos-manifest", "toyos-metal", @@ -3735,14 +3738,6 @@ version = "0.1.0" name = "toyos-edid" version = "0.1.0" -[[package]] -name = "toyos-elf" -version = "0.1.0" -dependencies = [ - "object", - "rustc-std-workspace-core", -] - [[package]] name = "toyos-elide" version = "0.1.0" @@ -3835,13 +3830,20 @@ dependencies = [ name = "toyos-keymap" version = "0.1.0" +[[package]] +name = "toyos-kimage" +version = "0.1.0" +dependencies = [ + "toyos-abi", + "toyos-kimage", +] + [[package]] name = "toyos-libc-copies" version = "0.1.0" dependencies = [ "toyos", "toyos-abi", - "toyos-elf", ] [[package]] @@ -4111,7 +4113,6 @@ dependencies = [ "object", "rustc-demangle", "toyos-abi", - "toyos-elf", "toyos-elide", ] diff --git a/Cargo.toml b/Cargo.toml index dc9842af659..dd31b16327c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,6 @@ members = [ "lib/crypto", "lib/device-memory", "lib/devicepath", - "lib/elf", "lib/display-output", "lib/edid", "lib/elide", @@ -68,6 +67,7 @@ members = [ "lib/radio", "lib/random", "lib/rootimage", + "lib/kimage", "lib/scsi", "lib/session", "lib/ssh", @@ -238,9 +238,11 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" bcachefs = { path = "lib/bcachefs" } toyos-abi = { path = "sdk/abi" } -# What `build::syscall_entry_bytes` finds a staged kernel's entry with: its -# segments, and its symbol table where the kernel's own backtrace finds it. -toyos-elf = { path = "lib/elf" } +# The one ELF parser (`toyos::elf`), which the build reads the kernel's and +# the supervisor's images with. +toyos = { path = "sdk/toyos" } +# The kernel section the build writes (`build::kimage`). +toyos-kimage = { path = "lib/kimage", features = ["write"] } toyos-symbols = { path = "lib/symbols" } # What `build::fpgate` decodes the linked x86-64 kernel with, to refuse an # instruction that touches the user FP or vector state outside `arch::fpu`: diff --git a/build/build.rs b/build/build.rs index b1b172009ad..7a3e8a2b21d 100644 --- a/build/build.rs +++ b/build/build.rs @@ -1796,21 +1796,21 @@ const MOV_RSP_GS_LEN: usize = 9; /// A kernel's `syscall_entry` from its first instruction to the end of the /// segment it is in, found at its symbol. fn syscall_entry_bytes(kernel: &[u8]) -> Result<&[u8], String> { - use toyos_elf::header::{ProgramHeader, PT_LOAD}; + use toyos::elf::header::{ProgramHeader, PT_LOAD}; let (syms, strs) = - toyos_symbols::locate(kernel).ok_or("the kernel has no readable `.symtab`")?; - let table = toyos_elf::SymTab::new(syms, strs); + toyos::elf::section::symbol_tables(kernel).ok_or("the kernel has no readable `.symtab`")?; + let table = toyos::elf::SymTab::new(syms, strs); let mut entries = table.defined().filter(|&(i, sym)| { - sym.kind() == toyos_elf::sym::STT_FUNC && table.name(i).ends_with(SYSCALL_ENTRY_SYMBOL) + sym.kind() == toyos::elf::sym::STT_FUNC && table.name(i).ends_with(SYSCALL_ENTRY_SYMBOL) }); let (Some((_, entry)), None) = (entries.next(), entries.next()) else { return Err(format!( "the kernel's `.symtab` does not name exactly one function `…{SYSCALL_ENTRY_SYMBOL}`" )); }; - 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 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()) @@ -1822,7 +1822,7 @@ fn syscall_entry_bytes(kernel: &[u8]) -> Result<&[u8], String> { .find_map(|segment| { 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) + toyos::elf::file_range(kernel, segment.offset.checked_add(within)?, left) }) .ok_or_else(|| format!("no `PT_LOAD` holds `syscall_entry` at {value:#x} in the file")) } @@ -2050,7 +2050,8 @@ fn said_key(plan: &Plan) -> &'static crate::signing::Key { key } -/// The kernel, the loader and the userland a mode's image is made of. +/// The kernel section (`build::kimage`), the loader and the userland a mode's +/// image is made of. fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Userland) { let kernel_features = plan.features.join(","); let arch = plan.arch; @@ -2081,7 +2082,7 @@ fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Us let userland = build_and_assemble(root, &config, &env, &[], &plan.root_omits, false, arch, plan.machines); let bl_bytes = fs::read(&bl_art).expect("Failed to read staged bootloader"); - (kernel_bytes, bl_bytes, userland) + (crate::kimage::section(&kernel_bytes, &userland.supervisor, arch), bl_bytes, userland) } /// Create an empty disk image the guest sees at full size and the host pays @@ -2303,7 +2304,8 @@ pub fn build_test_parts( drop(building); - Parts { kernel: kernel_bytes, bootloader: bl_bytes, userland } + let kernel = Arc::new(crate::kimage::section(&kernel_bytes, &userland.supervisor, arch)); + Parts { kernel, bootloader: bl_bytes, userland } } /// Build all binaries in a multi-binary crate. Returns vec of (binary_name, bytes). diff --git a/build/fpgate.rs b/build/fpgate.rs index df5cceb38fa..54d6e42623e 100644 --- a/build/fpgate.rs +++ b/build/fpgate.rs @@ -19,8 +19,8 @@ //! pass, and only in a function of the [`OWNER`] module. use iced_x86::{CpuidFeature, Decoder, DecoderOptions, Instruction, InstructionInfoFactory, Mnemonic, Register}; -use toyos_elf::section::SectionHeader; -use toyos_elf::{FileHeader, Layout, Machine, SectionTable, SymTab}; +use toyos::elf::section::SectionHeader; +use toyos::elf::{FileHeader, Layout, Machine, SectionTable, SymTab}; /// `SHT_PROGBITS` and `SHF_EXECINSTR` (System V gABI, "Sections"). const SHT_PROGBITS: u32 = 1; @@ -86,7 +86,7 @@ pub fn judge(kernel: &[u8]) -> Result<(), String> { if header.machine != Machine::X86_64 { return Err(format!("the kernel is for {:?}, and this reads x86-64", header.machine)); } - let table = toyos_symbols::file_range(kernel, header.shoff, toyos_symbols::shdr_table_len(&header)) + let table = toyos::elf::file_range(kernel, header.shoff, u64::from(header.shnum) * u64::from(header.shentsize)) .ok_or("the kernel's section headers lie outside it")?; let sections: Vec = SectionTable::new(table) .iter() @@ -103,7 +103,7 @@ pub fn judge(kernel: &[u8]) -> Result<(), String> { boundaries.push((at, bitness)); } for section in §ions { - let bytes = toyos_symbols::file_range(kernel, section.offset, section.size) + let bytes = toyos::elf::file_range(kernel, section.offset, section.size) .ok_or("an executable section lies outside the kernel")?; let end = section.addr + section.size; let mut cuts: Vec<(u64, u32)> = vec![(section.addr, 64)]; @@ -137,13 +137,13 @@ struct Functions<'a> { impl<'a> Functions<'a> { fn of(kernel: &'a [u8], header: &FileHeader, sections: &[SectionHeader]) -> Result { - let (syms, strs) = toyos_symbols::locate(kernel).ok_or("the kernel has no readable `.symtab`")?; + let (syms, strs) = toyos::elf::section::symbol_tables(kernel).ok_or("the kernel has no readable `.symtab`")?; let table = SymTab::new(syms, strs); let extent = Layout::parse(kernel, header.machine).map_err(|e| format!("{e}"))?.extent(); let in_code = |at: u64| sections.iter().any(|s| at >= s.addr && at < s.addr + s.size); let mut starts: Vec<(u64, &str)> = table .defined() - .filter(|(_, sym)| matches!(sym.kind(), toyos_elf::sym::STT_FUNC | STT_NOTYPE)) + .filter(|(_, sym)| matches!(sym.kind(), toyos::elf::sym::STT_FUNC | STT_NOTYPE)) .filter_map(|(i, sym)| Some((extent.min() + sym.address(extent)?.get(), table.name(i)))) .filter(|&(at, name)| in_code(at) && !name.is_empty()) .collect(); diff --git a/build/hostws.rs b/build/hostws.rs index 2fbad63e4d3..79bafd80482 100644 --- a/build/hostws.rs +++ b/build/hostws.rs @@ -311,7 +311,7 @@ mod tests { #[test] fn the_gate_refuses_a_crate_that_joined_neither_list() { let members: BTreeSet = - [".", "lib/elf", "system/usbd", "system/usbd/xhci"].iter().map(|s| s.to_string()).collect(); + [".", "lib/kimage", "system/usbd", "system/usbd/xhci"].iter().map(|s| s.to_string()).collect(); let all_declared: BTreeSet = members.clone(); assert!(unclaimed(&members, &all_declared).is_empty()); @@ -336,7 +336,7 @@ mod tests { let root = repo_root(); let found = crate_dirs(&root); assert!(found.contains("."), "the walk did not find the root package"); - assert!(found.contains("lib/elf"), "the walk did not find lib/elf: {found:?}"); + assert!(found.contains("lib/kimage"), "the walk did not find lib/kimage: {found:?}"); assert!( found.contains("system/usbd/xhci"), "the walk did not descend past the first level, so a nested member could \ @@ -450,7 +450,7 @@ mod tests { #[test] fn the_dead_path_scan_names_the_member_and_ignores_an_excluded_crate() { let members: BTreeSet = - [".", "toyos-abi", "toyos-elf"].iter().map(|s| s.to_string()).collect(); + [".", "toyos-abi", "toyos-kimage"].iter().map(|s| s.to_string()).collect(); assert_eq!( dead_member_target_paths(&members, "let abi = root.join(\"../../toyos-abi/target/x\");"), ["toyos-abi/target"], diff --git a/build/kimage.rs b/build/kimage.rs new file mode 100644 index 00000000000..161b2b6479b --- /dev/null +++ b/build/kimage.rs @@ -0,0 +1,169 @@ +//! The kernel section of a signed image (`toyos_kimage`), out of the kernel's +//! and the supervisor's ELF: what the loader and the kernel would otherwise +//! each parse at boot, read here once with the one parser (`toyos::elf`). +//! The kernel is ours and linked by this build, so anything here that it +//! could not be is this build's bug, and panics naming it. + +use toyos::elf::program::{lay_out, BASE}; +use toyos::elf::rela::{self, Op, RelaTable, Rules}; +use toyos::elf::section::{symbol_tables, SectionTable}; +use toyos::elf::sym::{SymTab, STT_FUNC}; +use toyos::elf::{file_range, Layout, Machine}; +use toyos_abi::syscall::VmProt; +use toyos_kimage::{Input, Symbol, PAGE, SUPERVISOR_OBJECT}; + +use crate::arch::Arch; + +fn machine(arch: Arch) -> (Machine, u16) { + match arch { + Arch::X86_64 => (Machine::X86_64, 62), + Arch::Aarch64 => (Machine::Aarch64, 183), + } +} + +/// The section of `kernel` and the supervisor `supervisor`, both for `arch`. +pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { + let (machine, e_machine) = machine(arch); + let layout = Layout::parse(kernel, machine).unwrap_or_else(|e| panic!("kernel.elf: {e}")); + let extent = layout.extent(); + // The image sits at its own addresses: an image offset is moved by the + // extent's start, which the kernel links at a page boundary. + let min = extent.min(); + assert!(min.is_multiple_of(PAGE), "kernel.elf: its lowest address {min:#x} is no page boundary"); + if let Some((a, b)) = layout.overlapping_load_pages(PAGE) { + panic!("kernel.elf: PT_LOAD segments {a} and {b} share a page, which cannot have both their protections"); + } + let memory = extent.max().next_multiple_of(PAGE); + let mut image = Vec::new(); + let mut ranges = Vec::new(); + for segment in layout.segments() { + let bytes = file_range(kernel, segment.file_offset(), segment.filesz()) + .expect("kernel.elf: a PT_LOAD's file bytes are past the file's end"); + let at = (min + segment.image().start().get()) as usize; + if image.len() < at + bytes.len() { + image.resize(at + bytes.len(), 0); + } + image[at..at + bytes.len()].copy_from_slice(bytes); + let (lo, hi) = segment.page_range(PAGE); + let flags = segment.flags(); + assert!(!(flags.executable() && flags.writable()), "kernel.elf: a segment both writable and executable"); + let prot = if flags.executable() { + VmProt::ReadExec + } else if flags.writable() { + VmProt::ReadWrite + } else { + VmProt::Read + }; + ranges.push((min + lo, min + hi, prot)); + } + ranges.sort_unstable_by_key(|&(lo, ..)| lo); + let relro = layout.relro_pages(PAGE).map_or((0, 0), |(lo, hi)| (min + lo, min + hi)); + + // Every relocation is a `RELATIVE`: the loader writes its base plus the + // target's address, and nothing else, before ExitBootServices. + let sections = layout + .section_headers() + .and_then(|table| file_range(kernel, table.file_offset, table.byte_len() as u64)) + .map(SectionTable::new) + .expect("kernel.elf: no section header table inside the file"); + let rela_sections = sections.rela_sections().unwrap_or_else(|form| panic!("kernel.elf: {form} is not supported")); + let rules = Rules { extent, window: (0, memory), tls: None }; + let mut relocations = Vec::new(); + for section in rela_sections { + let table = file_range(kernel, section.offset, section.size).expect("kernel.elf: SHT_RELA section is past the file's end"); + for raw in RelaTable::new(table, machine).iter() { + match rela::parse(raw, &rules, SymTab::empty()).unwrap_or_else(|e| panic!("kernel.elf: {e}")).map(|r| (r.offset(), r.op())) { + Some((offset, Op::Relative(target))) => relocations.push((offset, min + target.get())), + None => {} + Some(other) => panic!("kernel.elf: a relocation the loader does not apply: {other:?}"), + } + } + } + + let program = lay_out(supervisor, machine, SUPERVISOR_OBJECT, BASE).unwrap_or_else(|why| panic!("supervisor: {why}")); + Input { + machine: e_machine, + entry: min + layout.entry().get(), + memory, + relro, + ranges, + relocations, + symbols: symbols(kernel), + supervisor_entry: program.entry, + supervisor_base: program.image_base, + supervisor_build_id: program.build_id().to_vec(), + supervisor_maps: program.maps().to_vec(), + image, + } + .write() +} + +/// Each address a function of `kernel` begins at, ascending, and the function +/// `SymTab::resolve` names there: `STT_FUNC`, never at 0, and of two at one +/// address the one carrying a size. +fn symbols(kernel: &[u8]) -> Vec { + symbol_tables(kernel).map_or_else(Vec::new, |(symtab, strtab)| chosen(SymTab::new(symtab, strtab))) +} + +/// [`symbols`] of one table. +fn chosen(table: SymTab<'_>) -> Vec { + let mut chosen: std::collections::BTreeMap = std::collections::BTreeMap::new(); + for i in 0..table.count() { + let Some(sym) = table.get(i) else { continue }; + if sym.kind() != STT_FUNC || sym.value() == 0 { + continue; + } + let best = chosen.entry(sym.value()).or_insert((sym.size(), i)); + if best.0 == 0 && sym.size() > 0 { + *best = (sym.size(), i); + } + } + chosen.into_iter().map(|(addr, (size, i))| Symbol { addr, size, name: table.name(i).to_string() }).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The table names every address what the ELF symbol table names it, + /// over a real binary: a frame the kernel prints from the table is the one + /// it printed from its ELF. + #[test] + fn the_table_names_what_the_elf_table_names() { + let file = include_bytes!("../sdk/toyos/tests/elf/fixtures/input-test.bin"); + let input = Input { symbols: symbols(file), memory: 0x10_0000, ranges: vec![(0, 0x1000, VmProt::ReadExec)], ..Input::default() }; + let bytes = input.write(); + let table = toyos_kimage::Kernel::parse(&bytes).expect("the section reads back").symbols; + let (symtab, strtab) = symbol_tables(file).unwrap(); + let elf = SymTab::new(symtab, strtab); + let mut checked = 0; + for offset in (0..0x6_0000).step_by(3) { + assert_eq!(table.resolve(offset), elf.resolve(offset), "{offset:#x}"); + checked += u32::from(elf.resolve(offset).is_some()); + } + assert!(checked > 10_000, "only {checked} named offsets"); + } + + /// Of two functions at one address, the one with a size names it, before + /// or after the one without; an object, or a function at 0, names nothing. + #[test] + fn of_two_at_one_address_the_sized_one_is_kept() { + let entry = |name: u32, info: u8, value: u64, size: u64| { + let mut e = vec![0u8; 24]; + e[0..4].copy_from_slice(&name.to_le_bytes()); + e[4] = info; + e[6..8].copy_from_slice(&1u16.to_le_bytes()); + e[8..16].copy_from_slice(&value.to_le_bytes()); + e[16..24].copy_from_slice(&size.to_le_bytes()); + e + }; + const FUNC: u8 = (1 << 4) | 2; + const OBJECT: u8 = (1 << 4) | 1; + let strs = b"\0bare\0sized\0data\0zero\0"; + let syms = [entry(0, 0, 0, 0), entry(1, FUNC, 0x100, 0), entry(6, FUNC, 0x100, 8), entry(12, OBJECT, 0x200, 8), entry(17, FUNC, 0, 4)].concat(); + let kept = chosen(SymTab::new(&syms, strs)); + assert_eq!(kept, [Symbol { addr: 0x100, size: 8, name: "sized".into() }]); + let swapped = [entry(0, 0, 0, 0), entry(6, FUNC, 0x100, 8), entry(1, FUNC, 0x100, 0)].concat(); + assert_eq!(chosen(SymTab::new(&swapped, strs)), kept); + } +} diff --git a/build/lib.rs b/build/lib.rs index 098f20aaa99..cb4fb045f4a 100644 --- a/build/lib.rs +++ b/build/lib.rs @@ -24,6 +24,7 @@ pub mod flags; pub mod hostws; pub mod identity; pub mod image; +pub mod kimage; pub mod signing; /// Which kernel containers may be hashed, and by whose keys; read by nothing /// but its own tests. diff --git a/build/licence.rs b/build/licence.rs index f032e402c49..778a4368f38 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -675,15 +675,15 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ Terms::Font("OFL-1.1"), ), ( - "lib/elf/tests/fixtures/lld-headers.bin", + "sdk/toyos/tests/elf/fixtures/lld-headers.bin", "ba65b2a85776a973ad4fd49a224598391fde212e06b10aa51e2c8bcacfae0c79", - "ours: the first 4096 bytes of a rust-lld output (lib/elf/tests/real.rs)", + "ours: the first 4096 bytes of a rust-lld output (sdk/toyos/tests/elf_real.rs)", Terms::Spdx("MIT OR Apache-2.0"), ), ( - "lib/symbols/tests/fixtures/input-test.bin", + "sdk/toyos/tests/elf/fixtures/input-test.bin", "6a08f75ee01bdbd1e77c9b3affd6185e981d86995da432c90ed107676f08eb83", - "ours: a ToyOS binary this build produced (lib/symbols/tests/real.rs)", + "ours: a ToyOS binary this build produced (sdk/toyos/tests/elf_symbols.rs)", Terms::Spdx("MIT OR Apache-2.0"), ), ( diff --git a/build/sdkversion.rs b/build/sdkversion.rs index ef9faaae72e..64499f2b100 100644 --- a/build/sdkversion.rs +++ b/build/sdkversion.rs @@ -27,7 +27,6 @@ pub const PUBLISHED: &[Crate] = &[ Crate { name: "toyos-abi", dir: "sdk/abi" }, Crate { name: "toyos-keymap", dir: "sdk/keymap" }, Crate { name: "toyos-font", dir: "sdk/font" }, - Crate { name: "toyos-elf", dir: "lib/elf" }, Crate { name: "toyos", dir: "sdk/toyos" }, Crate { name: "toyos-window", dir: "sdk/window" }, Crate { name: "toyos-osrelease", dir: "sdk/osrelease" }, diff --git a/build/sysroot.rs b/build/sysroot.rs index 87d730fe716..a165b10edc9 100644 --- a/build/sysroot.rs +++ b/build/sysroot.rs @@ -23,9 +23,8 @@ //! moved to the commit its tree pins; //! a linked worktree in its own `rust/`, made on first need as a git worktree of //! the primary's fork repository at the commit this tree pins and never moved ([`fork_checkout`]). -//! `library/std` names `toyos-abi` and `toyos` by version, `toyos-elf` through -//! `toyos`, and every bootstrap run is handed this worktree's by one `[patch]` -//! ([`std_patch`]); each file of +//! `library/std` names `toyos-abi` and `toyos` by version, and every bootstrap +//! run is handed this worktree's by one `[patch]` ([`std_patch`]); each file of //! its ToyOS backend, `sdk/std` and the allocator files of `sdk/libc/src` it //! includes (`malloc.rs`, `mapped.rs`), it names by a `#[path]` up out of the //! fork. So each checkout's std compiles against its own worktree's ABI and @@ -67,14 +66,13 @@ use crate::toolchain::{self, host_triple, GuestTarget, Owner, Role, GUEST_TARGET use whole_toolchain::{whole, Whole}; /// The per-worktree sources that end up inside a sysroot: std links the -/// `toyos-abi`, `toyos-elf` and `toyos` [`std_patch`] hands it and compiles its ToyOS +/// `toyos-abi` and `toyos` [`std_patch`] hands it and compiles its ToyOS /// backend from `sdk/std`, C's allocator among it from `sdk/libc/src`, and -/// `libtoyos_c.a` is `sdk/libc` with `toyos-elf` and `toyos-osrelease`. -pub const SYSROOT_SOURCES: [&str; 7] = [ +/// `libtoyos_c.a` is `sdk/libc` with `toyos-osrelease`. +pub const SYSROOT_SOURCES: [&str; 6] = [ "sdk/abi/src", "sdk/toyos/src", "sdk/std", - "lib/elf/src", "sdk/osrelease/src", "sdk/libc/src", "sdk/libc/include", @@ -82,10 +80,9 @@ pub const SYSROOT_SOURCES: [&str; 7] = [ /// Their manifests, and the lockfile and cargo configuration libc is built /// under: the features, versions and flags of the same build. -pub(crate) const SYSROOT_MANIFESTS: [&str; 7] = [ +pub(crate) const SYSROOT_MANIFESTS: [&str; 6] = [ "sdk/abi/Cargo.toml", "sdk/toyos/Cargo.toml", - "lib/elf/Cargo.toml", "sdk/osrelease/Cargo.toml", "sdk/libc/Cargo.toml", "sdk/libc/Cargo.lock", @@ -95,7 +92,7 @@ pub(crate) const SYSROOT_MANIFESTS: [&str; 7] = [ /// Of [`SYSROOT_MANIFESTS`], the crates std names by version, which std's /// lockfile resolves with the fork's own through [`std_patch`]: what of a /// worktree can move a freestanding target's dependency versions. -const STD_MANIFESTS: [&str; 3] = ["sdk/abi/Cargo.toml", "lib/elf/Cargo.toml", "sdk/toyos/Cargo.toml"]; +const STD_MANIFESTS: [&str; 2] = ["sdk/abi/Cargo.toml", "sdk/toyos/Cargo.toml"]; /// One of the crates std names by version, in a worktree. pub(crate) struct StdCrate { @@ -170,7 +167,7 @@ const RECIPE: &str = "bootstrap stage-0 local rebuild, libraries from the stamp, #[derive(Clone, Copy, PartialEq, Eq)] enum Libraries { /// The fork's and [`SYSROOT_SOURCES`]: ToyOS userland's std, which names - /// `toyos-abi` and `toyos`, and through it `toyos-elf`. + /// `toyos-abi` and `toyos`. Worktree, /// The fork's alone: the kernel's `core` and `alloc` and the loader's std, /// which is what lets them be a key of their own. @@ -1277,7 +1274,7 @@ pub(crate) mod tests { /// The manifests [`std_crates`] reads, in a worktree at `root` that has /// no other source: what a bootstrap run is handed std's patch from. pub(crate) fn std_manifests(root: &Path) { - for (manifest, name) in STD_MANIFESTS.into_iter().zip(["toyos-abi", "toyos-elf", "toyos"]) { + for (manifest, name) in STD_MANIFESTS.into_iter().zip(["toyos-abi", "toyos"]) { write(&root.join(manifest), &format!("[package]\nname = \"{name}\"\n")); } } @@ -1317,8 +1314,6 @@ pub(crate) mod tests { "--config".to_string(), format!("patch.crates-io.toyos-abi.path=\"{at}/sdk/abi\""), "--config".to_string(), - format!("patch.crates-io.toyos-elf.path=\"{at}/lib/elf\""), - "--config".to_string(), format!("patch.crates-io.toyos.path=\"{at}/sdk/toyos\""), ], ); diff --git a/build/toolchain.rs b/build/toolchain.rs index 8bad280dcbc..5ba8ae2900f 100644 --- a/build/toolchain.rs +++ b/build/toolchain.rs @@ -154,9 +154,8 @@ fn toyos_sources_in_dep_info(crates: &[StdCrate], text: &str) -> Vec { paths_in_dep_info(text).filter(|path| std_crate_of(crates, path).is_some()).map(str::to_string).collect() } -/// Refuse a std whose dep-info under `dep_info` names a `toyos-abi`, -/// `toyos-elf` or `toyos` that is not this worktree's: a registry's, or -/// another checkout's. +/// Refuse a std whose dep-info under `dep_info` names a `toyos-abi` or `toyos` +/// that is not this worktree's: a registry's, or another checkout's. /// /// What the builder believed is not evidence; this reads what the compiler was /// handed. A std built against another `toyos-abi` still builds, links and @@ -181,8 +180,8 @@ pub(crate) fn assert_std_built_from(root: &Path, dep_info: &Path) { sources.iter().filter(|p| !Path::new(p).starts_with(&root)).collect(); assert!( foreign.is_empty(), - "std was compiled against {} toyos-abi, toyos-elf or toyos sources that are not this worktree's {}:\n {}\n\ - std names them by version and takes this worktree's only through the [patch] bootstrap is \ + "std was compiled against {} toyos-abi or toyos sources that are not this worktree's {}:\n {}\n\ + std names both by version and takes this worktree's only through the [patch] bootstrap is \ handed (`sysroot::std_patch`): a registry's copy is a resolve that patch did not reach, and \ another checkout's a patch that named another tree.", foreign.len(), @@ -862,36 +861,36 @@ mod tests { ); } - /// **A std that took any of its crates from a registry is refused, naming - /// the registry's source**: what a std build the patch did not reach - /// compiles, as its dep-info spells it. Only this worktree's copies pass. + /// **A std that took either crate from a registry is refused, naming the + /// registry's source**: what a std build the patch did not reach compiles, + /// as its dep-info spells it. Only this worktree's copies of both pass. #[test] - fn a_std_built_from_a_registry_s_copy_of_any_crate_is_refused() { + fn a_std_built_from_a_registry_s_copy_of_either_crate_is_refused() { let temp = TempDir::new("std-registry"); let root = fs::canonicalize(&*temp).unwrap(); let built = root.join("rust/build/toyos-std/host/stage0-std/x86_64-unknown-toyos"); fs::create_dir_all(built.join("dist")).unwrap(); crate::sysroot::tests::std_manifests(&root); - let ours = ["sdk/abi", "lib/elf", "sdk/toyos"].map(|dir| format!("{}/{dir}/src/lib.rs", root.display())); - let registry = [("toyos-abi", "0.63.0+267ff3ef79dccdcb"), ("toyos-elf", "0.2.0+0a1b2c3d4e5f6a7b"), ("toyos", "0.75.0+0ea8e9e321a9f71c")] - .map(|(name, version)| format!("/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/{name}-{version}/src/lib.rs")); - let built_from = |sources: &[String; 3]| { - fs::write(built.join("dist/libstd.d"), format!("{}/dist/libstd.rlib: {}\n", built.display(), sources.join(" "))) + let ours = |dir: &str| format!("{}/{dir}/src/lib.rs", root.display()); + let registry = |name: &str, version: &str| { + format!("/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/{name}-{version}/src/lib.rs") + }; + let built_from = |abi: &str, toyos: &str| { + fs::write(built.join("dist/libstd.d"), format!("{}/dist/libstd.rlib: {abi} {toyos}\n", built.display())) .unwrap(); std::panic::catch_unwind(|| assert_std_built_from(&root, &built)) .err() .map(|refused| *refused.downcast::().expect("a formatted refusal")) }; - assert_eq!(built_from(&ours), None); + assert_eq!(built_from(&ours("sdk/abi"), &ours("sdk/toyos")), None); - for (i, taken) in registry.iter().enumerate() { - let mut sources = ours.clone(); - sources[i] = taken.clone(); - let said = built_from(&sources).unwrap_or_else(|| panic!("a std built from {taken} was taken")); + let (abi, toyos) = (registry("toyos-abi", "0.63.0+267ff3ef79dccdcb"), registry("toyos", "0.75.0+0ea8e9e321a9f71c")); + for (taken, from) in [(&abi, built_from(&abi, &ours("sdk/toyos"))), (&toyos, built_from(&ours("sdk/abi"), &toyos))] { + let said = from.unwrap_or_else(|| panic!("a std built from {taken} was taken")); assert!(said.contains(taken.as_str()) && said.contains("a registry's copy"), "{said}"); } - let said = built_from(®istry).expect("a std built from the registry's copies of all was taken"); - assert!(registry.iter().all(|taken| said.contains(taken.as_str())), "{said}"); + let said = built_from(&abi, &toyos).expect("a std built from the registry's copies of both was taken"); + assert!(said.contains(&abi) && said.contains(&toyos), "{said}"); } /// **Freestanding libraries whose dep-info names the worktree outside its diff --git a/issues/an-executables-relro-stays-writable.md b/issues/an-executables-relro-stays-writable.md index 59c103284ba..5556ef286bb 100644 --- a/issues/an-executables-relro-stays-writable.md +++ b/issues/an-executables-relro-stays-writable.md @@ -8,9 +8,9 @@ opened: 2026-10-10 `PT_GNU_RELRO` names what a program's relocations write once and nothing writes after: `.data.rel.ro`, `.dynamic`, `.got`, every vtable and every table -of function pointers. The kernel maps it as the writable `PT_LOAD` that holds -it, reads no `PT_GNU_RELRO` (`rg -i relro kernel/src lib/elf/src` matches -nothing), and gives a program no way to take the write away once +of function pointers. A parent lays it out as part of the writable range that +holds it (`toyos::elf::program`), which the kernel maps as asked; nothing reads +an executable's `PT_GNU_RELRO`, and nothing takes the write away once `toyos::relocate` has run. A stray or hostile write in a running program can rewrite a vtable. A library's is read-only once the SDK's loader has relocated it (`sdk/toyos/src/dl/mod.rs`, `protect`); an executable's is not, and the same diff --git a/issues/elf-domain-lint-line-not-yet-added.md b/issues/elf-domain-lint-line-not-yet-added.md index d209cd5ddf6..d846cf576c2 100644 --- a/issues/elf-domain-lint-line-not-yet-added.md +++ b/issues/elf-domain-lint-line-not-yet-added.md @@ -4,17 +4,17 @@ kind: track opened: 2026-09-27 --- -# `toyos-elf` does not `forbid(clippy::arithmetic_side_effects)` yet +# `toyos::elf` does not `forbid(clippy::arithmetic_side_effects)` yet The domain-lint line — `#![forbid(clippy::arithmetic_side_effects, clippy::indexing_slicing, …)]` on -`lib/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. +`sdk/toyos/src/elf/mod.rs` — is what would make an unchecked `+`/`-`/`[]` on +a file-chosen value a compile error, so the "every number is checked" property +this module 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. +Exit condition: the line is on the module, every site inside it is checked or +carries a one-clause `#[allow]`, and CI runs clippy on it. Held by the orchestrator. diff --git a/issues/no-gate-decodes-kernel-elfs-instructions.md b/issues/no-gate-decodes-kernel-elfs-instructions.md index 94668d77a28..2bf4971685b 100644 --- a/issues/no-gate-decodes-kernel-elfs-instructions.md +++ b/issues/no-gate-decodes-kernel-elfs-instructions.md @@ -17,7 +17,7 @@ Five stages each owe a gate over `kernel.elf`'s instructions: The five share one x86-64 decoder, in Rust in the tree and run by the host gate, with no external disassembler: it lands before any of them, and each adds its own check over it rather than a decoder of its own. It takes its functions -from `toyos_symbols::locate` over `toyos_elf` (`build/build.rs:1575-1598`), not +from `toyos::elf::section::symbol_tables` (`build/build.rs`, `syscall_entry_bytes`), not from a second ELF reader. `global_asm!` emits no `.type` or `.size`, so code such as the AP trampoline (`kernel/src/arch/x86_64/smp.rs:396`), in `.text` with a `jmp far [mem]` and a `retf`, is covered by no sized function. diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index e7371ac9b73..e5f6a22cdfa 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -40,10 +40,21 @@ backtrace crate. `SYS_DLOPEN`, `SYS_DLSYM`, `SYS_DLCLOSE`, `SYS_TLS_ALLOC_BLOCK` `SYS_QUERY_MODULES` are deleted, and a program may protect its own memory read and execute, never write and execute at once. An executable's start, std's or libc's, loads the libraries it names (`DT_NEEDED`) through the same loader before `main` -(`toyos::dl::start`), and `std_tls` holds it. What is left in Ring 0 is the -program-header parse, which the end state keeps. AArch64 shared objects reach +(`toyos::dl::start`), and `std_tls` holds it. AArch64 shared objects reach their thread-locals through TLS descriptors, which the loader refuses. +**Stage 3 is built: nothing in the kernel parses an ELF** (owner, 2026-11-05). +A parent lays out its child's program (`toyos::elf::program`) and hands the +kernel ranges to map, each checked as an ordinary mapping is +(`SpawnArgs::maps_ptr`, `abuse_spawn_maps`), the entry, the image base and the +build-id a crash record names. The build lays out the kernel and the +supervisor ahead of the boot (`toyos_kimage`): the loader copies the kernel and +applies its `RELATIVE`s from that table, and the kernel takes its own ranges, +its symbols and the supervisor's program from it. The ELF decoder is a module +of `toyos` (`toyos::elf`), the one parser, which the build, `symbolize` and +the SDK's loader read with; `toyos-elf` is no crate, and the kernel and the +loader link none of it. + The evidence that it is worth doing: nine dated commits over eleven days fixed userland-reachable kernel defects in this code, six of them machine-wide panics; seven of the loader's twelve bounds are over quantities a workload sets and two @@ -67,14 +78,13 @@ Frees at least 2 MiB per running program, the kernel stops parsing program files' name tables, library frames get names." and wrote "adopt, make sure the code can never panic or fail is that possible? i dont want to allow a kernel crash because of that and it seems brittle by nature do you agree? can we -share a lot of code there? should we?". The kernel keeps its own ELF reader, -`toyos-elf`: the orchestrator's reading of the owner's crate ruling of the same -day, "if it makes sense we wrote our own elf parser or aml interpreter due to -security or other important reasons we do it". The kernel names only itself: a -killed program's frame is recorded as the file it ran, the offset in that file -and the file's build-id (`lib/symbols/src/frame.rs`), and -`/system/bin/symbolize` names it with the kernel's own lookup and demangling -code. What is left: a library's frames print bare, because the kernel records +share a lot of code there? should we?". The kernel reads no ELF for it: its +own names are the build's table (`toyos_kimage::Symbols`), which resolves as +the ELF symbol table does (`build::kimage`'s test). The kernel names only +itself: a killed program's frame is recorded as the file it ran, the offset in +that file and the build-id its parent handed the spawn +(`lib/symbols/src/frame.rs`), and `/system/bin/symbolize` names it out of the +file with `toyos::elf` and the kernel's demangling code. What is left: a library's frames print bare, because the kernel records only the executable's image for a process; and `rustc-demangle`'s standing. It is an unforked crates.io dependency, ~2k lines of third-party string parsing in Ring 0 on the panic path, now for the kernel's own names alone: fork it into diff --git a/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md b/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md index b5d45df11c6..d1bd145b759 100644 --- a/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md +++ b/issues/the-program-loader-runs-a-binary-whose-relr-table-it-never-reads.md @@ -6,8 +6,8 @@ opened: 2026-09-26 # `dlopen` loads a library whose `DT_RELR` table it never reads -`toyos_elf::dynamic::Dynamic::parse` decodes the tags the kernel's `dlopen` -acts on and drops every other one (`_ => {}`), so a library linked with +`toyos::elf::dynamic::Dynamic::parse` decodes the tags the SDK's loader +(`toyos::dl`) acts on and drops every other one (`_ => {}`), so a library linked with `-z pack-relative-relocs` loads with its relative relocations — the ones `DT_RELR`/`DT_RELRSZ` name, and that `DT_RELA` then no longer holds — never applied. `DT_REL` and `DT_TEXTREL` are dropped the same way. The process runs @@ -16,8 +16,8 @@ somewhere after, or does not die. Nothing in the tree links that way today: rustc passes `rust-lld` no `--pack-dyn-relocs`, and LLD's default is none. An executable carrying any of -the three is refused at its own start (`toyos::relocate`); a library is input -from outside the kernel, and `dlopen` has no such refusal. +the three is refused at its own start (`toyos::relocate`); a library is the +loading program's input, and `toyos::dl` has no such refusal. Exit: a dynamic section naming a relocation form `dlopen` does not apply is refused at `dlopen`, and a crafted library carrying `DT_RELR` is the test. diff --git a/issues/the-tree-says-who-uses-each-thing.md b/issues/the-tree-says-who-uses-each-thing.md index d35f4354c6a..058a3f5c191 100644 --- a/issues/the-tree-says-who-uses-each-thing.md +++ b/issues/the-tree-says-who-uses-each-thing.md @@ -213,9 +213,7 @@ tree's top level: `.cargo`, `.claude`, `.github`, `.gitignore`, `tests/libc-arch` as `sdk/libc/arch/`). The moves landed with stage 4, in one pull request, with a clean toolchain build and the guest suite green. **Remaining:** `build/sysroot.rs`'s `SYSROOT_SOURCES` and - `build/sdkversion.rs`'s `PUBLISHED` are still lists. `SYSROOT_SOURCES` - cannot come from `sdk/` alone: libc links `toyos-elf`, which is in - `lib/elf` (rule step 5). `PUBLISHED` leaves out `sdk/libc`, + `build/sdkversion.rs`'s `PUBLISHED` are still lists. `PUBLISHED` leaves out `sdk/libc`, `sdk/libc/arch` and `sdk/std`, which are not published, and `sdk/filepicker`, which the filepicker ruling makes public. No release dry run was made: `release` and `publish` run only on a push to `main`. diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 75bd57da0f6..31ed550c3d1 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -181,8 +181,8 @@ across `src/` and `tests/`. The harness hardwires q35 (`tests/common/qemu.rs:4246-4266`). `kvm_usable()` is `cfg!(target_arch = "x86_64") && …` (`src/lib.rs:65`). The rust fork has only `x86_64_unknown_toyos.rs`; its base `base/toyos.rs` is arch-neutral. -`toyos-elf` -refuses anything but `EM_X86_64` (`lib/elf/src/header.rs:24,75`). +`toyos::elf` reads a machine its caller names (`Machine::RUNNING`), and the +build lays the kernel out for each (`build/kimage.rs`). **Already abstracted.** The syscall stub already has both arms (`sdk/abi/src/syscall.rs:678,703`: `syscall` and `svc #0`). `kernel/pure/sched` @@ -347,9 +347,9 @@ Each stage names its exit; "measured" means a number from a run. declaration; the shootdown stress test and the loom-checked stop both pass; `CPU_OFF`/`SYSTEM_RESET`/`SYSTEM_OFF` replace ACPI reset and PM1a. The TLS-descriptor resolver lands here, in std with its loader half and a - `dlopen` test; until it does the kernel refuses `R_AARCH64_TLSDESC` by name - (`toyos_elf::rela::ExeRefusal::TlsDescriptor` for an executable, - `toyos_elf::RelocError::TlsDescriptor` for a library). + `dlopen` test; until it does a program refuses `R_AARCH64_TLSDESC` by name + (`toyos::relocate` for an executable, `toyos::elf::RelocError::TlsDescriptor` + for a library). **Every CPU starts, ahead of small-kernel stage 6 by the owner's word, as stage 4 did:** it ports no device interrupt, so nothing of the relay. Owed before the exit holds: the TLS-descriptor resolver; diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 1a3f5e88525..459ba8921c0 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -398,7 +398,6 @@ toyos-acpi = { path = "../lib/acpi" } toyos-amdvi = { path = "amdvi" } toyos-blackbox = { path = "../lib/blackbox" } toyos-bootmap = { path = "../lib/bootmap" } -toyos-elf = { path = "../lib/elf" } toyos-gicv3 = { path = "gicv3" } toyos-its = { path = "its" } toyos-microcode = { path = "../lib/microcode" } @@ -409,6 +408,9 @@ toyos-smmu = { path = "smmu" } toyos-tco = { path = "../lib/tco" } toyos-tsc = { path = "../lib/tsc" } toyos-rootimage = { path = "../lib/rootimage" } +# Its own image's ranges and symbols and the supervisor's program, as the +# build laid them out: the kernel parses no ELF. +toyos-kimage = { path = "../lib/kimage" } toyos-quiesce = { path = "../lib/quiesce" } toyos-random = { path = "../lib/random" } toyos-symbols = { path = "../lib/symbols" } diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 9e0934ea37d..8c0225ca6f3 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -53,8 +53,8 @@ pub mod watchdog; pub use irqchip::its as message_unit; pub use smmu as iommu_unit; -/// The machine every program image this kernel loads must be built for. -pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::Aarch64; +/// `e_machine` of this kernel's image (`toyos_kimage::Kernel::machine`). +pub const E_MACHINE: u16 = 183; /// Interrupts masked on this CPU for as long as the guard lives, and then put /// back as they were — restored, not enabled — so a guard nests inside a region diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 74e8ea8159c..4288ae818dd 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -65,8 +65,8 @@ pub use iommu_unit::interrupt as message_unit; pub use apic::MSI_DOORBELL; -/// The machine every program image this kernel loads must be built for. -pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64; +/// `e_machine` of this kernel's image (`toyos_kimage::Kernel::machine`). +pub const E_MACHINE: u16 = 62; /// Interrupts masked on this CPU for as long as the guard lives, and then put /// back as they were — restored, not enabled — so a guard nests inside a region diff --git a/kernel/src/bootimage.rs b/kernel/src/bootimage.rs index 7631bf6a579..a95b3ed550f 100644 --- a/kernel/src/bootimage.rs +++ b/kernel/src/bootimage.rs @@ -1,9 +1,9 @@ -//! The payloads the loader hands the kernel beside its own ELF: the +//! The payloads the loader hands the kernel beside its own image: the //! supervisor's executable, the file server's and ROOT's bytes -//! (`toyos_abi::boot::BootImage`). The kernel parses none of them but the -//! supervisor's program headers, which it spawns the supervisor from; the -//! other two it hands the supervisor, whose file server serves `/system` out -//! of ROOT. +//! (`toyos_abi::boot::BootImage`). The kernel parses none of them: it spawns +//! the supervisor from the ranges the build laid out of its executable +//! (`toyos_kimage::Supervisor`), and hands the other two to the supervisor, +//! whose file server serves `/system` out of ROOT. //! //! [`init`] runs before `mm::init` and refuses the boot by name unless each //! image is non-empty, page-aligned and whole pages inside one `LoaderData` diff --git a/kernel/src/file_backing.rs b/kernel/src/file_backing.rs deleted file mode 100644 index a41f93db769..00000000000 --- a/kernel/src/file_backing.rs +++ /dev/null @@ -1,55 +0,0 @@ -use alloc::sync::Arc; - -use crate::mm::space::CopySource; -use crate::object::shm::SharedMemObject; -use toyos_abi::syscall::SyscallError; - -/// `mm::PAGE_SIZE`: `usize` for buffer sizing, `u64` for file offsets. -const BLOCK_SIZE: usize = crate::mm::PAGE_SIZE as usize; - -/// An executable in a shared memory object handed over by handle, since no -/// file server's volume is the kernel's: a file server's object for the file -/// (for ROOT, part of the boot image), or one a caller read into an object of -/// its own; or a boot image the loader verified. -/// -/// **Nothing is copied at the call, and every page is copied once when it is -/// read.** The object is alive while any process pages from it, and a -/// writable one's bytes stay its holders' to change. So each read takes one -/// copy of the page into the reader's own buffer and what the loader keeps is -/// that copy: a change after the hand-off reaches only pages not yet read, -/// which is the caller changing its own child's program, and never a value -/// the kernel checked and then read again. -pub struct SharedImage { - object: Arc, - size: u64, -} - -impl SharedImage { - /// The first `len` bytes of `object`. Refused unless they are ordinary - /// memory — a device aperture is no program, and a read of one is a device - /// access — and the object holds them all. - pub fn over(object: Arc, len: u64) -> Result { - if len > object.size() || !object.is_ordinary() { - return Err(SyscallError::InvalidArgument); - } - Ok(Self { object, size: len }) - } - - pub fn read_page(&self, file_offset: u64, buf: &mut [u8; BLOCK_SIZE]) { - buf.fill(0); - if file_offset >= self.size { - return; - } - let valid = BLOCK_SIZE.min((self.size - file_offset) as usize); - // The one fetch of each byte: its holders may be writing it. - self.object.read(file_offset, &mut buf[..valid]); - } - - pub fn file_size(&self) -> u64 { - self.size - } - - pub fn source(&self) -> Arc { - self.object.clone() - } -} diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 79f019fdd55..51db7c80786 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -1,13 +1,12 @@ -//! Building a process out of an ELF file. +//! Building a process out of the ranges its parent laid out. //! -//! The kernel reads an executable's program headers and its build-id note, -//! copies its `PT_LOAD`s into memory of the new process's own and jumps to its -//! entry. Everything else is the program's: it relocates itself +//! The kernel parses no program. A parent reads its child's program and names +//! the child's memory as ranges ([`Program`], `toyos_abi::syscall::SpawnMap`), +//! each checked as an ordinary mapping is, the entry point and the stack's +//! arguments; the kernel builds the space, places the stack and starts the +//! main thread. Everything else is the program's: it relocates itself //! (`toyos::relocate`), lays out its own thread-local storage (`toyos::tls`) -//! and loads its own libraries. -//! -//! Every number the file names is untrusted: a refusal is -//! `SyscallError::{InvalidArgument, ResourceExhausted}`, never a panic. +//! and loads its own libraries (`toyos::dl`). //! //! A spawn that lands writes one record, `spawn: pid=N (…ms)`, once //! the process is in the table and placed; a spawn that is refused writes @@ -27,7 +26,6 @@ use alloc::vec::Vec; use crate::object::{ops, HandleTable, KObjectRef}; use crate::mm::space::CopySource; -use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::process::{ Admission, Endowments, Parent, Pid, ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserImage, PROCESS_TABLE, @@ -35,8 +33,8 @@ use crate::process::{ use crate::sync::Lock; use crate::{scheduler, UserAddr}; use toyos_abi::handle::Rights; -use toyos_abi::syscall::SyscallError; -use toyos_elf::Layout; +use crate::object::shm::SharedMemObject; +use toyos_abi::syscall::{SpawnMap, SyscallError}; use toyos_symbols::frame::BuildId; /// The main thread's stack: committed whole, populated as it is touched. @@ -45,73 +43,74 @@ const USER_STACK_SIZE: usize = 8 * 1024 * 1024; /// The reserved range below the main stack. const STACK_GUARD: u64 = 1024 * 1024; -/// Read a byte range from the supplied executable image. -/// -/// Returns only the part of the request the file actually holds — callers -/// treat a short result as truncated and length-check before indexing; `len` -/// comes from untrusted ELF fields, so the read is clamped rather than sized -/// by it. -pub(crate) fn read_file_range( - backing: &crate::file_backing::SharedImage, - offset: u64, - len: usize, -) -> Vec { - let available = backing.file_size().saturating_sub(offset); - let len = len.min(available as usize); - let mut result = Vec::with_capacity(len); - let mut remaining = len; - let mut file_off = offset; - let mut page_buf = [0u8; PAGE_BYTES]; - - while remaining > 0 { - let off_in_block = (file_off % 4096) as usize; - let chunk = (4096 - off_in_block).min(remaining); - - backing.read_page(file_off - off_in_block as u64, &mut page_buf); - result.extend_from_slice(&page_buf[off_in_block..off_in_block + chunk]); - - file_off += chunk as u64; - remaining -= chunk; - } +/// One range of a child's memory, its numbers checked where the request was +/// read (`syscall::proc::spawn_maps`): whole pages, and a copy's bytes inside +/// an object of ordinary memory. +pub struct Map { + pub at: UserAddr, + pub len: u64, + pub prot: kernel::vm::Prot, + /// The object its pages start as a copy of, from which page boundary, and + /// how many bytes; zeros past them, and `None` for zeros alone. + pub copy: Option<(Arc, u64, u64)>, +} - result +impl Map { + /// `raw`, checked as an ordinary mapping is: whole pages of the user half, + /// a protection, and a copy only of ordinary memory `object` holds from a + /// page boundary, `object` being `None` for zeros. + pub fn checked(raw: &SpawnMap, object: Option>) -> Result { + let page = crate::mm::PAGE_SIZE; + let prot = crate::syscall::vm::vm_prot(raw.prot)?; + let at = UserAddr::checked(raw.addr).filter(|at| at.raw().is_multiple_of(page)); + let ends = raw.addr.checked_add(raw.len).is_some_and(|end| end <= toyos_userbound::USER_TOP); + let (Some(at), true) = (at, ends && raw.len != 0 && raw.len.is_multiple_of(page) && raw.copied <= raw.len) else { + return Err(SyscallError::InvalidArgument); + }; + let copy = match object { + None if raw.copied == 0 => None, + None => return Err(SyscallError::InvalidArgument), + Some(_) if !raw.offset.is_multiple_of(page) => return Err(SyscallError::InvalidArgument), + // Ordinary memory alone: a device aperture is no program, and a + // read of one is a device access. Each page is copied once, at + // its first touch, so a holder that writes the object later + // changes only pages not yet read: its own child's. + Some(object) => { + let held = raw.offset.checked_add(raw.copied).ok_or(SyscallError::InvalidArgument)?; + if held > object.size() || !object.is_ordinary() { + return Err(SyscallError::InvalidArgument); + } + Some((object as Arc, raw.offset, raw.copied)) + } + }; + Ok(Map { at, len: raw.len, prot, copy }) + } } +/// What a parent laid out of its child's program. +pub struct Program { + pub maps: Vec, + pub entry: u64, + /// Where the program's address 0 lands: what a crash record names a + /// frame's offset from. + pub image_base: u64, + pub build_id: Option, +} -/// Commit one range of private memory per `PT_LOAD` segment at `image_start` -/// plus its offset, its file bytes and the rest of it zero, each page copied -/// out of `source` at its first touch. -/// -/// `Err` when two segments would share a page, which their ranges cannot: -/// checked before the first range, and a refusal after it leaves ranges in a -/// space nothing runs in yet. -fn load_segments( - space: &mut crate::mm::space::AddressSpace, - layout: &Layout, - image_start: UserAddr, - source: &Arc, -) -> Result<(), SyscallError> { +/// Lay `maps` into `space`, a fresh one holding nothing but the clock page. +/// `InvalidArgument` for a range that meets another or the clock page; a +/// refusal after the first range leaves ranges in a space nothing runs in yet. +fn lay(space: &mut crate::mm::space::AddressSpace, maps: &[Map]) -> Result<(), SyscallError> { use crate::mm::space::At; - - if let Some((a, b)) = layout.overlapping_load_pages(4096) { - log!("spawn: PT_LOAD segments {} and {} contend for a page", a, b); - return Err(SyscallError::InvalidArgument); - } - for seg in layout.segments() { - let (lo, hi) = seg.page_range(4096); - let (seg_start, seg_end) = ((image_start + lo).raw(), (image_start + hi).raw()); - if seg_end == seg_start { - continue; + for map in maps { + if !space.vacant(map.at.raw(), map.len) { + return Err(SyscallError::InvalidArgument); } - // The file's bytes from the segment's first page: the bytes of that - // page before the segment are the file's too, as a linker lays them. - // Past the file's end and the segment's file size, the range reads - // zeros. - let file_start = seg.file_offset() & !4095; - let file_bytes = seg.filesz() + seg.file_offset() % 4096; - let copied = file_bytes.min(source.size().saturating_sub(file_start)); - let at = At::Fixed(UserAddr::new(seg_start)); - space.copy(seg_end - seg_start, at, segment_prot(seg), Arc::clone(source), file_start, copied)?; + let at = At::Fixed(map.at); + match &map.copy { + Some((source, offset, copied)) => space.copy(map.len, at, map.prot, Arc::clone(source), *offset, *copied)?, + None => space.reserve(map.len, at, map.prot)?, + }; } Ok(()) } @@ -142,30 +141,14 @@ fn argv_block(top: u64, args: &[&str]) -> (u64, Vec) { (sp, block) } -/// What one `PT_LOAD` segment's pages may be used for. -/// -/// `PF_W | PF_X` refuses the write, not the execution: taking `X` away would -/// let a hostile ELF run as data instead. -fn segment_prot(seg: &toyos_elf::Segment) -> kernel::vm::Prot { - use kernel::vm::Prot; - if seg.flags().executable() { - Prot::ReadExec - } else if seg.flags().writable() { - Prot::ReadWrite - } else { - Prot::Read - } -} - - -/// Load a program and place its main thread under `parent`, answering its pid +/// Build the process `program` describes and place its main thread under `parent`, answering its pid /// and what `commit` left its caller holding of it. Everything built for the /// child is counted to [`Commit::job`], which it runs in, and `commit` builds /// the child's handle table around its handle to itself once nothing is left /// to refuse. /// -/// `path` labels the supplied executable image; the kernel resolves no path. -/// `argv[0]` is the name the child goes by. +/// `path` labels the program; the kernel resolves no path. `argv[0]` is the +/// name the child goes by. /// /// `Refusal`, not `-> !`, is the error type: every failure below owns a /// partly built process (address space, stack, kernel stack), and nothing @@ -175,7 +158,7 @@ pub fn spawn( argv: &[&str], commit: C, env: Vec, - backing: Arc, + program: Program, parent: Parent, ) -> Result<(Pid, C::Held), crate::object::Refusal> { let job = Arc::clone(commit.job()); @@ -189,24 +172,15 @@ pub fn spawn( let charge = ProcessData::charge(&job, path, &env).map_err(SyscallError::from)?; let t0 = crate::clock::nanos_since_boot(); - let header_size = 4096.min(backing.file_size() as usize); - let header_data = read_file_range(backing.as_ref(), 0, header_size); - let layout = match Layout::parse(&header_data, crate::arch::ELF_MACHINE) { - Ok(l) => l, - Err(e) => { - log!("spawn: {}: {}", path, e.as_str()); - return Err(SyscallError::InvalidArgument.into()); - } - }; - - // The program reads the mapped table to relocate itself and lay out its - // thread-local storage. - if layout.program_headers().is_none() { - log!("spawn: {}: no PT_LOAD maps the program header table", path); + let Program { maps, entry, image_base, build_id } = program; + // Inside a range the child may execute, which `spawn_maps` placed in the user half. + let executes = |map: &Map| map.prot == kernel::vm::Prot::ReadExec && (map.at.raw()..map.at.raw() + map.len).contains(&entry); + let Some(entry) = maps.iter().any(executes).then(|| toyos_userbound::Entry::new(entry)).flatten() else { + log!("spawn: {}: the entry {:#x} is in no executable range", path, entry); return Err(SyscallError::InvalidArgument.into()); - } - - let source = backing.source(); + }; + let start = maps.iter().map(|map| map.at.raw()).min().ok_or(SyscallError::InvalidArgument)?; + let end = maps.iter().map(|map| map.at.raw() + map.len).max().ok_or(SyscallError::InvalidArgument)?; let t1 = crate::clock::nanos_since_boot(); @@ -214,28 +188,15 @@ pub fn spawn( log!("spawn: {}: no address space: every user PCID is held, or no frame is left for its root", path); return Err(SyscallError::ResourceExhausted.into()); }; - let (image_start, base, stack_base, sp) = { + let (stack_base, sp) = { use crate::mm::space::At; use kernel::vm::Prot; let mut space = child_pt.lock(); crate::clock::map_page(&mut space); - // The image's span is placed whole, so its segments keep the distances - // the file gave them; the holes between them stay reserved. - let extent = layout.extent(); - let Some(span) = layout.span().checked_next_multiple_of(crate::mm::PAGE_SIZE).filter(|&s| s != 0) else { - log!("spawn: {}: an image spanning {:#x}", path, layout.span()); - return Err(SyscallError::InvalidArgument.into()); - }; - let image_start = space.reserve(span, At::Anywhere(PAGE_2M), Prot::None)?; - // 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(image_start.raw(), extent.min(), layout.span()) else { - log!("spawn: {}: image at vaddr_min {:#x} spanning {:#x} cannot rebase to {image_start:#x}", - path, extent.min(), layout.span()); - return Err(SyscallError::InvalidArgument.into()); - }; - load_segments(&mut space, &layout, image_start, &source)?; + if let Err(e) = lay(&mut space, &maps) { + log!("spawn: {}: a range meets another or the clock page, or is refused: {:?}", path, e); + return Err(e.into()); + } // Never executable, and a reserved guard below it, so running off the // stack's bottom faults instead of writing what lies there. let stack = space.reserve(STACK_GUARD + USER_STACK_SIZE as u64, At::ANYWHERE, Prot::None)?; @@ -246,28 +207,11 @@ pub fn spawn( return Err(SyscallError::InvalidArgument.into()); } space.fill(sp, &block); - (image_start, base, stack_base, sp) + (stack_base, sp) }; - // Inside the image, which `rebase_base` placed inside the user half. - let Some(entry) = toyos_userbound::Entry::new((image_start + layout.entry().get()).raw()) - else { - log!("spawn: {}: the entry is outside the user half", path); - return Err(SyscallError::InvalidArgument.into()); - }; - let image_end = (image_start + layout.span()).raw(); - - // What a crash record names a frame by. The build-id is read at each - // `PT_NOTE`'s own offset, a page of it at most. - let image = Arc::new(UserImage { - name: String::from(path), - build_id: BuildId::find(&header_data, |note| { - Some(read_file_range(backing.as_ref(), note.offset, note.filesz.min(PAGE_BYTES as u64) as usize)) - }), - start: image_start.raw(), - end: image_end, - bias: base, - }); + // What a crash record names a frame by. + let image = Arc::new(UserImage { name: String::from(path), build_id, start, end, bias: image_base }); let (ks_alloc, ks_sp) = match alloc_kernel_stack(&job, Start::Process { entry, sp }) { Some(ks) => ks, @@ -351,14 +295,14 @@ pub fn spawn( /// The name the supervisor runs under: no file, since it runs from its boot image. const SUPERVISOR: &str = "supervisor"; -/// Start the supervisor from its boot image, holding the machine's one +/// Start the supervisor from its boot image as the build laid it out, holding the machine's one /// full-rights `SysCap` and the file server's image and ROOT's /// (`toyos_abi::syscall::FILESERVER_IMAGE_LABEL`, `ROOT_IMAGE_LABEL`). /// /// Nothing else can construct one: what the supervisor endows is the entire set of /// processes that can ever claim a device, enter the RT band, or power off. /// Panics on failure: a boot that cannot start the supervisor has nowhere to report to. -pub fn spawn_supervisor(images: crate::bootimage::Objects) -> Pid { +pub fn spawn_supervisor(images: crate::bootimage::Objects, laid: &toyos_kimage::Supervisor<'_>) -> Pid { let job = crate::object::job::new_job(); let mut handles = HandleTable::new(&job); let mut entries = Vec::new(); @@ -413,14 +357,21 @@ pub fn spawn_supervisor(images: crate::bootimage::Objects) -> Pid { }); labels.extend_from_slice(label.as_bytes()); } - let len = images.supervisor.immutable().expect("a boot image is immutable"); - let image: Arc = Arc::new( - crate::file_backing::SharedImage::over(images.supervisor, len) - .expect("spawn_supervisor: a boot image is ordinary memory, its length inside it"), - ); + // Its ranges as the build laid them out of the supervisor section + // (`toyos_kimage`), checked as a spawn's are. + let maps = laid.maps().map(|raw| { + let object = (raw.object == toyos_kimage::SUPERVISOR_OBJECT).then(|| Arc::clone(&images.supervisor)); + Map::checked(&raw, object).unwrap_or_else(|e| panic!("spawn_supervisor: the build's range {raw:?}: {e:?}")) + }); + let program = Program { + maps: maps.collect(), + entry: laid.entry, + image_base: laid.image_base, + build_id: BuildId::new(laid.build_id), + }; // Built by the kernel and owing nobody anything: no table but the supervisor's own holds it. let table = SupervisorTable { job, handles, entries, labels }; - match spawn(SUPERVISOR, &[SUPERVISOR], table, Vec::new(), image, Parent::Root) { + match spawn(SUPERVISOR, &[SUPERVISOR], table, Vec::new(), program, Parent::Root) { Ok((pid, ())) => pid, Err(crate::object::Refusal::Error(e)) => panic!("spawn_supervisor: failed to spawn: {e:?}"), Err(crate::object::Refusal::Handle(e)) => panic!("spawn_supervisor: {e}"), diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 4192535feba..2781a100365 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -44,7 +44,6 @@ mod power; mod inventory; mod loaded; mod bootimage; -mod file_backing; mod symbols; mod process; mod loader; @@ -280,8 +279,8 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { kernel_args.kernel_stack_addr, kernel_args.kernel_stack_size ); log!( - "boot: kernel elf {:#x}+{:#x}, rsdp {:#x}, boot pml4 {:#x}", - kernel_args.kernel_elf_addr, kernel_args.kernel_elf_size, + "boot: kernel image {:#x}+{:#x}, rsdp {:#x}, boot pml4 {:#x}", + kernel_args.kernel_image_addr, kernel_args.kernel_image_size, kernel_args.rsdp_addr, kernel_args.boot_pml4_addr ); log!( @@ -311,10 +310,14 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { (None, _) => log!("{} none: the loader named no slot", params::SLOT_RECORD), } - let kernel_elf = core::slice::from_raw_parts( - DirectMap::from_phys(kernel_args.kernel_elf_addr).as_ptr::(), - kernel_args.kernel_elf_size as usize, + // The signed kernel section the loader laid this kernel out of, kept for + // the machine's life: its ranges, symbols and the supervisor's program. + let kernel_section: &'static [u8] = core::slice::from_raw_parts( + DirectMap::from_phys(kernel_args.kernel_image_addr).as_ptr::(), + kernel_args.kernel_image_size as usize, ); + let kernel_image = toyos_kimage::Kernel::parse(kernel_section).unwrap_or_else(|why| panic!("boot: the loader's {why}")); + assert_eq!(kernel_image.machine, arch::E_MACHINE, "boot: the loader's kernel image is another machine's"); // After the boot's own lines, so its refusal reaches a channel that says // what machine this is, and while the loader's arguments are still mapped. random::key(loader_args, &mut kernel_args); @@ -329,7 +332,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { ); let loader = [ mm::Region { start: kernel_args.kernel_memory_addr, end: kernel_args.kernel_memory_addr + kernel_args.kernel_memory_size }, - mm::Region { start: kernel_args.kernel_elf_addr, end: kernel_args.kernel_elf_addr + kernel_args.kernel_elf_size }, + mm::Region { start: kernel_args.kernel_image_addr, end: kernel_args.kernel_image_addr + kernel_args.kernel_image_size }, // The loader's black-box page, which is ordinary `LoaderData` and so // memory the allocator would otherwise hand out. Empty on a boot whose // parameter line names none. @@ -346,7 +349,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { mm::Region { start: kernel_args.microcode_addr, end: kernel_args.microcode_addr + kernel_args.microcode_len }, ]; // A region the loader did not allocate withholds memory nothing uses, so one the firmware map does not hold as `LoaderData` is refused. - // Block 1: the ELF region (`kernel_elf_addr`+`kernel_elf_size`) is not page-aligned. + // Block 1: the section's region (`kernel_image_addr`+`kernel_image_size`) is not page-aligned. for region in loader.iter().filter(|r| r.start < r.end) { assert!( held( @@ -377,9 +380,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { reserved[a].start, reserved[a].end, reserved[b].start, reserved[b].end ); } - let layout = toyos_elf::Layout::parse(kernel_elf, arch::ELF_MACHINE) - .unwrap_or_else(|e| panic!("boot: the loader's kernel.elf: {e}")); - let image = mm::Image::new(&layout, image); + let image = mm::Image::new(&kernel_image, image); // Before the first hash container, `mm::init`'s address space. hasher::seed(); @@ -397,7 +398,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { let platform = arch::boot::interrupts(kernel_args.rsdp_addr); counters::bring_up(); symbols::set_kernel_base(kernel_args.kernel_memory_addr); - symbols::load_kernel(kernel_elf, mm::PHYS_OFFSET + kernel_args.kernel_memory_addr); + symbols::load_kernel(kernel_image.symbols, mm::PHYS_OFFSET + kernel_args.kernel_memory_addr); arch::boot::clock(kernel_args); trace::enable(); @@ -445,7 +446,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // Off the loader's images, so nothing from here to the supervisor's // spawn asks a disk for anything: every storage driver comes up after it. // The supervisor reads /system/etc/system.manifest itself; the boot config never names the program it starts. - let pid = process::spawn_supervisor(bootimage::objects(boot_images)); + let pid = process::spawn_supervisor(bootimage::objects(boot_images), &kernel_image.supervisor); log!("spawned the supervisor pid={pid}"); let t_devices = clock::nanos_since_boot(); diff --git a/kernel/src/mm/image.rs b/kernel/src/mm/image.rs index 34f93ed304f..c87be7469d3 100644 --- a/kernel/src/mm/image.rs +++ b/kernel/src/mm/image.rs @@ -1,63 +1,44 @@ //! The kernel's own image where the loader placed it, and what each of its -//! pages may be used for: its `PT_LOAD` segments' flags, page by page, and -//! its `PT_GNU_RELRO` pages read-only, since the loader applied every +//! pages may be used for: the build's ranges for it (`toyos_kimage`), page by +//! page, and its `PT_GNU_RELRO` pages read-only, since the loader applied every //! relocation before the kernel ran. -use toyos_elf::{Layout, Segment}; +use toyos_abi::syscall::VmProt; +use toyos_kimage::Kernel; use super::policy::Prot; use super::{Region, PAGE_SIZE, PHYS_OFFSET}; -/// The loader's allocation for the kernel: the image at its own link -/// addresses, then the boot stack. +/// The loader's allocation for the kernel: the image at its own addresses, +/// then the boot stack. pub struct Image<'a> { memory: Region, - /// Where image offset 0 is. - base: u64, - segments: &'a [Segment], - /// The one executable segment's pages, physical. + kernel: Kernel<'a>, + /// The one executable range's pages, physical. text: Region, /// `PT_GNU_RELRO`'s whole pages, physical. relro: Region, } impl<'a> Image<'a> { - /// The image `layout` describes, loaded into `memory`. + /// The image `kernel` describes, loaded into `memory`. /// - /// Panics where a page could not have a single protection that is not - /// both writable and executable: an executable segment that is writable, - /// two segments that share a page, or other than one executable segment, - /// which is what the loader's boot map maps read-only. And where it has no - /// `PT_GNU_RELRO` of a whole page opening a writable segment, so a link - /// that loses it, or moves it into the middle of one, stops the boot. - pub fn new(layout: &'a Layout, memory: Region) -> Self { - let segments = layout.segments(); - if let Some((a, b)) = layout.overlapping_load_pages(PAGE_SIZE) { - panic!("kernel image: PT_LOAD segments {a} and {b} share a page"); - } - let mut executable = segments.iter().filter(|segment| segment.flags().executable()); - let (Some(text), None) = (executable.next(), executable.next()) else { - panic!("kernel image: not exactly one executable PT_LOAD segment"); - }; - assert!(!text.writable(), "kernel image: its executable segment is writable"); - let base = memory.start + layout.extent().min(); - let (start, end) = text.page_range(PAGE_SIZE); - let text = Region { start: base + start, end: base + end }; + /// Panics where it has no RELRO of whole pages opening a writable range, + /// so a link that loses it, or moves it into the middle of one, stops the + /// boot. `Kernel::parse` already refused ranges that are not one + /// executable and disjoint whole pages. + pub fn new(kernel: &Kernel<'a>, memory: Region) -> Self { + let text = kernel.text().unwrap_or_else(|why| panic!("{why}")); + let text = Region { start: memory.start + text.lo, end: memory.start + text.hi }; assert!(memory.start <= text.start && text.end <= memory.end, "kernel image: its text is outside its memory"); - let opens = layout.relro().map(|relro| relro.start().get()); - let Some(opens) = opens else { panic!("kernel image: no PT_GNU_RELRO, so nothing holds its relocated pointers read-only") }; - // Its first page is taken whole: the bytes before it must be no - // writable segment's, which they are where it opens its segment, as no - // two segments share a page. + let (lo, hi) = kernel.relro; + assert!(lo < hi, "kernel image: no PT_GNU_RELRO, so nothing holds its relocated pointers read-only"); assert!( - segments.iter().any(|segment| segment.writable() && segment.image().start().get() == opens), - "kernel image: PT_GNU_RELRO at {opens:#x} opens no writable PT_LOAD", + kernel.ranges().flatten().any(|range| range.prot == VmProt::ReadWrite && range.lo == lo), + "kernel image: PT_GNU_RELRO at {lo:#x} opens no writable range", ); - let Some((start, end)) = layout.relro_pages(PAGE_SIZE) else { - panic!("kernel image: PT_GNU_RELRO at {opens:#x} holds no whole page"); - }; - let relro = Region { start: base + start, end: base + end }; - Self { memory, base, segments, text, relro } + let relro = Region { start: memory.start + lo, end: memory.start + hi }; + Self { memory, kernel: *kernel, text, relro } } /// Whether any byte of `[phys, phys + len)` is the image's. @@ -66,21 +47,17 @@ impl<'a> Image<'a> { } /// What the 4 KiB page at `phys` may be used for, or `None` for a page - /// that is not the image's: its segment's flags, and a page no segment + /// that is not the image's: its range's protection, and a page no range /// holds — the boot stack — is data. pub fn prot(&self, phys: u64) -> Option { if !self.overlaps(phys, PAGE_SIZE) { return None; } - let holder = phys.checked_sub(self.base).and_then(|offset| { - self.segments.iter().find(|segment| { - let (start, end) = segment.page_range(PAGE_SIZE); - (start..end).contains(&offset) - }) - }); - Some(match holder { - Some(segment) if segment.flags().executable() => Prot::ReadExec, - Some(segment) if !segment.writable() => Prot::Read, + let offset = phys - self.memory.start; + let holder = self.kernel.ranges().flatten().find(|range| (range.lo..range.hi).contains(&offset)); + Some(match holder.map(|range| range.prot) { + Some(VmProt::ReadExec) => Prot::ReadExec, + Some(VmProt::Read) => Prot::Read, Some(_) if (self.relro.start..self.relro.end).contains(&phys) => Prot::Read, _ => Prot::ReadWrite, }) diff --git a/kernel/src/mm/space.rs b/kernel/src/mm/space.rs index 5028e659fbd..c51dd64d1f6 100644 --- a/kernel/src/mm/space.rs +++ b/kernel/src/mm/space.rs @@ -321,6 +321,12 @@ impl AddressSpace { } } + /// Whether `[start, start + len)` is whole pages of the user half that + /// nothing in the space holds. + pub fn vacant(&self, start: u64, len: u64) -> bool { + pages(start, len).is_some_and(|end| self.inventory.holds(start, end) && self.inventory.is_free(start, end)) + } + /// `len` bytes of private memory, committed unless `prot` is `None`, which /// reserves the range and nothing more. A commitment is promised its /// frames here, so it is refused here, `ResourceExhausted`, and never at a diff --git a/kernel/src/symbols.rs b/kernel/src/symbols.rs index 6592b70ed5e..1bc57a08357 100644 --- a/kernel/src/symbols.rs +++ b/kernel/src/symbols.rs @@ -1,66 +1,31 @@ //! The kernel's own symbols: [`SymbolTable`] is read from the fault handler, //! panic handler and double fault, so it never allocates, locks or does I/O. -//! `toyos-symbols` holds the pure byte-level decisions; this module holds only -//! what must run in that context, plus the boot-time global and `log!` -//! integration. The kernel names no program's frame: a user frame is recorded -//! as file and offset (`process::record_user_frame`). +//! The table is the build's (`toyos_kimage::Symbols`), read out of the kernel +//! section the loader handed over; `toyos-symbols` holds how a name is +//! printed. The kernel names no program's frame: a user frame is recorded as +//! file and offset (`process::record_user_frame`). use core::sync::atomic::{AtomicPtr, AtomicU64, Ordering}; use alloc::boxed::Box; -use toyos_elf::sym::SymTab; use toyos_symbols::demangled; -/// Zero-allocation symbol table: raw pointers into the kernel image's ELF sections, safe to call from any context including panic/double-fault. +/// The kernel's functions, in the section the loader handed over and keeps +/// mapped for the machine's life, and where the kernel's address 0 is. pub struct SymbolTable { - symtab: *const u8, - symtab_len: usize, - strtab: *const u8, - strtab_len: usize, + table: toyos_kimage::Symbols<'static>, base: u64, } -// SAFETY: the pointers name only the kernel image in the direct map, mapped -// for the life of the machine; `SymbolTable` is `!Send` only because -// `*const u8` is. -unsafe impl Send for SymbolTable {} -// SAFETY: no method writes through either pointer, so concurrent reads from -// multiple CPUs alias only immutable bytes. -unsafe impl Sync for SymbolTable {} - impl SymbolTable { - // A failed `locate` returns an empty table rather than an error: a kernel that cannot find its symbols still boots. - fn from_elf(data: &[u8], base: u64) -> Self { - let (symtab, strtab) = toyos_symbols::locate(data).unwrap_or((&[], &[])); - Self { - symtab: symtab.as_ptr(), - symtab_len: symtab.len(), - strtab: strtab.as_ptr(), - strtab_len: strtab.len(), - base, - } - } - - fn tables(&self) -> SymTab<'_> { - // SAFETY: both ranges are slices `locate` borrowed out of the kernel - // image in the direct map, mapped for the life of the machine, or the - // dangling-but-aligned pointer of an empty slice with length 0. - unsafe { - SymTab::new( - core::slice::from_raw_parts(self.symtab, self.symtab_len), - core::slice::from_raw_parts(self.strtab, self.strtab_len), - ) - } - } - /// Resolve an address to (mangled name, offset); no allocation, lock or panic. - pub fn resolve(&self, addr: u64) -> Option<(&str, u64)> { - self.tables().resolve(addr.checked_sub(self.base)?) + pub fn resolve(&self, addr: u64) -> Option<(&'static str, u64)> { + self.table.resolve(addr.checked_sub(self.base)?) } /// [`resolve`](Self::resolve) for a return address: steps back one byte /// first, since a return address can land one past the callee's last byte. - pub fn resolve_return(&self, return_addr: u64) -> Option<(&str, u64)> { + pub fn resolve_return(&self, return_addr: u64) -> Option<(&'static str, u64)> { let (name, offset) = self.resolve(return_addr.saturating_sub(1))?; Some((name, offset + 1)) } @@ -75,10 +40,10 @@ pub fn set_kernel_base(base: u64) { KERNEL_BASE.store(base, Ordering::Release); } -/// Load kernel symbols from raw ELF bytes in the direct map; called once at boot. -pub fn load_kernel(data: &[u8], base: u64) { - let table = SymbolTable::from_elf(data, base); - let count = table.tables().count(); +/// Take the kernel's symbols, its address 0 at `base`; called once at boot. +pub fn load_kernel(table: toyos_kimage::Symbols<'static>, base: u64) { + let count = table.count(); + let table = SymbolTable { table, base }; KERNEL_SYMS.store(Box::into_raw(Box::new(table)), Ordering::Release); log!("symbols: loaded {} kernel symbols", count); } diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index a5e5f10f0ab..4de818a4d49 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -47,11 +47,11 @@ use super::machine::{ use super::machine::lower_sysinfo_bound; use super::paged::{sys_paged_create, sys_paged_holder, sys_paged_notices, sys_paged_op, sys_paged_read, sys_pager_create}; use super::proc::{ - spawn_pager, spawn_place, sys_endowments, sys_exit, sys_nanosleep, sys_process_stats, + spawn_pager, spawn_place, spawn_program, sys_endowments, sys_exit, sys_nanosleep, sys_process_stats, sys_account_info, sys_job_create, sys_job_floor, sys_job_own, sys_process_quit, sys_process_wait, sys_rt_enter, sys_spawn, sys_thread_exit, sys_thread_get_affinity, sys_thread_join, sys_thread_set_affinity, sys_set_thread_pointer, sys_thread_spawn, sys_wake_latency_hold, sys_yield, }; use super::vm::{ - shared_image, sys_vm_decommit, sys_vm_discard, sys_vm_remap, + sys_vm_decommit, sys_vm_discard, sys_vm_remap, sys_vm_protect, sys_vm_release, sys_vm_reserve, }; @@ -109,14 +109,11 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_PIPE => sys_pipe(), SYS_SPAWN => { let Ok(args) = ctx.copy_in::(UserAddr::new(a1)) else { return bad_addr }; - // First, and nothing copied: the image is the caller's object, and - // an endowment below may move the caller's own handle to it. - let image = match args.image_len { - 0 => return SyscallError::InvalidArgument.to_u64(), - len => match shared_image(args.image, len) { - Ok(image) => alloc::sync::Arc::new(image) as alloc::sync::Arc, - Err(refused) => return refused, - }, + // First: each range's object is the caller's, and an endowment + // below may move the caller's own handle to it. + let program = match spawn_program(&ctx, &args) { + Ok(program) => program, + Err(refused) => return refused, }; // Before an endowment can move the handle that names it. let parent = match spawn_place(args.place) { @@ -177,7 +174,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> alloc::vec::Vec::new() }; let argv: alloc::vec::Vec<&str> = text.split('\0').filter(|s| !s.is_empty()).collect(); - sys_spawn(&path, &argv, pending, env, image, parent) + sys_spawn(&path, &argv, pending, env, program, parent) } SYS_PROCESS_WAIT => sys_process_wait(RawHandle(a1 as u32), a2), SYS_PROCESS_KILL => { diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index aa55ef2c404..e96a287f96c 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -13,7 +13,7 @@ mod ipc; mod machine; mod paged; mod proc; -mod vm; +pub(crate) mod vm; use toyos_abi::handle::RawHandle; use toyos_abi::syscall::SyscallError; diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 544852cb953..d24bcc37174 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -66,17 +66,53 @@ pub(super) fn spawn_place(place: u64) -> Result { .map_err(|refused| refused.refuse()) } +/// The program `args` lays out: each range checked as an ordinary mapping, +/// and each object it copies looked up with `MAP` before an endowment could +/// move the handle naming it. +pub(super) fn spawn_program(ctx: &crate::user_ptr::SyscallContext<'_>, args: &SpawnArgs) -> Result { + let invalid = || SyscallError::InvalidArgument.to_u64(); + let count = usize::try_from(args.maps_count).ok().filter(|n| (1..=MAX_SPAWN_MAPS).contains(n)).ok_or_else(invalid)?; + if args.build_id_len > MAX_BUILD_ID as u64 { + return Err(invalid()); + } + let mut maps = Vec::new(); + maps.try_reserve_exact(count).map_err(|_| SyscallError::ResourceExhausted.to_u64())?; + for i in 0..count { + let at = args.maps_ptr.checked_add((i * core::mem::size_of::()) as u64).ok_or_else(invalid)?; + let raw = ctx.copy_in::(UserAddr::new(at)).map_err(SyscallError::to_u64)?; + let object = match raw.object { + handle if handle == u64::from(toyos_abi::HANDLE_INVALID.0) => None, + handle => { + let raw_handle = u32::try_from(handle).map_err(|_| invalid())?; + let object = process::with_process_data(|data| { + data.handles.get::(RawHandle(raw_handle), Rights::MAP) + }); + Some(object.map_err(|refused| refused.refuse())?) + } + }; + maps.push(crate::loader::Map::checked(&raw, object).map_err(SyscallError::to_u64)?); + } + let build_id = match args.build_id_len { + 0 => None, + len => { + let bytes = ctx.user_vec(UserAddr::new(args.build_id_ptr), len).map_err(SyscallError::to_u64)?; + toyos_symbols::frame::BuildId::new(&bytes) + } + }; + Ok(crate::loader::Program { maps, entry: args.entry, image_base: args.image_base, build_id }) +} + /// Start a program under `parent` and return the handle to it the spawn's commit put in the caller's table. pub(super) fn sys_spawn( path: &str, args: &[&str], pending: crate::loader::PendingHandles, env: Vec, - image: alloc::sync::Arc, + program: crate::loader::Program, parent: process::Parent, ) -> u64 { // Nothing to clean up: spawn's frame owns the child's resources on error. - match process::spawn(path, args, pending, env, image, parent) { + match process::spawn(path, args, pending, env, program, parent) { Ok((_, handle)) => u64::from(handle.0), Err(e) => e.refuse(), } diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index a6408be9696..756d998bac0 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -21,7 +21,7 @@ fn whole_pages(size: u64) -> Option { /// The protection a `VmProt` word names. No word names write and execute at /// once: a program's loader writes its code `ReadWrite` and then protects it /// `ReadExec`, so no page of private memory is ever both. -fn vm_prot(word: u64) -> Result { +pub(crate) fn vm_prot(word: u64) -> Result { Ok(match VmProt::from_u64(word).ok_or(SyscallError::InvalidArgument)? { VmProt::None => Prot::None, VmProt::Read => Prot::Read, @@ -106,17 +106,4 @@ pub(super) fn sys_vm_remap(addr: u64, old: u64, new: u64, may_move: u64) -> u64 remapped.map_or_else(SyscallError::to_u64, UserAddr::raw) } -/// The first `len` bytes of the shared memory object `handle` names, as a -/// program image; `Err` is the syscall's answer. -pub(super) fn shared_image(handle: u64, len: u64) -> Result { - let Ok(raw) = u32::try_from(handle) else { return Err(SyscallError::InvalidArgument.to_u64()) }; - let object = process::with_process_data(|data| { - data.handles.get::( - toyos_abi::handle::RawHandle(raw), - toyos_abi::handle::Rights::MAP, - ) - }) - .map_err(|e| e.refuse())?; - crate::file_backing::SharedImage::over(object, len).map_err(|e| e.to_u64()) -} diff --git a/lib/elf/Cargo.toml b/lib/elf/Cargo.toml deleted file mode 100644 index e03058be52c..00000000000 --- a/lib/elf/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt: the -# kernel depends on it by path and its tests run on the host, -# with no target-triple juggling. An ELF is bytes somebody else wrote, and every -# one a loader opens crossed a trust boundary — so the parser has to be -# exercised against hostile bytes far more often than a QEMU boot allows. -# `toyos`'s loader (`toyos::dl`) depends on it, so std does, and it is -# published before `toyos` (`build/sdkversion.rs`). - -[package] -name = "toyos-elf" -description = "ELF64 decoding of untrusted program images (no_std, no alloc, forbid(unsafe_code))." -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" -repository = "https://github.com/ToyOSOrg/ToyOS" - -[features] -rustc-dep-of-std = ["core"] - -[dependencies] -core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } - -[dev-dependencies] -# unaligned: an include_bytes! fixture is aligned to 1, and object reads a header in place. -object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] } diff --git a/lib/kimage/Cargo.toml b/lib/kimage/Cargo.toml new file mode 100644 index 00000000000..a87aadf14f0 --- /dev/null +++ b/lib/kimage/Cargo.toml @@ -0,0 +1,24 @@ +# A member of the host workspace (root `Cargo.toml`), like toyos-rootimage: the +# loader and the kernel depend on it by path, the build writes it, and its +# tests run on the host. What lives here is the kernel section of a signed +# image: the kernel already laid out, its relocations, its ranges and symbols, +# and the supervisor's program, so neither the loader nor the kernel parses an +# ELF. + +[package] +name = "toyos-kimage" +description = "The kernel's boot image: laid out by the build, read by the loader and the kernel without an ELF parse." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[features] +# The build's writer, which allocates. +write = [] + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } + +[dev-dependencies] +toyos-kimage = { path = ".", features = ["write"] } diff --git a/lib/kimage/src/lib.rs b/lib/kimage/src/lib.rs new file mode 100644 index 00000000000..d9b79593aae --- /dev/null +++ b/lib/kimage/src/lib.rs @@ -0,0 +1,290 @@ +//! The kernel section of a signed image (`toyos_update::image::KERNEL`): the +//! kernel laid out by the build at its own addresses, the `RELATIVE` +//! relocations the loader applies once it has placed it, the protection of +//! each range, its symbols, and the supervisor's program +//! (`toyos_abi::syscall::SpawnMap`s over the supervisor section). The build +//! reads the kernel's and the supervisor's ELF with `toyos::elf`; the loader +//! and the kernel read only this. +//! +//! ```text +//! header magic "TOYOSKRN" | format u32 | e_machine u16 | 0 u16 +//! entry u64 | memory u64 | relro [lo, hi) u64 u64 +//! ranges u32 | relocations u32 | symbols u32 | names u32 | image_at u64 +//! ranges [lo u64 | hi u64 | VmProt u64], page-aligned, ascending, disjoint +//! relocations [offset u64 | addend u64]: base + addend, 8 bytes at offset +//! symbols [addr u64 | size u64 | name u32 | len u32], ascending by addr +//! names the symbols' names +//! supervisor entry u64 | image_base u64 | build-id len u64 | build-id [32] | maps u64 +//! [SpawnMap], `object` 0 for the supervisor section +//! image at image_at, a page boundary: the image's bytes from address 0, +//! zeros past them to `memory` +//! ``` +//! +//! Little-endian throughout. The section is signed, so a malformed one is the +//! build's bug: [`Kernel::parse`] refuses it by name, as a bug fence, rather +//! than trusting its counts. + +#![no_std] +#![forbid(unsafe_code)] + +#[cfg(feature = "write")] +extern crate alloc; + +#[cfg(feature = "write")] +mod write; +#[cfg(feature = "write")] +pub use write::{Input, Symbol}; + +use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID, MAX_SPAWN_MAPS}; + +pub const MAGIC: [u8; 8] = *b"TOYOSKRN"; +pub const FORMAT: u32 = 1; +const HEADER: usize = 72; +const RANGE: usize = 24; +const RELOCATION: usize = 16; +const SYMBOL: usize = 24; +const PROGRAM: usize = 64; +const MAP: usize = core::mem::size_of::(); +pub const PAGE: u64 = 4096; + +/// One range of the image and what its pages may be used for. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Range { + pub lo: u64, + pub hi: u64, + pub prot: VmProt, +} + +/// A kernel section, its every count and offset checked against its bytes. +#[derive(Clone, Copy, Debug)] +pub struct Kernel<'a> { + pub machine: u16, + /// The entry's address in the image. + pub entry: u64, + /// Bytes the image occupies from address 0, whole pages. + pub memory: u64, + /// `PT_GNU_RELRO`'s whole pages, read-only once relocated. + pub relro: (u64, u64), + ranges: &'a [u8], + relocations: &'a [u8], + pub symbols: Symbols<'a>, + pub supervisor: Supervisor<'a>, + /// The image's bytes from address 0, at most `memory`. + pub image: &'a [u8], +} + +fn u64_at(bytes: &[u8], at: usize) -> Option { + Some(u64::from_le_bytes(bytes.get(at..at.checked_add(8)?)?.try_into().ok()?)) +} + +fn u32_at(bytes: &[u8], at: usize) -> Option { + Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?)) +} + +/// `count` records of `size` bytes from `*at`, moving `*at` past them. +fn take<'a>(bytes: &'a [u8], at: &mut usize, count: usize, size: usize) -> Option<&'a [u8]> { + let end = count.checked_mul(size)?.checked_add(*at)?; + let taken = bytes.get(*at..end)?; + *at = end; + Some(taken) +} + +impl<'a> Kernel<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + const SHORT: &str = "kernel image: a table past the section's end"; + if bytes.get(..8) != Some(&MAGIC[..]) { + return Err("kernel image: not TOYOSKRN"); + } + if u32_at(bytes, 8) != Some(FORMAT) { + return Err("kernel image: another format"); + } + let field = |at| u64_at(bytes, at).ok_or(SHORT); + let count = |at| u32_at(bytes, at).map(|n| n as usize).ok_or(SHORT); + let machine = u16::from_le_bytes([bytes.get(12).copied().ok_or(SHORT)?, bytes.get(13).copied().ok_or(SHORT)?]); + let (entry, memory, relro) = (field(16)?, field(24)?, (field(32)?, field(40)?)); + let mut at = HEADER; + let ranges = take(bytes, &mut at, count(48)?, RANGE).ok_or(SHORT)?; + let relocations = take(bytes, &mut at, count(52)?, RELOCATION).ok_or(SHORT)?; + let entries = take(bytes, &mut at, count(56)?, SYMBOL).ok_or(SHORT)?; + let names = take(bytes, &mut at, count(60)?, 1).ok_or(SHORT)?; + let program = take(bytes, &mut at, 1, PROGRAM).ok_or(SHORT)?; + let maps = usize::try_from(u64_at(program, 56).ok_or(SHORT)?).map_err(|_| SHORT)?; + if maps > MAX_SPAWN_MAPS { + return Err("kernel image: more supervisor ranges than a spawn carries"); + } + let maps = take(bytes, &mut at, maps, MAP).ok_or(SHORT)?; + let id_len = u64_at(program, 16).filter(|&n| n <= MAX_BUILD_ID as u64).ok_or("kernel image: a build-id past its bound")?; + let image_at = usize::try_from(field(64)?).map_err(|_| SHORT)?; + let image = bytes.get(image_at..).filter(|_| image_at >= at && image_at as u64 % PAGE == 0).ok_or(SHORT)?; + let kernel = Kernel { + machine, + entry, + memory, + relro, + ranges, + relocations, + symbols: Symbols { entries, names }, + supervisor: Supervisor { + entry: u64_at(program, 0).ok_or(SHORT)?, + image_base: u64_at(program, 8).ok_or(SHORT)?, + build_id: &program[24..24 + id_len as usize], + maps, + }, + image, + }; + kernel.check() + } + + fn check(self) -> Result { + if !self.memory.is_multiple_of(PAGE) || self.image.len() as u64 > self.memory { + return Err("kernel image: its bytes are not whole pages of its memory"); + } + let mut covered = 0; + for range in self.ranges() { + let range = range.ok_or("kernel image: a range with no protection")?; + if range.lo < covered || range.hi <= range.lo || range.hi > self.memory || range.lo % PAGE != 0 || range.hi % PAGE != 0 { + return Err("kernel image: ranges that are not ascending whole pages of its memory"); + } + covered = range.hi; + } + self.text()?; + if self.entry >= self.memory { + return Err("kernel image: an entry past its memory"); + } + let (lo, hi) = self.relro; + if lo > hi || hi > self.memory || lo % PAGE != 0 || hi % PAGE != 0 { + return Err("kernel image: RELRO that is not whole pages of its memory"); + } + if self.relocations().any(|(offset, addend)| offset.checked_add(8).is_none_or(|end| end > self.memory) || addend > self.memory) { + return Err("kernel image: a relocation outside its memory"); + } + self.symbols.check()?; + Ok(self) + } + + /// Every range and its protection; `None` for a word that names none. + pub fn ranges(&self) -> impl Iterator> + 'a { + self.ranges.chunks_exact(RANGE).map(|r| { + let (lo, hi) = (u64_at(r, 0)?, u64_at(r, 8)?); + Some(Range { lo, hi, prot: VmProt::from_u64(u64_at(r, 16)?)? }) + }) + } + + /// The one executable range, which no writable byte shares a page with. + pub fn text(&self) -> Result { + let mut executable = self.ranges().flatten().filter(|r| r.prot == VmProt::ReadExec); + match (executable.next(), executable.next()) { + (Some(text), None) => Ok(text), + _ => Err("kernel image: not exactly one executable range"), + } + } + + /// `(offset, addend)`: the loader writes its base plus `addend` at `offset`. + pub fn relocations(&self) -> impl Iterator + 'a { + self.relocations.chunks_exact(RELOCATION).filter_map(|r| Some((u64_at(r, 0)?, u64_at(r, 8)?))) + } +} + +/// The kernel's functions, each at the address nearest below an address that +/// names it. +#[derive(Clone, Copy, Debug)] +pub struct Symbols<'a> { + entries: &'a [u8], + names: &'a [u8], +} + +impl<'a> Symbols<'a> { + pub fn count(&self) -> usize { + self.entries.len() / SYMBOL + } + + fn entry(&self, i: usize) -> Option<(u64, u64, &'a str)> { + let e = self.entries.get(i.checked_mul(SYMBOL)?..)?; + let (name, len) = (u32_at(e, 16)? as usize, u32_at(e, 20)? as usize); + let name = core::str::from_utf8(self.names.get(name..name.checked_add(len)?)?).ok()?; + Some((u64_at(e, 0)?, u64_at(e, 8)?, name)) + } + + fn check(&self) -> Result<(), &'static str> { + let mut last = 0; + for i in 0..self.count() { + let (addr, _, _) = self.entry(i).ok_or("kernel image: a symbol whose name is past the names")?; + if addr <= last && i != 0 { + return Err("kernel image: symbols not ascending"); + } + last = addr; + } + Ok(()) + } + + /// The function `offset` falls in, and how far in: the nearest at or + /// below it, owning every address up to the next if it has no size, and + /// none past its last byte if it has one. No allocation, lock or panic: a + /// panic handler and an NMI read it. + pub fn resolve(&self, offset: u64) -> Option<(&'a str, u64)> { + let (mut lo, mut hi) = (0, self.count()); + while lo < hi { + let mid = lo + (hi - lo) / 2; + match self.entry(mid)?.0 <= offset { + true => lo = mid + 1, + false => hi = mid, + } + } + let (addr, size, name) = self.entry(lo.checked_sub(1)?)?; + let within = offset - addr; + (!(size > 0 && within >= size) && !name.is_empty()).then_some((name, within)) + } +} + +/// The supervisor's program, as the build laid it out. +#[derive(Clone, Copy, Debug)] +pub struct Supervisor<'a> { + pub entry: u64, + pub image_base: u64, + pub build_id: &'a [u8], + maps: &'a [u8], +} + +/// What a supervisor range's `object` names: the supervisor section. +pub const SUPERVISOR_OBJECT: u64 = 0; + +impl Supervisor<'_> { + /// Its ranges, `object` [`SUPERVISOR_OBJECT`] or `HANDLE_INVALID` for zeros. + pub fn maps(&self) -> impl Iterator + '_ { + self.maps.chunks_exact(MAP).filter_map(|m| { + Some(SpawnMap { + addr: u64_at(m, 0)?, + len: u64_at(m, 8)?, + object: u64_at(m, 16)?, + offset: u64_at(m, 24)?, + copied: u64_at(m, 32)?, + prot: u64_at(m, 40)?, + }) + }) + } +} + +/// A static image loaded at its own addresses into one allocation with its +/// stack after it, in offsets from the allocation's base. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StackedImage { + /// Bytes to allocate: the image, then the stack. + pub size: u64, + /// Where the stack's lowest byte is; its top is `size`. + pub stack: u64, +} + +impl StackedImage { + /// The image ending at `end` with a `stack_size`-byte stack after it, or + /// `None` for one no address holds. The stack starts on the next page: + /// both ABIs want the stack pointer 16-byte aligned, which AArch64's + /// `SCTLR_EL1.SA` enforces on every access through it. `stack_size` is + /// whole pages, so the top is too. + pub fn place(end: u64, stack_size: u64) -> Option { + if !stack_size.is_multiple_of(PAGE) { + return None; + } + let stack = end.checked_next_multiple_of(PAGE)?; + Some(StackedImage { size: stack.checked_add(stack_size)?, stack }) + } +} diff --git a/lib/kimage/src/write.rs b/lib/kimage/src/write.rs new file mode 100644 index 00000000000..0093ba38d89 --- /dev/null +++ b/lib/kimage/src/write.rs @@ -0,0 +1,92 @@ +//! The build's half: a kernel section out of what the build read of the +//! kernel's and the supervisor's ELF. + +use alloc::vec::Vec; + +use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID}; + +use crate::{FORMAT, MAGIC, PAGE}; + +/// One function the kernel names a frame by. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Symbol { + pub addr: u64, + pub size: u64, + pub name: alloc::string::String, +} + +/// Everything a kernel section carries. +#[derive(Clone, Debug, Default)] +pub struct Input { + pub machine: u16, + pub entry: u64, + pub memory: u64, + pub relro: (u64, u64), + pub ranges: Vec<(u64, u64, VmProt)>, + pub relocations: Vec<(u64, u64)>, + /// Ascending by address, one per address. + pub symbols: Vec, + pub supervisor_entry: u64, + pub supervisor_base: u64, + pub supervisor_build_id: Vec, + pub supervisor_maps: Vec, + /// The image's bytes from address 0. + pub image: Vec, +} + +impl Input { + /// The section, as [`crate::Kernel::parse`] reads it. + pub fn write(&self) -> Vec { + assert!(self.supervisor_build_id.len() <= MAX_BUILD_ID, "a build-id past its bound"); + let mut names: Vec = Vec::new(); + let mut symbols: Vec = Vec::new(); + for symbol in &self.symbols { + symbols.extend(symbol.addr.to_le_bytes()); + symbols.extend(symbol.size.to_le_bytes()); + symbols.extend((names.len() as u32).to_le_bytes()); + symbols.extend((symbol.name.len() as u32).to_le_bytes()); + names.extend(symbol.name.as_bytes()); + } + let mut out: Vec = Vec::new(); + out.extend(MAGIC); + out.extend(FORMAT.to_le_bytes()); + out.extend(self.machine.to_le_bytes()); + out.extend(0u16.to_le_bytes()); + for word in [self.entry, self.memory, self.relro.0, self.relro.1] { + out.extend(word.to_le_bytes()); + } + for count in [self.ranges.len(), self.relocations.len(), self.symbols.len(), names.len()] { + out.extend((count as u32).to_le_bytes()); + } + let image_at_field = out.len(); + out.extend(0u64.to_le_bytes()); + for &(lo, hi, prot) in &self.ranges { + out.extend(lo.to_le_bytes()); + out.extend(hi.to_le_bytes()); + out.extend((prot as u64).to_le_bytes()); + } + for &(offset, addend) in &self.relocations { + out.extend(offset.to_le_bytes()); + out.extend(addend.to_le_bytes()); + } + out.extend(symbols); + out.extend(names); + out.extend(self.supervisor_entry.to_le_bytes()); + out.extend(self.supervisor_base.to_le_bytes()); + out.extend((self.supervisor_build_id.len() as u64).to_le_bytes()); + let mut id = [0u8; MAX_BUILD_ID]; + id[..self.supervisor_build_id.len()].copy_from_slice(&self.supervisor_build_id); + out.extend(id); + out.extend((self.supervisor_maps.len() as u64).to_le_bytes()); + for map in &self.supervisor_maps { + for word in [map.addr, map.len, map.object, map.offset, map.copied, map.prot] { + out.extend(word.to_le_bytes()); + } + } + let image_at = (out.len() as u64).next_multiple_of(PAGE); + out[image_at_field..image_at_field + 8].copy_from_slice(&image_at.to_le_bytes()); + out.resize(image_at as usize, 0); + out.extend(&self.image); + out + } +} diff --git a/lib/kimage/tests/round_trip.rs b/lib/kimage/tests/round_trip.rs new file mode 100644 index 00000000000..dbc4f1443c3 --- /dev/null +++ b/lib/kimage/tests/round_trip.rs @@ -0,0 +1,83 @@ +//! A section the build writes reads back as written, and every malformation +//! the reader fences is refused by name. + +use toyos_abi::syscall::{SpawnMap, VmProt}; +use toyos_kimage::{Input, Kernel, StackedImage, Symbol}; + +fn input() -> Input { + Input { + machine: 62, + entry: 0x1010, + memory: 0x4000, + relro: (0x2000, 0x3000), + ranges: vec![(0, 0x1000, VmProt::Read), (0x1000, 0x2000, VmProt::ReadExec), (0x2000, 0x4000, VmProt::ReadWrite)], + relocations: vec![(0x2000, 0x1010), (0x2008, 0)], + symbols: vec![ + Symbol { addr: 0x1000, size: 0x10, name: "a".into() }, + Symbol { addr: 0x1020, size: 0, name: "b".into() }, + Symbol { addr: 0x1080, size: 4, name: String::new() }, + ], + supervisor_entry: 0x2_0000_1000, + supervisor_base: 0x2_0000_0000, + supervisor_build_id: vec![0x5a; 20], + supervisor_maps: vec![SpawnMap { addr: 0x2_0000_0000, len: 0x1000, object: 0, offset: 0, copied: 0x800, prot: VmProt::ReadExec as u64 }], + image: vec![0xcc; 0x2100], + } +} + +#[test] +fn a_section_reads_back_as_written() { + let bytes = input().write(); + let kernel = Kernel::parse(&bytes).unwrap(); + assert_eq!((kernel.machine, kernel.entry, kernel.memory, kernel.relro), (62, 0x1010, 0x4000, (0x2000, 0x3000))); + assert_eq!(kernel.text().unwrap().lo, 0x1000); + assert_eq!(kernel.relocations().collect::>(), [(0x2000, 0x1010), (0x2008, 0)]); + assert_eq!(kernel.image, &[0xcc; 0x2100][..]); + assert_eq!((kernel.supervisor.entry, kernel.supervisor.build_id), (0x2_0000_1000, &[0x5a; 20][..])); + assert_eq!(kernel.supervisor.maps().collect::>(), input().supervisor_maps); +} + +/// The rules `SymTab::resolve` names a frame by: nearest at or below, a sized +/// one owning only its bytes, one with no size owning everything to the next, +/// and an empty name naming nothing. +#[test] +fn a_symbol_resolves_as_the_elf_table_would() { + let bytes = input().write(); + let symbols = Kernel::parse(&bytes).unwrap().symbols; + assert_eq!(symbols.resolve(0xfff), None); + assert_eq!(symbols.resolve(0x1004), Some(("a", 4))); + assert_eq!(symbols.resolve(0x1010), None); + assert_eq!(symbols.resolve(0x1070), Some(("b", 0x50))); + assert_eq!(symbols.resolve(0x1081), None); +} + +#[test] +fn a_malformed_section_is_refused_by_name() { + let refused = |edit: &dyn Fn(&mut Input)| { + let mut i = input(); + edit(&mut i); + Kernel::parse(&i.write()).err() + }; + assert!(refused(&|i| i.ranges.push((0x3000, 0x4000, VmProt::ReadExec))).is_some(), "two executable ranges"); + assert!(refused(&|i| i.ranges[2].1 = 0x5000).is_some(), "a range past memory"); + assert!(refused(&|i| i.relocations.push((0x3ffc, 0))).is_some(), "a relocation past memory"); + assert!(refused(&|i| i.symbols.swap(0, 1)).is_some(), "symbols out of order"); + assert!(refused(&|i| i.image = vec![0; 0x5000]).is_some(), "bytes past memory"); + let bytes = input().write(); + assert!(Kernel::parse(&bytes[..100]).is_err()); + assert!(Kernel::parse(b"TOYOSIMG").is_err()); +} + +#[test] +fn the_stack_after_an_image_is_aligned_wherever_the_image_ends() { + const STACK: u64 = 8 * 1024 * 1024; + assert_eq!(StackedImage::place(0x41_0008, STACK), Some(StackedImage { size: 0xc1_1000, stack: 0x41_1000 })); + for end in [1, 0x40_fc30, 0x41_0000, 0x41_0008, 0x41_0ff8, 0x41_0fff] { + let placed = StackedImage::place(end, STACK).expect("fits"); + assert!(placed.stack >= end && placed.stack - end < 4096, "{end:#x}: {placed:?}"); + assert_eq!(placed.stack % 16, 0); + assert_eq!(placed.size - placed.stack, STACK); + } + assert_eq!(StackedImage::place(u64::MAX - 8, STACK), None); + assert_eq!(StackedImage::place(0x41_0000, STACK + 8), None); +} diff --git a/lib/symbols/Cargo.toml b/lib/symbols/Cargo.toml index 4ee19d85df1..3fa9a370143 100644 --- a/lib/symbols/Cargo.toml +++ b/lib/symbols/Cargo.toml @@ -1,16 +1,13 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-elf and -# toyos-gpt: the kernel and `system/symbolize` depend on it by path and its -# tests run on the host, with no target-triple juggling. What lives here is -# every pure decision a backtrace frame needs over bytes: where in an ELF file -# its symbol tables live, how much of the record's budget a demangled name -# gets, the record a killed program's frame is written as, and the function a -# recorded offset names. The kernel keeps the raw-pointer table of its own symbols — it has to -# be readable from a panic, a double fault and an NMI, which is a static this -# crate cannot own and an `unsafe` this crate does not need. +# A member of the host workspace (root `Cargo.toml`), like toyos-gpt: the +# kernel and `system/symbolize` depend on it by path and its tests run on the +# host, with no target-triple juggling. What lives here is every pure decision +# a backtrace frame needs that reads no ELF: how much of the record's budget a +# demangled name gets, and the record a killed program's frame is written as. +# Reading an image is `toyos::elf`'s, which the kernel does not link. [package] name = "toyos-symbols" -description = "Backtrace frames: locating an ELF's symbol tables, budgeting the demangled name, and a killed program's file+offset record and its naming (no_std, no alloc, forbid(unsafe_code))." +description = "Backtrace frames: budgeting the demangled name, and a killed program's file+offset record (no_std, no alloc, forbid(unsafe_code))." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" @@ -18,7 +15,6 @@ license = "MIT OR Apache-2.0" [dependencies] rustc-demangle = "0.1" toyos-abi = { path = "../../sdk/abi" } -toyos-elf = { path = "../elf" } toyos-elide = { path = "../elide" } [dev-dependencies] diff --git a/lib/symbols/src/frame.rs b/lib/symbols/src/frame.rs index 7b1a8ba3e97..5084d6a30ea 100644 --- a/lib/symbols/src/frame.rs +++ b/lib/symbols/src/frame.rs @@ -20,7 +20,7 @@ use core::fmt::{self, Display, Write}; use toyos_abi::log::MAX_RECORD_MESSAGE; -use toyos_elf::note::{self, NoteSegment, MAX_BUILD_ID}; +use toyos_abi::syscall::MAX_BUILD_ID; use toyos_elide::{widest, Elided, MARKER_MAX}; /// A user frame's own text, every number at its widest: indent, `0x` and @@ -48,16 +48,6 @@ impl BuildId { pub fn as_bytes(&self) -> &[u8] { self.bytes.get(..usize::from(self.len)).unwrap_or(&[]) } - - /// The build-id of the image whose program header table `headers` holds: - /// the first `PT_NOTE` that `read` hands back bytes for and that carries - /// one. `read` answers a segment's bytes, as many of them as it will read. - pub fn find>(headers: &[u8], mut read: impl FnMut(NoteSegment) -> Option) -> Option { - note::segments(headers).find_map(|segment| { - let bytes = read(segment)?; - note::build_id(bytes.as_ref(), segment.align).and_then(BuildId::new) - }) - } } impl Display for BuildId { diff --git a/lib/symbols/src/lib.rs b/lib/symbols/src/lib.rs index a8b047f2c61..1606848cb72 100644 --- a/lib/symbols/src/lib.rs +++ b/lib/symbols/src/lib.rs @@ -1,8 +1,7 @@ -//! The pure half of a backtrace frame, shared by the kernel and by userland's -//! namer: where an ELF file's symbol tables live, how much of a record's budget -//! a demangled name gets, a killed program's frame record ([`frame`]), and the -//! function a recorded offset falls in ([`name`]), marked when no build-id -//! vouches for it. +//! The pure half of a backtrace frame that reads no ELF, shared by the kernel +//! and by userland's namer: how much of a record's budget a demangled name +//! gets, and a killed program's frame record ([`frame`]). Reading an image's +//! symbols or build-id is `toyos::elf`'s, which the kernel does not link. //! //! `kernel/src/symbols.rs` keeps the rest of the kernel's own frames — a //! `SymbolTable` holding raw pointers into the kernel image, because the @@ -11,10 +10,7 @@ //! every function is of the bytes a caller already has. //! //! `no_std`, no allocation, no `unsafe`, and no panic on any input: the kernel -//! runs it on its crash path and the namer runs it on whatever file a record -//! names. [`locate`] borrows straight out of its caller's bytes rather than -//! copying them, which is what lets the kernel's table point directly at the -//! ELF instead of a copy of it. +//! runs it on its crash path. #![no_std] #![forbid(unsafe_code)] @@ -25,13 +21,8 @@ extern crate std; pub mod frame; use toyos_abi::log::MAX_RECORD_MESSAGE; -use toyos_elf::section::{SectionTable, SHT_SYMTAB}; -use toyos_elf::sym::SymTab; -use toyos_elf::FileHeader; use toyos_elide::{widest, Elided, MARKER_MAX}; -use frame::BuildId; - /// A backtrace frame's own text: ` ` + `{addr:#x}` + ` ` + `+` + /// `{offset:#x}`, with both numbers at their widest — `0x` and sixteen hex /// digits is every `u64` there is. @@ -75,99 +66,6 @@ pub fn demangled(raw: &str) -> Elided, SYMBOL_HEAD, Elided(rustc_demangle::demangle(raw)) } -/// `[offset, offset + len)` of `data`, or `None` when that is not wholly inside -/// it. Both numbers came out of the file, so the addition is checked. -pub fn file_range(data: &[u8], offset: u64, len: u64) -> Option<&[u8]> { - let start = usize::try_from(offset).ok()?; - let end = usize::try_from(offset.checked_add(len)?).ok()?; - data.get(start..end) -} - -/// Bytes the section header table occupies. Cannot overflow: both factors are -/// `u16`, and `e_shentsize` is honoured rather than assumed. -pub fn shdr_table_len(ehdr: &toyos_elf::FileHeader) -> u64 { - ehdr.shnum as u64 * ehdr.shentsize as u64 -} - -/// `.symtab` and its `.strtab`, as byte slices straight out of `data` — no -/// copying, only bounds checks. `None` when the file has no symbol table, or -/// when a declared extent does not fit inside `data`; both are "no symbols", -/// which the kernel's caller turns into a table that names nothing rather -/// than a refused boot — a kernel that cannot find its own symbols still -/// boots and prints bare addresses. -/// -/// Every extent here came out of the file, so every one of them is bounded -/// against it rather than trusted: a section running past EOF would otherwise -/// be read as whatever follows the image in memory. -pub fn locate(data: &[u8]) -> Option<(&[u8], &[u8])> { - let ehdr = toyos_elf::FileHeader::parse(data).ok()?; - let shdrs = file_range(data, ehdr.shoff, shdr_table_len(&ehdr))?; - let (syms, strs) = SectionTable::new(shdrs).symbols(SHT_SYMTAB)?; - let symtab = file_range(data, syms.offset, syms.size)?; - let strtab = file_range(data, strs.offset, strs.size)?; - Some((symtab, strtab)) -} - -/// Why a recorded frame names no function. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Unnamed { - /// The file is no ELF image this crate reads. - NotElf(toyos_elf::Error), - /// The file's build-id is not the one the record carries: it is another - /// build of that name, or one of the two has none. - OtherBuild { file: Option }, - /// The file has no symbol table. - NoSymbols, - /// No function in the file's symbol table holds the offset. - NoSymbol, -} - -/// Whether a name is known to be of the build that ran. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Check { - /// The file carries the build-id the record does. - BuildId, - /// Neither carries one, so nothing tells this build from another build of - /// that path: the name is printed, and printed as unchecked. - Unchecked, -} - -/// A recorded offset named: the function it falls in, how far into it, and -/// what vouches that the file is the build that ran. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Named<'a> { - pub function: &'a str, - pub within: u64, - pub check: Check, -} - -/// `+`, and ` (unchecked: no build-id)` after a name -/// [`Check::Unchecked`]. -impl core::fmt::Display for Named<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}+{:#x}", demangled(self.function), self.within)?; - match self.check { - Check::BuildId => Ok(()), - Check::Unchecked => f.write_str(" (unchecked: no build-id)"), - } - } -} - -/// The function `offset` falls in, and how far into it, out of `file`'s own -/// symbol table — when `file` is the build `id` names (`None`: a file with no -/// build-id). The same lookup the kernel names its own frames with. -pub fn name<'a>(file: &'a [u8], offset: u64, id: Option<&BuildId>) -> Result, Unnamed> { - FileHeader::parse(file).map_err(Unnamed::NotElf)?; - let carried = BuildId::find(file, |s| file_range(file, s.offset, s.filesz)); - if carried.as_ref() != id { - return Err(Unnamed::OtherBuild { file: carried }); - } - let check = if carried.is_some() { Check::BuildId } else { Check::Unchecked }; - let (symtab, strtab) = locate(file).ok_or(Unnamed::NoSymbols)?; - let (function, within) = SymTab::new(symtab, strtab).resolve(offset).ok_or(Unnamed::NoSymbol)?; - Ok(Named { function, within, check }) -} - #[cfg(test)] mod tests { use super::*; @@ -188,25 +86,4 @@ mod tests { assert!(rendered.contains("bytes elided")); assert!(rendered.len() <= SYMBOL_BUDGET); } - - #[test] - fn file_range_refuses_an_extent_the_addition_overflows() { - assert_eq!(file_range(b"abc", 1, u64::MAX), None); - } - - #[test] - fn file_range_refuses_an_extent_past_the_end() { - assert_eq!(file_range(b"abcdef", 3, 10), None); - } - - #[test] - fn file_range_answers_an_extent_wholly_inside() { - assert_eq!(file_range(b"abcdef", 2, 3), Some(&b"cde"[..])); - } - - #[test] - fn locate_refuses_bytes_too_short_for_a_file_header() { - assert_eq!(locate(&[]), None); - assert_eq!(locate(&[0u8; 10]), None); - } } diff --git a/lib/userbound/Cargo.toml b/lib/userbound/Cargo.toml index e3a84bf7d72..99142ec8dd1 100644 --- a/lib/userbound/Cargo.toml +++ b/lib/userbound/Cargo.toml @@ -1,5 +1,5 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt, toyos-pci -# and toyos-elf: the kernel depends on it by path and its tests run on the host, +# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and +# toyos-pci: the kernel depends on it by path and its tests run on the host, # with no target-triple juggling. What lives here is the line between Ring 3 and # Ring 0 as it appears in an address — the one thing every user pointer the # kernel dereferences is checked against, and the one thing a trap is classified diff --git a/loader/Cargo.toml b/loader/Cargo.toml index 92fcff91b54..f665be9f5da 100644 --- a/loader/Cargo.toml +++ b/loader/Cargo.toml @@ -18,7 +18,8 @@ toyos-acpi = { path = "../lib/acpi" } toyos-bootmap = { path = "../lib/bootmap" } toyos-blackbox = { path = "../lib/blackbox" } toyos-wallclock = { path = "../lib/wallclock" } -toyos-elf = { path = "../lib/elf" } +# The kernel section, laid out by the build: no ELF is parsed here. +toyos-kimage = { path = "../lib/kimage" } # ROOT is picked off the boot disk by the GPT. toyos-gpt = { path = "../lib/gpt" } # Which ROOT, how it is read in bounded chunks, and the handoff the kernel diff --git a/loader/src/arch/aarch64.rs b/loader/src/arch/aarch64.rs index 304e27c5da2..8a7a4ad9baf 100644 --- a/loader/src/arch/aarch64.rs +++ b/loader/src/arch/aarch64.rs @@ -3,8 +3,8 @@ use toyos_abi::boot::KernelArgs; use toyos_bootmap::Typing; -/// The machine the kernel image must be built for: the loader's own. -pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::Aarch64; +/// `e_machine` of the kernel this loader starts (`toyos_kimage::Kernel::machine`). +pub const E_MACHINE: u16 = 183; /// How the boot map's descriptors are encoded. pub use toyos_bootmap::aarch64 as encoding; diff --git a/loader/src/arch/x86_64.rs b/loader/src/arch/x86_64.rs index fe23de90e84..de3b7fd7435 100644 --- a/loader/src/arch/x86_64.rs +++ b/loader/src/arch/x86_64.rs @@ -2,8 +2,8 @@ use toyos_abi::boot::KernelArgs; -/// The machine the kernel image must be built for: the loader's own. -pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64; +/// `e_machine` of the kernel this loader starts (`toyos_kimage::Kernel::machine`). +pub const E_MACHINE: u16 = 62; /// How the boot map's entries are encoded. pub use toyos_bootmap::x86_64 as encoding; diff --git a/loader/src/main.rs b/loader/src/main.rs index 16a2e9c40f4..f9cd4c9d89b 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -8,8 +8,6 @@ use core::mem; use alloc::vec; use alloc::alloc::Layout; use toyos_devicepath::HardDrive; -use toyos_elf::section::SectionTable; -use toyos_elf::{rela, ImageOffset, Op, RelaTable, SymTab}; use efi::{ cstr16, CStr16, DevicePath, EdidActive, Gop, Handle, LoadedImage, MemoryDescriptor, Mode, PixelFormat, ResetType, SimpleFileSystem, Status, SystemTable, ACPI2_GUID, PAGE_SIZE, @@ -247,125 +245,43 @@ const _: () = assert!(WATCHDOG_CODE > 0xffff); /// Kernel virtual base: all physical memory is mapped here in the kernel's address space. const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000; -/// `[offset, offset + len)` of the file, or `None` when that is not wholly -/// inside it. -/// -/// Every `offset` and `len` passed here came out of the image's own headers, so -/// both are numbers the file chose: the addition is checked and the bytes are -/// taken with `get` rather than indexed. Each caller refuses on `None` — a -/// table this cannot cover is never read short. -fn file_range(bytes: &[u8], offset: u64, len: u64) -> Option<&[u8]> { - let start = usize::try_from(offset).ok()?; - let end = usize::try_from(offset.checked_add(len)?).ok()?; - bytes.get(start..end) -} - -fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { - let layout = toyos_elf::Layout::parse(kernel_elf_bytes, arch::ELF_MACHINE) - .unwrap_or_else(|e| panic!("kernel.elf: {e}")); - - // Section headers are optional to `toyos-elf`, which loads programs whose - // sections carry only symbol names. Here they carry the relocations that - // make the image runnable, so a file with no readable table is refused - // rather than started unrelocated. - let sections = layout - .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"); - +/// Lay out the kernel the signed section describes (`toyos_kimage`): its +/// bytes at their own addresses in one allocation with its stack after it, +/// and every `RELATIVE` relocation written with that allocation's address. +/// The build parsed its ELF; nothing here does. +fn load_kernel(section: &[u8]) -> LoadedKernel { + let kernel = toyos_kimage::Kernel::parse(section).unwrap_or_else(|why| panic!("{why}")); + assert_eq!(kernel.machine, arch::E_MACHINE, "kernel image: another machine's"); let stack_size: usize = 8 * 1024 * 1024; // 8MB - println!("Kernel stack size: {}", stack_size); - // 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 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; - + let placed = toyos_kimage::StackedImage::place(kernel.memory, stack_size as u64) + .expect("kernel image: image plus stack does not fit an allocation"); + let mem_size = usize::try_from(placed.size).expect("kernel image: image plus stack does not fit an allocation"); println!("Kernel memory size: {}", mem_size); - let mut process_mem = alloc_kernel_memory(mem_size); println!("Kernel memory located at: {:?}", process_mem.as_ptr()); - - for segment in layout.segments() { - println!("Loading segment: {:?}", segment); - 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 = 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); - } + // In bounds: `parse` holds the bytes inside `memory`, below `mem_size`. + process_mem[..kernel.image.len()].copy_from_slice(kernel.image); // The boot map's read-only text (`toyos_bootmap::Access`): the image's one - // executable segment, which no writable byte shares a page with. - let page = PAGE_SIZE as u64; - let mut executable = layout.segments().iter().filter(|segment| segment.flags().executable()); - let (Some(text), None) = (executable.next(), executable.next()) else { - panic!("kernel.elf: the boot map maps one executable segment, and this image has another number of them"); - }; - assert!(!text.writable(), "kernel.elf: its executable segment is writable"); - if let Some((a, b)) = layout.overlapping_load_pages(page) { - panic!("kernel.elf: PT_LOAD segments {a} and {b} share a page, which cannot have both their protections"); - } - let (text_start, text_end) = text.page_range(page); - let text = ((extent.min() + text_start) as usize, (text_end - text_start) as usize); - - 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), - tls: None, - }; + // executable range, which no writable byte shares a page with. + let text = kernel.text().unwrap_or_else(|why| panic!("{why}")); + let text = (text.lo as usize, (text.hi - text.lo) as usize); + + // `parse` put every `offset + 8` and every addend inside the image, so + // each store lands in `process_mem` and stores an address inside it. + let base = PHYS_OFFSET + process_mem.as_ptr() as u64; 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 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; - } + for (offset, addend) in kernel.relocations() { + let at = offset as usize; + process_mem[at..at + 8].copy_from_slice(&(base + addend).to_le_bytes()); + reloc_count += 1; } println!("Applied {} relocations", reloc_count); LoadedKernel { memory: process_mem, - entry_offset: at(layout.entry()), + entry_offset: kernel.entry as usize, stack_offset: placed.stack as usize, stack_size, text, @@ -548,7 +464,7 @@ struct Handed { } #[allow(clippy::too_many_arguments)] -fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], handed: Handed, slot_volume_guid: [u8; 16], microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! { +fn start_kernel(kernel: LoadedKernel, kernel_section: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], handed: Handed, slot_volume_guid: [u8; 16], microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! { // Said before it is refused, for `report_reach`'s reason. match arch::cpu_as_entered() { Ok(None) => {} @@ -657,8 +573,8 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v kernel_stack_addr: kernel.stack_offset as u64, kernel_stack_size: kernel.stack_size as u64, rsdp_addr, - kernel_elf_addr: kernel_elf_bytes.as_ptr() as u64, - kernel_elf_size: kernel_elf_bytes.len() as u64, + kernel_image_addr: kernel_section.as_ptr() as u64, + kernel_image_size: kernel_section.len() as u64, gop_framebuffer, gop_framebuffer_size, gop_width, @@ -758,8 +674,8 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v mem::forget(memory_map); #[expect(clippy::disallowed_methods, reason = "the kernel runs from these pages")] mem::forget(kernel.memory); - #[expect(clippy::disallowed_methods, reason = "`kernel_args.kernel_elf_addr` hands it to the kernel")] - mem::forget(kernel_elf_bytes); + #[expect(clippy::disallowed_methods, reason = "`kernel_args.kernel_image_addr` hands it to the kernel")] + mem::forget(kernel_section); #[expect(clippy::disallowed_methods, reason = "`kernel_args.cmdline_addr` hands it to the kernel")] mem::forget(cmdline); @@ -1012,8 +928,8 @@ fn main(handle: Handle, system_table: SystemTable) -> Status { .unwrap_or_else(|e| panic!("slot {}'s cmdline is not UTF-8: {e}", chosen.which.letter())); println!("Boot parameter: {params:?}"); - println!("Loading kernel elf..."); - let loaded_kernel = load_kernel_elf(&kernel_bytes); + println!("Loading kernel image..."); + let loaded_kernel = load_kernel(&kernel_bytes); let microcode = microcode::find(system_table.boot_services(), &chosen.root.pages); diff --git a/rust b/rust index 7b390729d73..a189b3a6b33 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit 7b390729d73b5e997afb18cb824fe9a2239cb285 +Subproject commit a189b3a6b3346db3c162fc6fd382f9b36b5f3ab1 diff --git a/sdk/abi/src/boot.rs b/sdk/abi/src/boot.rs index c265d882aec..5ffffc85c25 100644 --- a/sdk/abi/src/boot.rs +++ b/sdk/abi/src/boot.rs @@ -9,8 +9,8 @@ pub struct KernelArgs { pub kernel_stack_addr: u64, pub kernel_stack_size: u64, pub rsdp_addr: u64, - pub kernel_elf_addr: u64, - pub kernel_elf_size: u64, + pub kernel_image_addr: u64, + pub kernel_image_size: u64, pub gop_framebuffer: u64, pub gop_framebuffer_size: u64, pub gop_width: u32, @@ -357,8 +357,8 @@ mod tests { kernel_stack_addr: 0, kernel_stack_size: 0, rsdp_addr: 0, - kernel_elf_addr: 0, - kernel_elf_size: 0, + kernel_image_addr: 0, + kernel_image_size: 0, gop_framebuffer: 0, gop_framebuffer_size: 0, gop_width: 0, diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 5c52726c18c..a9d829fba58 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -401,9 +401,9 @@ 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, 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. + /// The path the caller read the program from, which names it in the + /// spawn's record. **Never `argv[0]`**, which is the child's first + /// argument and the name it goes by. Nothing opens it. pub path_ptr: u64, pub path_len: u64, pub argv_ptr: u64, @@ -416,17 +416,22 @@ crate::user_safe! { /// The label blob every [`EndowEntry`]'s `label_off`/`label_len` indexes. pub labels_ptr: u64, pub labels_len: u64, - /// The program's bytes, in a memory object the caller holds — its file - /// server's for the file, or one the caller read the program into: - /// `image` is a handle to it carrying `MAP`, and `image_len` how many of - /// its first bytes the program is — or 0, for a program the kernel opens - /// at `path` itself. `PermissionDenied` for a handle without `MAP`; - /// `InvalidArgument` for a length the object does not hold, or a device's - /// window. 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. - pub image: u64, - pub image_len: u64, + /// The child's memory, `[SpawnMap]`, as the caller laid out its program: + /// 1 to [`MAX_SPAWN_MAPS`] ranges, none meeting another or the clock + /// page, each checked as an ordinary mapping (`SpawnMap`). Everything + /// else of the space is empty until the child maps it, but its stack. + pub maps_ptr: u64, + pub maps_count: u64, + /// Where the child's main thread starts: inside a [`VmProt::ReadExec`] + /// range of `maps`. + pub entry: u64, + /// Where the program's address 0 lands, which a crash record names a + /// frame's offset from. A label, which the kernel checks nothing of. + pub image_base: u64, + /// The program's build-id, 0 to [`MAX_BUILD_ID`] bytes, which a crash + /// record names the program by: 0 for none. + pub build_id_ptr: u64, + pub build_id_len: u64, /// The process the child is placed under, whose end takes it down: a /// handle carrying [`Rights::WRITE`] to it — a copy of that process's /// [`SELF_LABEL`] — or [`HANDLE_INVALID`] for the caller itself. @@ -447,6 +452,40 @@ crate::user_safe! { } } +crate::user_safe! { + /// One range of a spawned child's memory (`SpawnArgs::maps_ptr`). Each is + /// refused `InvalidArgument` unless `addr` and `len` are whole pages of the + /// user half, `prot` is a [`VmProt`], `copied <= len`, and `object` holds + /// `[offset, offset + copied)` of ordinary memory from a page boundary; + /// `PermissionDenied` for an `object` handle without `MAP`. No page is both + /// writable and executable, since no `VmProt` is. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct SpawnMap { + pub addr: u64, + pub len: u64, + /// A memory object whose bytes the range's pages start as, each copied at + /// its first touch and private to the child from then on; or + /// [`HANDLE_INVALID`] for a range of zeros, which `VmProt::None` leaves + /// reserved. The object stays the caller's. + pub object: u64, + pub offset: u64, + /// How many of the object's bytes from `offset` the range copies; zeros + /// past them. + pub copied: u64, + /// A [`VmProt`]. + pub prot: u64, + } +} + +const _: () = assert!(core::mem::size_of::() == 48); + +/// The most ranges a spawn lays out: a program's `PT_LOAD`s, at most +/// sixteen, and the holes between them. +pub const MAX_SPAWN_MAPS: usize = 64; + +/// The most bytes of a build-id a spawn carries. +pub const MAX_BUILD_ID: usize = 32; + /// One `(label, handle)` pair of a process's endowment table. /// /// `label_off`/`label_len` index the label blob that travels beside the diff --git a/sdk/libc/Cargo.lock b/sdk/libc/Cargo.lock index 4149f9284f6..c721dd62bdd 100644 --- a/sdk/libc/Cargo.lock +++ b/sdk/libc/Cargo.lock @@ -30,17 +30,12 @@ name = "toyos" version = "0.18.0" dependencies = [ "toyos-abi", - "toyos-elf", ] [[package]] name = "toyos-abi" version = "0.16.0" -[[package]] -name = "toyos-elf" -version = "0.1.0" - [[package]] name = "toyos-libc" version = "0.1.0" @@ -48,7 +43,6 @@ dependencies = [ "dlmalloc", "toyos", "toyos-abi", - "toyos-elf", "toyos-osrelease", ] diff --git a/sdk/libc/Cargo.toml b/sdk/libc/Cargo.toml index 067604a345a..d78a63221e7 100644 --- a/sdk/libc/Cargo.toml +++ b/sdk/libc/Cargo.toml @@ -15,7 +15,6 @@ fd-owner = ["toyos/fd-table"] [dependencies] toyos-abi = { path = "../abi" } toyos = { path = "../toyos" } -toyos-elf = { path = "../../lib/elf" } toyos-osrelease = { path = "../osrelease" } dlmalloc = "0.2.7" diff --git a/sdk/libc/arch/Cargo.toml b/sdk/libc/arch/Cargo.toml index e5d6717bbb5..6c9e060810a 100644 --- a/sdk/libc/arch/Cargo.toml +++ b/sdk/libc/arch/Cargo.toml @@ -17,4 +17,3 @@ std-runtime = [] [dev-dependencies] toyos-abi = { path = "../../abi" } toyos = { path = "../../toyos" } -toyos-elf = { path = "../../../lib/elf" } diff --git a/sdk/libc/src/elfsym.rs b/sdk/libc/src/elfsym.rs index a41ee60a54b..4094f808e02 100644 --- a/sdk/libc/src/elfsym.rs +++ b/sdk/libc/src/elfsym.rs @@ -9,8 +9,8 @@ use core::ffi::CStr; -use toyos_elf::header::{ProgramHeader, PROGRAM_HEADER_SIZE, PT_DYNAMIC, PT_LOAD}; -use toyos_elf::{sym, Dynamic, SymTab}; +use toyos::elf::header::{ProgramHeader, PROGRAM_HEADER_SIZE, PT_DYNAMIC, PT_LOAD}; +use toyos::elf::{sym, Dynamic, SymTab}; /// A loaded image, as `dl_iterate_phdr` describes it. #[derive(Clone, Copy)] diff --git a/sdk/toyos/Cargo.toml b/sdk/toyos/Cargo.toml index 681280654c4..4dbfda621c4 100644 --- a/sdk/toyos/Cargo.toml +++ b/sdk/toyos/Cargo.toml @@ -7,7 +7,7 @@ description = "The ToyOS userland SDK: typed handles, IPC, ports, namespaces, su repository = "https://github.com/ToyOSOrg/ToyOS" [features] -rustc-dep-of-std = ["core", "alloc", "toyos-abi/rustc-dep-of-std", "toyos-elf/rustc-dep-of-std", "fd-table"] +rustc-dep-of-std = ["core", "alloc", "toyos-abi/rustc-dep-of-std", "fd-table"] # This copy owns the process's descriptor table (`src/fd.rs`): on in std's # build and in libc's C-only build, and in no other. fd-table = [] @@ -104,8 +104,9 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("toyos") core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } alloc = { version = "1.0.0", optional = true, package = "rustc-std-workspace-alloc" } toyos-abi = { path = "../abi", default-features = false } -# The loader's ELF decoding (`dl`). -toyos-elf = { path = "../../lib/elf" } [dev-dependencies] loom = "0.7" +# The ELF module's oracle: unaligned, since an include_bytes! fixture is aligned +# to 1 and object reads a header in place. +object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] } diff --git a/sdk/toyos/src/dl/image/mod.rs b/sdk/toyos/src/dl/image/mod.rs index 2bdcce71de0..97e615f9da1 100644 --- a/sdk/toyos/src/dl/image/mod.rs +++ b/sdk/toyos/src/dl/image/mod.rs @@ -14,9 +14,9 @@ use alloc::string::String; use alloc::vec::Vec; use core::marker::PhantomData; -use toyos_elf::dynamic::ENTRY_SIZE as DYN_SIZE; -use toyos_elf::sym::{STB_WEAK, STT_TLS}; -use toyos_elf::{ +use crate::elf::dynamic::ENTRY_SIZE as DYN_SIZE; +use crate::elf::sym::{STB_WEAK, STT_TLS}; +use crate::elf::{ rela, Dynamic, GnuHash, InitArray, Layout, Machine, Op, RelaTable, RelocError, SegmentFlags, Sym, SymTab, TlsRef, TlsSegment, }; @@ -27,10 +27,7 @@ use super::Refused; pub(crate) const PAGE: u64 = 4096; /// The machine this program runs on, whose images it loads. -#[cfg(target_arch = "x86_64")] -pub(crate) const MACHINE: Machine = Machine::X86_64; -#[cfg(target_arch = "aarch64")] -pub(crate) const MACHINE: Machine = Machine::Aarch64; +const MACHINE: Machine = Machine::RUNNING; const DT_NEEDED: u64 = 1; @@ -168,7 +165,7 @@ impl<'p, 'm> Mapped<'p, 'm> { let syms = match parsed.symtab { Some(at) => { let whole = self.view_to_end(at).ok_or(OUTSIDE)?; - &whole[..whole.len().min(count.saturating_mul(toyos_elf::sym::ENTRY_SIZE))] + &whole[..whole.len().min(count.saturating_mul(crate::elf::sym::ENTRY_SIZE))] } None => &[][..], }; @@ -176,7 +173,7 @@ impl<'p, 'm> Mapped<'p, 'm> { Some(at) => self.view(at, parsed.strsz.unwrap_or(0)).ok_or(OUTSIDE)?, None => &[][..], }; - let table = |t: Option| t.map(|t| self.view(t.vaddr, t.size).ok_or(OUTSIDE)).transpose(); + let table = |t: Option| t.map(|t| self.view(t.vaddr, t.size).ok_or(OUTSIDE)).transpose(); let (rela, jmprel) = (table(parsed.rela)?, table(parsed.jmprel)?); let symbols = SymTab::new(syms, strs); let extent = self.plan.layout.extent(); @@ -246,7 +243,7 @@ impl<'a> Tables<'a> { .map(|entry| (u64::from_le_bytes(entry[..8].try_into().unwrap()), u64::from_le_bytes(entry[8..].try_into().unwrap()))) .take_while(|&(tag, _)| tag != 0) .filter(|&(tag, _)| tag == DT_NEEDED) - .map(move |(_, at)| toyos_elf::cstr(strs, at)) + .map(move |(_, at)| crate::elf::cstr(strs, at)) } /// The symbol this image exports as `name`, through its `.gnu.hash` @@ -317,12 +314,12 @@ pub(crate) enum Pass<'b> { /// names and only an executable defines. pub(crate) fn relocate(mapped: &Mapped<'_, '_>, tables: &Tables<'_>, id: usize, scope: &dyn Scope, pass: Pass<'_>) -> Result<(), Refused> { let base = mapped.base(); - let symbol = |i: toyos_elf::SymIndex| -> (Sym, &str) { + let symbol = |i: crate::elf::SymIndex| -> (Sym, &str) { let sym = tables.symbols.get(i.get()).expect("dl: a parse bounded the index by the table"); (sym, sym.name_in(tables.symbols.strings())) }; let undefined = |name: &str| Refused::Undefined(String::from(name)); - let address = |i: toyos_elf::SymIndex| -> Result { + let address = |i: crate::elf::SymIndex| -> Result { let (sym, name) = symbol(i); let own = sym.address(tables.rules.extent).map(|at| base + at.get()); match scope.address(name).or(own) { diff --git a/sdk/toyos/src/dl/image/tests.rs b/sdk/toyos/src/dl/image/tests.rs index b42f7bd8e8f..1bdaed908f3 100644 --- a/sdk/toyos/src/dl/image/tests.rs +++ b/sdk/toyos/src/dl/image/tests.rs @@ -11,7 +11,7 @@ use alloc::vec::Vec; use super::{plan, relocate, Mapped, Pass, Scope}; use crate::dl::Refused; -use toyos_elf::RelocError; +use crate::elf::RelocError; const SIZE: usize = 0x2000; const PHDRS: usize = 0x40; diff --git a/sdk/toyos/src/dl/mod.rs b/sdk/toyos/src/dl/mod.rs index 3e9352572bc..21a87b892ac 100644 --- a/sdk/toyos/src/dl/mod.rs +++ b/sdk/toyos/src/dl/mod.rs @@ -28,7 +28,7 @@ use core::ffi::CStr; use core::fmt; use toyos_abi::syscall::{self, SyscallError, VmAt, VmProt}; -use toyos_elf::RelocError; +use crate::elf::RelocError; use crate::files; use crate::process::ImageRefused; diff --git a/lib/elf/src/dynamic.rs b/sdk/toyos/src/elf/dynamic.rs similarity index 98% rename from lib/elf/src/dynamic.rs rename to sdk/toyos/src/elf/dynamic.rs index 259f378ad4c..a4e60b4b994 100644 --- a/lib/elf/src/dynamic.rs +++ b/sdk/toyos/src/elf/dynamic.rs @@ -5,9 +5,9 @@ //! 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::gnu_hash::GnuHash; -use crate::layout::{Extent, ImageRange}; -use crate::{read, sym, Error}; +use crate::elf::gnu_hash::GnuHash; +use crate::elf::layout::{Extent, ImageRange}; +use crate::elf::{read, sym, Error}; /// A table named by a (location, size) pair of tags. /// diff --git a/lib/elf/src/gnu_hash.rs b/sdk/toyos/src/elf/gnu_hash.rs similarity index 98% rename from lib/elf/src/gnu_hash.rs rename to sdk/toyos/src/elf/gnu_hash.rs index abaea232e61..03db65b949f 100644 --- a/lib/elf/src/gnu_hash.rs +++ b/sdk/toyos/src/elf/gnu_hash.rs @@ -7,8 +7,8 @@ //! off the end answers `None` — a symbol count derived from a walk that ran off //! the end is not a count. -use crate::read; -use crate::sym::SymTab; +use crate::elf::read; +use crate::elf::sym::SymTab; /// A `.gnu.hash` table whose header is readable and internally usable. /// diff --git a/lib/elf/src/header.rs b/sdk/toyos/src/elf/header.rs similarity index 94% rename from lib/elf/src/header.rs rename to sdk/toyos/src/elf/header.rs index ca92648eaeb..f660e69d77a 100644 --- a/lib/elf/src/header.rs +++ b/sdk/toyos/src/elf/header.rs @@ -5,8 +5,8 @@ //! thing and the loader does another, which is the class of defect the whole //! crate exists to remove. -use crate::read; -use crate::Error; +use crate::elf::read; +use crate::elf::Error; /// Bytes in `e_ident`. pub const EI_NIDENT: usize = 16; @@ -26,7 +26,7 @@ const EM_AARCH64: u16 = 183; /// The instruction set an image is built for. /// -/// Each machine names its own relocation numbers ([`crate::rela`]); nothing +/// Each machine names its own relocation numbers ([`crate::elf::rela`]); nothing /// else in this crate is per-architecture. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Machine { @@ -35,6 +35,12 @@ pub enum Machine { } impl Machine { + /// The machine this code runs on, whose images it loads and spawns. + #[cfg(target_arch = "x86_64")] + pub const RUNNING: Machine = Machine::X86_64; + #[cfg(target_arch = "aarch64")] + pub const RUNNING: Machine = Machine::Aarch64; + /// The machine an `e_machine` names, or `None` for one ToyOS does not run. pub const fn from_raw(e_machine: u16) -> Option { match e_machine { diff --git a/lib/elf/src/layout.rs b/sdk/toyos/src/elf/layout.rs similarity index 99% rename from lib/elf/src/layout.rs rename to sdk/toyos/src/elf/layout.rs index b925da7c1db..59097ec7007 100644 --- a/lib/elf/src/layout.rs +++ b/sdk/toyos/src/elf/layout.rs @@ -9,11 +9,11 @@ //! 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::{ +use crate::elf::header::{ FileHeader, Machine, ProgramHeader, PROGRAM_HEADER_SIZE, PT_DYNAMIC, PT_GNU_EH_FRAME, PT_GNU_RELRO, PT_LOAD, PT_TLS, SECTION_HEADER_SIZE, }; -use crate::{Error, MAX_LOAD_SEGMENTS, MAX_TLS_ALIGN}; +use crate::elf::{Error, MAX_LOAD_SEGMENTS, MAX_TLS_ALIGN}; /// An image's loadable extent, `[min, max]` with `min <= max`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/lib/elf/src/lib.rs b/sdk/toyos/src/elf/mod.rs similarity index 92% rename from lib/elf/src/lib.rs rename to sdk/toyos/src/elf/mod.rs index 6fb23d11169..4e48b201953 100644 --- a/lib/elf/src/lib.rs +++ b/sdk/toyos/src/elf/mod.rs @@ -2,25 +2,27 @@ //! this file want mapped, where does everything it names live inside it, and //! which of the numbers in it may a loader act on?** //! -//! An ELF is untrusted input — the kernel maps whatever image a spawn names, -//! and libc's loader whatever library a process names — so the kernel's -//! fail-fast rule does not apply to anything decoded here. Every length, count, offset +//! An ELF is untrusted input — a parent lays out whatever program it spawns, +//! and the loader whatever library a process names — so nothing decoded here +//! is trusted. Every length, count, offset //! and vaddr is checked before it is used, nothing is indexed without a bound, //! and no path panics. What cannot be satisfied is refused by name through //! [`Error`]; nothing is silently truncated to fit. //! -//! `no_std`, no allocation, no `unsafe`. The bounded pieces are bounded by -//! constants in this crate ([`MAX_LOAD_SEGMENTS`], [`MAX_TLS_ALIGN`]); the +//! No allocation, no `unsafe`. The bounded pieces are bounded by +//! constants in this module ([`MAX_LOAD_SEGMENTS`], [`MAX_TLS_ALIGN`]); the //! unbounded ones — a relocation table, a symbol table, a section header table //! — are *views* over bytes the caller already holds, so nothing here is sized //! by a number the file chose. //! -//! # What this crate is not +//! # What this module is not //! //! It maps nothing, allocates nothing and writes nothing. Turning a //! [`Layout`] into an address space, applying a relocation and copying a TLS //! template are its callers', and the split is the point: everything in here -//! is a pure function of bytes and is tested on the host in milliseconds. +//! is a pure function of bytes and is tested on the host in milliseconds +//! (`tests/elf_*.rs`). The build reads the kernel's and the supervisor's images +//! with it too, so ToyOS has one ELF parser. //! //! # Scope //! @@ -30,7 +32,6 @@ //! ([`Layout::parse`]), so an image for the other one is refused as //! [`Error::WrongMachine`] before anything of it is mapped. -#![no_std] #![forbid(unsafe_code)] pub mod dynamic; @@ -38,6 +39,7 @@ pub mod gnu_hash; pub mod header; pub mod layout; pub mod note; +pub mod program; pub mod rela; pub mod section; pub mod sym; @@ -196,6 +198,14 @@ impl core::fmt::Display for Error { /// value — came out of a file. Past the end of the table there is no name, and /// no name is the right answer: a name is only ever compared against another /// name, so an unreadable one matches nothing. +/// `[offset, offset + len)` of `data`, or `None` when that is not wholly inside +/// it. Both numbers came out of the file, so the addition is checked. +pub fn file_range(data: &[u8], offset: u64, len: u64) -> Option<&[u8]> { + let start = usize::try_from(offset).ok()?; + let end = usize::try_from(offset.checked_add(len)?).ok()?; + data.get(start..end) +} + pub fn cstr(strings: &[u8], offset: u64) -> &str { match usize::try_from(offset) { Ok(off) => read::cstr(strings, off), diff --git a/lib/elf/src/note.rs b/sdk/toyos/src/elf/note.rs similarity index 83% rename from lib/elf/src/note.rs rename to sdk/toyos/src/elf/note.rs index 09fd15f2284..601a694285d 100644 --- a/lib/elf/src/note.rs +++ b/sdk/toyos/src/elf/note.rs @@ -7,16 +7,22 @@ //! 4, or 8 for a segment that declares it. Nothing here refuses a file: a note //! that cannot be read names no build-id. -use crate::header::{FileHeader, ProgramHeader, PROGRAM_HEADER_SIZE, PT_NOTE}; -use crate::read; +use crate::elf::header::{FileHeader, ProgramHeader, PROGRAM_HEADER_SIZE, PT_NOTE}; +use crate::elf::read; /// `n_type` of a GNU build-id note. pub const NT_GNU_BUILD_ID: u32 = 3; -/// The longest build-id this crate answers. lld's is 20 +/// The longest build-id this module answers. lld's is 20 /// bytes, a `uuid` or `md5` one 16; past this a descriptor is no build-id any /// linker writes. -pub const MAX_BUILD_ID: usize = 32; +pub const MAX_BUILD_ID: usize = toyos_abi::syscall::MAX_BUILD_ID; + +/// The build-id of the whole image `file`: the first `PT_NOTE` inside it that +/// carries one. +pub fn build_id_in(file: &[u8]) -> Option<&[u8]> { + segments(file).find_map(|segment| build_id(crate::elf::file_range(file, segment.offset, segment.filesz)?, segment.align)) +} /// One `PT_NOTE`: where its bytes are in the file, and the alignment its notes /// are padded to. diff --git a/sdk/toyos/src/elf/program.rs b/sdk/toyos/src/elf/program.rs new file mode 100644 index 00000000000..a0370763429 --- /dev/null +++ b/sdk/toyos/src/elf/program.rs @@ -0,0 +1,116 @@ +//! A program laid out for a spawn: the ranges of the child's memory its +//! `PT_LOAD`s are, as `toyos_abi::syscall::SpawnMap`s, its entry and its +//! build-id. The kernel parses no program; it checks each range as an +//! ordinary mapping and builds the child from them. The build lays the +//! supervisor out with the same function, ahead of the boot. + +use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID, MAX_SPAWN_MAPS}; +use toyos_abi::HANDLE_INVALID; + +use crate::elf::{note, Layout, Machine}; + +/// The page the ranges are whole ones of. +const PAGE: u64 = 4096; + +/// Where a spawned program's lowest page goes in its child: above the clock +/// page (`toyos_abi::clock::CLOCK_PAGE`), in a space nothing else holds yet. +pub const BASE: u64 = 0x2_0000_0000; + +/// What a program asks of a spawn: its ranges, its entry, where its address 0 +/// lands and its build-id. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Program { + maps: [SpawnMap; MAX_SPAWN_MAPS], + count: usize, + pub entry: u64, + pub image_base: u64, + build_id: [u8; MAX_BUILD_ID], + build_id_len: usize, +} + +impl Program { + /// The ranges, each a `PT_LOAD` copied from the object or a hole between + /// two, reserved, in address order. + pub fn maps(&self) -> &[SpawnMap] { + &self.maps[..self.count] + } + + pub fn build_id(&self) -> &[u8] { + &self.build_id[..self.build_id_len] + } +} + +/// The program `file`, an image for `machine`, laid out with its lowest page +/// at `at`, each `PT_LOAD` copied from `object`, the memory object holding +/// `file`. Refused by name where the kernel would refuse a range, or the +/// program could not start: no `PT_LOAD` maps its program header table, which +/// it reads to relocate itself. +/// +/// A segment both writable and executable is mapped `ReadExec`: taking the +/// execution away would let a hostile file run as data instead. +pub fn lay_out(file: &[u8], machine: Machine, object: u64, at: u64) -> Result { + let layout = Layout::parse(file, machine).map_err(|e| e.as_str())?; + if layout.program_headers().is_none() { + return Err("ELF: no PT_LOAD maps the program header table"); + } + if layout.overlapping_load_pages(PAGE).is_some() { + return Err("ELF: two PT_LOAD segments share a page"); + } + if !at.is_multiple_of(PAGE) { + return Err("a program's base is a page boundary"); + } + let image_base = at.checked_sub(layout.extent().min()).ok_or("ELF: an image below its own base")?; + let mut program = Program { + maps: [SpawnMap::default(); MAX_SPAWN_MAPS], + count: 0, + entry: at.checked_add(layout.entry().get()).ok_or("ELF: an entry past the address space")?, + image_base, + build_id: [0; MAX_BUILD_ID], + build_id_len: 0, + }; + let mut segments: [(u64, u64, u64, u64, VmProt); crate::elf::MAX_LOAD_SEGMENTS] = [(0, 0, 0, 0, VmProt::None); crate::elf::MAX_LOAD_SEGMENTS]; + let mut n = 0; + for seg in layout.segments() { + let (lo, hi) = seg.page_range(PAGE); + if hi == lo { + continue; + } + // The file's bytes from the segment's first page: the bytes of that + // page before the segment are the file's too, as a linker lays them. + let offset = seg.file_offset() & !(PAGE - 1); + let bytes = seg.filesz().checked_add(seg.file_offset() % PAGE).ok_or("ELF: a segment past the file")?; + let copied = bytes.min((file.len() as u64).saturating_sub(offset)); + let prot = if seg.flags().executable() { + VmProt::ReadExec + } else if seg.flags().writable() { + VmProt::ReadWrite + } else { + VmProt::Read + }; + segments[n] = (lo, hi, offset, copied, prot); + n += 1; + } + let segments = &mut segments[..n]; + segments.sort_unstable_by_key(|&(lo, ..)| lo); + let mut push = |map: SpawnMap| -> Result<(), &'static str> { + *program.maps.get_mut(program.count).ok_or("ELF: more ranges than a spawn carries")? = map; + program.count += 1; + Ok(()) + }; + let address = |offset: u64| at.checked_add(offset).ok_or("ELF: an image past the address space"); + let mut covered = segments.first().map_or(0, |&(lo, ..)| lo); + for &(lo, hi, offset, copied, prot) in segments.iter() { + if lo > covered { + let hole = SpawnMap { addr: address(covered)?, len: lo - covered, object: u64::from(HANDLE_INVALID.0), ..SpawnMap::default() }; + push(hole)?; + } + let len = hi.checked_sub(lo).filter(|_| hi != u64::MAX).ok_or("ELF: a segment past the address space")?; + push(SpawnMap { addr: address(lo)?, len, object, offset, copied, prot: prot as u64 })?; + covered = hi; + } + if let Some(id) = note::build_id_in(file) { + program.build_id[..id.len()].copy_from_slice(id); + program.build_id_len = id.len(); + } + Ok(program) +} diff --git a/lib/elf/src/rela.rs b/sdk/toyos/src/elf/rela.rs similarity index 98% rename from lib/elf/src/rela.rs rename to sdk/toyos/src/elf/rela.rs index fdd520878cd..e2d5f68f52e 100644 --- a/lib/elf/src/rela.rs +++ b/sdk/toyos/src/elf/rela.rs @@ -12,11 +12,11 @@ //! Every type [`Op`] has no variant for, but `NONE`, is refused: a slot left //! with its link-time value is a wrong pointer, not a skipped relocation. -use crate::header::Machine; -use crate::layout::{Extent, ImageOffset, TlsSegment}; -use crate::read; -use crate::sym::{SymIndex, SymTab}; -use crate::tls::TlsOffset; +use crate::elf::header::Machine; +use crate::elf::layout::{Extent, ImageOffset, TlsSegment}; +use crate::elf::read; +use crate::elf::sym::{SymIndex, SymTab}; +use crate::elf::tls::TlsOffset; /// Bytes in one `Elf64_Rela`. pub const ENTRY_SIZE: usize = 24; @@ -219,7 +219,7 @@ impl TlsSymRef { } /// The addend `A`, resolved against the defining symbol's `S` through - /// [`crate::Sym::tls_offset`]. + /// [`crate::elf::Sym::tls_offset`]. pub const fn addend(self) -> i64 { self.addend } diff --git a/lib/elf/src/section.rs b/sdk/toyos/src/elf/section.rs similarity index 80% rename from lib/elf/src/section.rs rename to sdk/toyos/src/elf/section.rs index 7f5ebab035b..709e875d566 100644 --- a/lib/elf/src/section.rs +++ b/sdk/toyos/src/elf/section.rs @@ -7,8 +7,8 @@ use core::fmt; -use crate::header::SECTION_HEADER_SIZE; -use crate::read; +use crate::elf::header::SECTION_HEADER_SIZE; +use crate::elf::read; pub const SHT_SYMTAB: u32 = 2; pub const SHT_RELA: u32 = 4; @@ -108,6 +108,14 @@ impl<'a> SectionTable<'a> { Some((syms, strs)) } + /// `SHT_SYMTAB` and its `.strtab` in the whole image `file` this table + /// belongs to, as byte slices of it; `None` when it has none, or a + /// declared extent leaves the file. + pub fn symbol_tables(self, file: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> { + let (syms, strs) = self.symbols(SHT_SYMTAB)?; + Some((crate::elf::file_range(file, syms.offset, syms.size)?, crate::elf::file_range(file, strs.offset, strs.size)?)) + } + /// Every `SHT_RELA` section, for a loader that applies those and nothing /// else: the only way to them, so no such loader can start an image whose /// other relocations it never read. Refused when the table also names one @@ -123,3 +131,12 @@ impl<'a> SectionTable<'a> { Ok(self.iter().filter(|sh| sh.kind == SHT_RELA)) } } + +/// `.symtab` and its `.strtab` in the whole image `file`, as byte slices of +/// it, bounded against it and never copied; `None` when it has none or a +/// declared extent leaves it, which is "no symbols" to every reader. +pub fn symbol_tables(file: &[u8]) -> Option<(&[u8], &[u8])> { + let header = crate::elf::FileHeader::parse(file).ok()?; + let len = u64::from(header.shnum) * u64::from(header.shentsize); + SectionTable::new(crate::elf::file_range(file, header.shoff, len)?).symbol_tables(file) +} diff --git a/lib/elf/src/sym.rs b/sdk/toyos/src/elf/sym.rs similarity index 96% rename from lib/elf/src/sym.rs rename to sdk/toyos/src/elf/sym.rs index ef8ea9090ff..b23a3bd9383 100644 --- a/lib/elf/src/sym.rs +++ b/sdk/toyos/src/elf/sym.rs @@ -7,10 +7,10 @@ use core::ffi::CStr; -use crate::layout::{Extent, ImageOffset, TlsSegment}; -use crate::read; -use crate::tls::TlsOffset; -use crate::Error; +use crate::elf::layout::{Extent, ImageOffset, TlsSegment}; +use crate::elf::read; +use crate::elf::tls::TlsOffset; +use crate::elf::Error; /// Bytes in one `Elf64_Sym`. pub const ENTRY_SIZE: usize = 24; @@ -59,6 +59,16 @@ impl Sym { self.shndx != 0 } + /// `st_value`: an image offset, or a TLS offset for `STT_TLS`. + pub const fn value(&self) -> u64 { + self.value + } + + /// `st_size`, 0 for a symbol that bounds nothing. + pub const fn size(&self) -> u64 { + self.size + } + pub const fn bind(&self) -> u8 { self.info >> 4 } @@ -77,7 +87,7 @@ impl Sym { /// 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)) + crate::elf::cstr(strings, u64::from(self.name)) } /// Where a defined, non-TLS symbol lies in the image `extent` describes, diff --git a/lib/elf/src/tls.rs b/sdk/toyos/src/elf/tls.rs similarity index 85% rename from lib/elf/src/tls.rs rename to sdk/toyos/src/elf/tls.rs index 37cdabbe5f0..e77bd375466 100644 --- a/lib/elf/src/tls.rs +++ b/sdk/toyos/src/elf/tls.rs @@ -1,11 +1,11 @@ //! A thread-local datum's place inside its module's `PT_TLS`. -use crate::layout::TlsSegment; +use crate::elf::layout::TlsSegment; /// `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. +/// The end is inclusive for the reason [`crate::elf::Extent::offset`]'s is. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TlsOffset(u64); diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index bb11cbb4f6f..07bd7eb27f2 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -410,7 +410,7 @@ pub fn read_dir(fd: Fd, cursor: u64) -> Result<(Vec, Option), Error> { } /// The whole of the file at `path`, wherever it is served: what a spawn names -/// as its program (`SpawnArgs::image`) and a library's loader maps, since the +/// as its program (`SpawnMap::object`) and a library's loader maps, since the /// kernel opens neither. The object its server holds it in where there is one /// ([`crate::fs::IMAGE`]), and otherwise a copy read into memory of this /// process's own. diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 6d04501a7e8..ef4ba7a5783 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -21,6 +21,7 @@ pub mod census; pub mod device; pub mod devset; pub mod dl; +pub mod elf; pub mod endow; pub mod fd; pub mod files; diff --git a/sdk/toyos/src/process.rs b/sdk/toyos/src/process.rs index 0804bf557a8..7902a894a6a 100644 --- a/sdk/toyos/src/process.rs +++ b/sdk/toyos/src/process.rs @@ -101,9 +101,8 @@ impl FromHandle for Process { } } -/// A program's bytes in a memory object this process holds, which the kernel -/// pages a child from: how a program the kernel does not serve is spawned -/// (`SpawnArgs::image`). +/// A program's bytes in a memory object this process holds, which a spawn +/// lays out and the kernel pages the child from (`SpawnMap::object`). pub struct Image { object: Object, len: u64, @@ -226,14 +225,15 @@ impl Image { &bytes[..self.len as usize] } - /// What `SpawnArgs::image` and `SpawnArgs::image_len` carry. The object is - /// this process's until the spawn returns; the child keeps its own. - pub fn spawn_words(&self) -> (u64, u64) { + /// The handle a spawn's ranges name the object by (`SpawnMap::object`). + /// The object is this process's until the spawn returns; the child's pages + /// are copied out of it. + pub fn handle(&self) -> u64 { let handle = match &self.object { Object::Read(object) => object.as_handle(), Object::Immutable(image) => image.as_handle(), }; - (u64::from(handle.raw().0), self.len) + u64::from(handle.raw().0) } } diff --git a/sdk/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index f5e4430798f..7698b2d8ec3 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -197,9 +197,16 @@ impl<'a> Spawn<'a> { impl Helper<'_> { /// Spawn the child here, carrying the spawn's environment and running - /// `image` when one is given, which the kernel pages the child from. + /// `image`, which this process lays out ([`crate::elf::program`]) and the + /// kernel pages the child from. Refused [`Refused::Invalid`] without one, + /// or for one that names a layout the child could not start from. pub fn spawn(self, image: Option<&Image>) -> Result { let s = self.0; + let image = image.ok_or(Refused::Invalid("a spawn names the program it runs"))?; + let program = crate::elf::program::lay_out(image.bytes(), crate::elf::Machine::RUNNING, image.handle(), crate::elf::program::BASE) + .map_err(Refused::Invalid)?; + let maps = program.maps(); + let build_id = program.build_id(); let path = image_and_name(s.program, s.image).0; let env = crate::files::starting_in(s.env, s.cwd) .ok_or(Refused::Invalid("a working directory that is not absolute, is too long or holds a NUL"))?; @@ -255,7 +262,6 @@ impl Helper<'_> { } moved.push(copy); } - let (image, image_len) = image.map_or((0, 0), Image::spawn_words); let args = SpawnArgs { path_ptr: path.as_ptr() as u64, path_len: path.len() as u64, @@ -267,8 +273,12 @@ impl Helper<'_> { endow_count: count as u64, labels_ptr: labels.as_ptr() as u64, labels_len: used as u64, - image, - image_len, + maps_ptr: maps.as_ptr() as u64, + maps_count: maps.len() as u64, + entry: program.entry, + image_base: program.image_base, + build_id_ptr: build_id.as_ptr() as u64, + build_id_len: build_id.len() as u64, place: match s.parent { Parent::Caller => u64::from(toyos_abi::HANDLE_INVALID.0), Parent::Place(place) => u64::from(place.raw().0), diff --git a/lib/elf/tests/common/mod.rs b/sdk/toyos/tests/elf/common/mod.rs similarity index 94% rename from lib/elf/tests/common/mod.rs rename to sdk/toyos/tests/elf/common/mod.rs index e2f8288b8ef..5f39066e50b 100644 --- a/lib/elf/tests/common/mod.rs +++ b/sdk/toyos/tests/elf/common/mod.rs @@ -186,24 +186,24 @@ 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 +/// `[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 { +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) + 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 { +/// 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) + toyos::elf::Layout::parse(&bytes, toyos::elf::Machine::X86_64) .unwrap() .tls() .unwrap() diff --git a/lib/symbols/tests/fixtures/input-test.bin b/sdk/toyos/tests/elf/fixtures/input-test.bin similarity index 100% rename from lib/symbols/tests/fixtures/input-test.bin rename to sdk/toyos/tests/elf/fixtures/input-test.bin diff --git a/lib/elf/tests/fixtures/lld-headers.bin b/sdk/toyos/tests/elf/fixtures/lld-headers.bin similarity index 100% rename from lib/elf/tests/fixtures/lld-headers.bin rename to sdk/toyos/tests/elf/fixtures/lld-headers.bin diff --git a/lib/elf/tests/crafted.rs b/sdk/toyos/tests/elf_crafted.rs similarity index 98% rename from lib/elf/tests/crafted.rs rename to sdk/toyos/tests/elf_crafted.rs index c59861480b9..c6da85d2da9 100644 --- a/lib/elf/tests/crafted.rs +++ b/sdk/toyos/tests/elf_crafted.rs @@ -11,11 +11,12 @@ // tables inside them. Every item is used by one of the two, so the unused half // is dead only from here. #[allow(dead_code)] +#[path = "elf/common/mod.rs"] mod common; use common::*; -use toyos_elf::header::PROGRAM_HEADER_SIZE; -use toyos_elf::{Error, ImageRange, Layout, Machine, StackedImage, MAX_LOAD_SEGMENTS}; +use toyos::elf::header::PROGRAM_HEADER_SIZE; +use toyos::elf::{Error, ImageRange, Layout, Machine, StackedImage, MAX_LOAD_SEGMENTS}; fn refused(bytes: Vec) -> Error { match Layout::parse(&bytes, Machine::X86_64) { @@ -403,7 +404,7 @@ fn a_table_no_segment_holds_whole_is_absent() { let after = Elf::new(0x2000).entry(0x1000).ph(Phdr::load(PH_OFF as u64 + 8, 0x1000, 0x1000, 0x1000, PF_R)); assert_eq!(table_of(&accepted(after.build())), None); // And one at vaddr 0, so a loader's vaddr-0 checks pass and only the table - // refuses it: `abuse_elf_loader`'s `phdrs_unmapped`. + // refuses it (`program::lay_out`). let unmapped = accepted(Elf::new(0x2000).ph(Phdr::load(0x1000, 0, 0x1000, 0x1000, PF_R | PF_X)).build()); assert_eq!((unmapped.extent().min(), table_of(&unmapped)), (0, None)); } diff --git a/lib/elf/tests/fuzz.rs b/sdk/toyos/tests/elf_fuzz.rs similarity index 98% rename from lib/elf/tests/fuzz.rs rename to sdk/toyos/tests/elf_fuzz.rs index 422590a7288..c4b88f7dac6 100644 --- a/lib/elf/tests/fuzz.rs +++ b/sdk/toyos/tests/elf_fuzz.rs @@ -18,14 +18,15 @@ //! refuses everything cannot pass as one that accepts nothing wrong. #[allow(dead_code)] +#[path = "elf/common/mod.rs"] mod common; use common::*; -use toyos_elf::dynamic::{Dynamic, InitArray}; -use toyos_elf::rela::{self, Op, ReadTables, RelaTable, Rules, TlsRef}; -use toyos_elf::sym::{self, SymTab}; -use toyos_elf::tls::TlsOffset; -use toyos_elf::{ImageRange, Layout, Machine, TlsSegment}; +use toyos::elf::dynamic::{Dynamic, InitArray}; +use toyos::elf::rela::{self, Op, ReadTables, RelaTable, Rules, TlsRef}; +use toyos::elf::sym::{self, SymTab}; +use toyos::elf::tls::TlsOffset; +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. @@ -441,7 +442,7 @@ fn every_derived_address_lies_inside_the_image_or_the_image_is_refused() { /// answers is a `GNU` build-id note's whole descriptor out of the bytes given. #[test] fn no_note_segment_panics_the_build_id_read() { - use toyos_elf::note::{self, MAX_BUILD_ID, NT_GNU_BUILD_ID}; + use toyos::elf::note::{self, MAX_BUILD_ID, NT_GNU_BUILD_ID}; let mut rng = Rng { state: 0xA076_1D64_78BD_642F, hostile: 20 }; let mut found = 0u64; const ITERATIONS: u64 = 200_000; diff --git a/lib/elf/tests/note.rs b/sdk/toyos/tests/elf_note.rs similarity index 97% rename from lib/elf/tests/note.rs rename to sdk/toyos/tests/elf_note.rs index 7991840057e..be06be3c01d 100644 --- a/lib/elf/tests/note.rs +++ b/sdk/toyos/tests/elf_note.rs @@ -2,13 +2,14 @@ //! `object`'s note reader, which is not ToyOS code. #[allow(dead_code)] +#[path = "elf/common/mod.rs"] mod common; use common::*; use object::elf::{FileHeader64, NT_GNU_BUILD_ID as OBJECT_NT_GNU_BUILD_ID}; use object::read::elf::NoteIterator; use object::LittleEndian; -use toyos_elf::note::{self, NoteSegment, MAX_BUILD_ID, NT_GNU_BUILD_ID}; +use toyos::elf::note::{self, NoteSegment, MAX_BUILD_ID, NT_GNU_BUILD_ID}; const PT_NOTE: u32 = 4; const OTHER: u32 = 1; diff --git a/sdk/toyos/tests/elf_program.rs b/sdk/toyos/tests/elf_program.rs new file mode 100644 index 00000000000..d9e178469b1 --- /dev/null +++ b/sdk/toyos/tests/elf_program.rs @@ -0,0 +1,118 @@ +//! A program laid out for a spawn: against lld's own images, read by `object` +//! (not ToyOS code), and against the files a spawn must refuse. + +#[path = "elf/common/mod.rs"] +#[allow(dead_code)] +mod common; + +use common::*; +use object::{Object, ObjectSegment, SegmentFlags}; +use toyos::elf::program::{lay_out, BASE}; +use toyos::elf::Machine; +use toyos_abi::syscall::{SpawnMap, VmProt}; +use toyos_abi::HANDLE_INVALID; + +const OBJECT: u64 = 7; +const NONE: u64 = HANDLE_INVALID.0 as u64; + +/// Every `PT_LOAD` of lld's image is one range at its page-rounded vaddr above +/// [`BASE`], copied from the object from its file page, with the protection +/// its flags give; the holes between them are reserved; the entry is the +/// file's, moved by the base. +#[test] +fn lld_images_are_laid_out_as_object_reads_them() { + for (file, machine) in [ + (&include_bytes!("relocate/x86_64.elf")[..], Machine::X86_64), + (&include_bytes!("relocate/aarch64.elf")[..], Machine::Aarch64), + ] { + let program = lay_out(file, machine, OBJECT, BASE).expect("lld's image lays out"); + let elf = object::File::parse(file).expect("object reads lld's image"); + assert_eq!(program.entry, BASE + elf.entry()); + assert_eq!(program.image_base, BASE); + let loads: Vec<_> = program.maps().iter().filter(|m| m.object == OBJECT).copied().collect(); + let segments: Vec<_> = elf.segments().collect(); + assert_eq!(loads.len(), segments.len()); + for (map, segment) in loads.iter().zip(&segments) { + let SegmentFlags::Elf { p_flags } = segment.flags() else { unreachable!("an ELF's") }; + let (offset, filesz) = segment.file_range(); + let lo = segment.address() & !0xfff; + let hi = (segment.address() + segment.size()).next_multiple_of(0x1000); + let prot = match p_flags & 3 { + 1 | 3 => VmProt::ReadExec, + 2 => VmProt::ReadWrite, + _ => VmProt::Read, + }; + let want = SpawnMap { addr: BASE + lo, len: hi - lo, object: OBJECT, offset: offset & !0xfff, copied: filesz + offset % 0x1000, prot: prot as u64 }; + assert_eq!(*map, want, "{machine:?}"); + } + // No range meets another, and every hole between two is reserved. + for pair in program.maps().windows(2) { + assert_eq!(pair[0].addr + pair[0].len, pair[1].addr, "{machine:?}: a gap no range reserves"); + } + for hole in program.maps().iter().filter(|m| m.object == NONE) { + assert_eq!((hole.prot, hole.copied), (VmProt::None as u64, 0)); + } + } +} + +/// A spawn's base is where the lowest page goes, and the image base is that +/// less the image's own lowest address. +#[test] +fn the_image_moves_with_its_base() { + let file = Elf::new(0x3000) + .ph(Phdr::load(0, 0x10_0000, 0x1000, 0x1000, PF_R | PF_X)) + .ph(Phdr::load(0x1000, 0x10_1000, 0x800, 0x2000, PF_R | PF_W)) + .entry(0x10_0040) + .build(); + let program = lay_out(&file, Machine::X86_64, OBJECT, BASE).unwrap(); + assert_eq!((program.image_base, program.entry), (BASE - 0x10_0000, BASE + 0x40)); + assert_eq!( + program.maps(), + [ + SpawnMap { addr: BASE, len: 0x1000, object: OBJECT, offset: 0, copied: 0x1000, prot: VmProt::ReadExec as u64 }, + SpawnMap { addr: BASE + 0x1000, len: 0x2000, object: OBJECT, offset: 0x1000, copied: 0x800, prot: VmProt::ReadWrite as u64 }, + ] + ); +} + +/// A segment both writable and executable is mapped `ReadExec`: no range a +/// spawn asks for is both. +#[test] +fn a_writable_executable_segment_runs_but_is_not_written() { + let file = Elf::new(0x2000).ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_W | PF_X)).build(); + assert_eq!(lay_out(&file, Machine::X86_64, OBJECT, BASE).unwrap().maps()[0].prot, VmProt::ReadExec as u64); +} + +/// A segment's copy stops at the file's end: what the file does not hold is +/// zeros, never bytes past the object. +#[test] +fn a_copy_stops_at_the_files_end() { + let file = Elf::new(0x1800).ph(Phdr::load(0, 0, 0x1800, 0x1800, PF_R | PF_X)).build(); + let map = lay_out(&file[..0x1200], Machine::X86_64, OBJECT, BASE); + // The header page alone is held; the parse needs only it. + assert_eq!(map.map(|p| p.maps()[0].copied), Ok(0x1200)); +} + +#[test] +fn what_a_spawn_cannot_start_is_refused_by_name() { + // No `PT_LOAD` maps the program header table, which the program reads. + let unmapped = Elf::new(0x2000).ph(Phdr::load(0x1000, 0x1000, 0x1000, 0x1000, PF_R | PF_X)).entry(0x1000).build(); + assert_eq!(lay_out(&unmapped, Machine::X86_64, OBJECT, BASE), Err("ELF: no PT_LOAD maps the program header table")); + // Two segments on one page could not each have their protection. + let shared = Elf::new(0x2000) + .ph(Phdr::load(0, 0, 0x800, 0x800, PF_R | PF_X)) + .ph(Phdr::load(0x800, 0x800, 0x800, 0x800, PF_R | PF_W)) + .build(); + assert_eq!(lay_out(&shared, Machine::X86_64, OBJECT, BASE), Err("ELF: two PT_LOAD segments share a page")); + // Another machine's image. + assert!(lay_out(&Elf::honest(0x1000).build(), Machine::Aarch64, OBJECT, BASE).is_err()); + // A base that is no page boundary. + assert!(lay_out(&Elf::honest(0x1000).build(), Machine::X86_64, OBJECT, BASE + 8).is_err()); +} + +/// A build-id note is carried, as the crash record names the program by it. +#[test] +fn the_build_id_is_carried() { + let file = include_bytes!("elf/fixtures/input-test.bin"); + assert_eq!(lay_out(file, Machine::X86_64, OBJECT, BASE).unwrap().build_id(), toyos::elf::note::build_id_in(file).unwrap_or(&[])); +} diff --git a/lib/elf/tests/real.rs b/sdk/toyos/tests/elf_real.rs similarity index 91% rename from lib/elf/tests/real.rs rename to sdk/toyos/tests/elf_real.rs index 54a4e4d4e42..404844d861f 100644 --- a/lib/elf/tests/real.rs +++ b/sdk/toyos/tests/elf_real.rs @@ -10,9 +10,9 @@ //! of=lib/elf/tests/fixtures/lld-headers.bin bs=4096 count=1`, and expect //! every number below to move: each is `readelf -hlnW` of the fixture. -use toyos_elf::{Layout, Machine}; +use toyos::elf::{Layout, Machine}; -const LLD_HEADERS: &[u8] = include_bytes!("fixtures/lld-headers.bin"); +const LLD_HEADERS: &[u8] = include_bytes!("elf/fixtures/lld-headers.bin"); #[test] fn a_linked_binary_parses_to_what_readelf_says() { @@ -76,18 +76,18 @@ fn objects_build_id(file: &[u8]) -> Option> { /// Ours, read as the kernel reads it: each `PT_NOTE` at its own offset. fn our_build_id(file: &[u8]) -> Option> { - toyos_elf::note::segments(file).find_map(|s| { + toyos::elf::note::segments(file).find_map(|s| { let notes = file.get(s.offset as usize..(s.offset + s.filesz) as usize)?; - toyos_elf::note::build_id(notes, s.align).map(<[u8]>::to_vec) + toyos::elf::note::build_id(notes, s.align).map(<[u8]>::to_vec) }) } #[test] fn an_lld_binarys_build_id_is_what_object_and_readelf_read() { Layout::parse(LLD_HEADERS, Machine::X86_64).expect("rust-lld's own output"); - let segments: Vec<_> = toyos_elf::note::segments(LLD_HEADERS).collect(); + let segments: Vec<_> = toyos::elf::note::segments(LLD_HEADERS).collect(); // `readelf -l`: `NOTE 0x0002a8 0x00000000000002a8 0x00000000000002a8 0x000024 0x000024 R 0x4`. - assert_eq!(segments, [toyos_elf::note::NoteSegment { offset: 0x2a8, filesz: 0x24, align: 4 }]); + assert_eq!(segments, [toyos::elf::note::NoteSegment { offset: 0x2a8, filesz: 0x24, align: 4 }]); let ours = our_build_id(LLD_HEADERS); assert_eq!(ours, objects_build_id(LLD_HEADERS)); // `readelf -n`: `Build ID: 97082d0f339a81a4dadc05e2f47960b079ab178c`. diff --git a/lib/symbols/tests/real.rs b/sdk/toyos/tests/elf_symbols.rs similarity index 70% rename from lib/symbols/tests/real.rs rename to sdk/toyos/tests/elf_symbols.rs index a824eb63fc8..8b11fdb54e8 100644 --- a/lib/symbols/tests/real.rs +++ b/sdk/toyos/tests/elf_symbols.rs @@ -1,19 +1,20 @@ -//! The positive control: `locate` agrees with an outside judge on a real +//! The positive control: `section::symbol_tables` agrees with an outside judge on a real //! binary's symbol table. //! //! Every other property this crate could assert is a property of hostile or //! synthetic bytes, and a parser that refused everything would pass those //! trivially. -use toyos_elf::sym::SymTab; +use toyos::elf::section::symbol_tables; +use toyos::elf::sym::SymTab; -const BINARY: &[u8] = include_bytes!("fixtures/input-test.bin"); +const BINARY: &[u8] = include_bytes!("elf/fixtures/input-test.bin"); #[test] fn locate_finds_the_same_tables_readelf_reports() { - let (symtab, strtab) = toyos_symbols::locate(BINARY).expect("input-test has a symtab"); + let (symtab, strtab) = symbol_tables(BINARY).expect("input-test has a symtab"); - // `readelf -S lib/symbols/tests/fixtures/input-test.bin`: + // `readelf -S sdk/toyos/tests/elf/fixtures/input-test.bin`: // [ 6] .symtab SYMTAB ... 00016ea8 ... // [ 7] .strtab STRTAB ... 0004b65f ... assert_eq!(symtab.len(), 0x16ea8); @@ -22,7 +23,7 @@ fn locate_finds_the_same_tables_readelf_reports() { #[test] fn a_real_binarys_own_functions_resolve_to_their_own_names() { - let (symtab, strtab) = toyos_symbols::locate(BINARY).expect("input-test has a symtab"); + let (symtab, strtab) = symbol_tables(BINARY).expect("input-test has a symtab"); let table = SymTab::new(symtab, strtab); // `readelf --syms`: `3888: 0000000000005a0c 16 FUNC GLOBAL DEFAULT 1 _start` @@ -42,7 +43,7 @@ fn a_real_binarys_own_functions_resolve_to_their_own_names() { } #[test] -fn a_file_header_locate_cannot_read_answers_none() { - assert_eq!(toyos_symbols::locate(&BINARY[..32]), None); - assert_eq!(toyos_symbols::locate(&[]), None); +fn a_file_header_it_cannot_read_answers_none() { + assert_eq!(symbol_tables(&BINARY[..32]), None); + assert_eq!(symbol_tables(&[]), None); } diff --git a/lib/elf/tests/tables.rs b/sdk/toyos/tests/elf_tables.rs similarity index 98% rename from lib/elf/tests/tables.rs rename to sdk/toyos/tests/elf_tables.rs index cdec089a003..cd2a609f62b 100644 --- a/lib/elf/tests/tables.rs +++ b/sdk/toyos/tests/elf_tables.rs @@ -10,15 +10,16 @@ // whole files that point at them. Every item is used by one of the two, so the // unused half is dead only from here. #[allow(dead_code)] +#[path = "elf/common/mod.rs"] mod common; use common::*; -use toyos_elf::dynamic::{self, Dynamic}; -use toyos_elf::gnu_hash::{self, GnuHash}; -use toyos_elf::rela::{self, Op, Rela, RelaTable, RelocError, RelocKind, Rules}; -use toyos_elf::section::{SectionTable, Unapplied, SHT_DYNSYM, SHT_REL, SHT_RELA, SHT_RELR, SHT_SYMTAB}; -use toyos_elf::sym::SymTab; -use toyos_elf::Machine; +use toyos::elf::dynamic::{self, Dynamic}; +use toyos::elf::gnu_hash::{self, GnuHash}; +use toyos::elf::rela::{self, Op, Rela, 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 and the symbol count are the @@ -33,7 +34,7 @@ fn validate( window, tls: Some(tls_segment(u64::MAX)), }; - let syms = vec![0; sym_count * toyos_elf::sym::ENTRY_SIZE]; + let syms = vec![0; sym_count * toyos::elf::sym::ENTRY_SIZE]; entries.try_for_each(|r| rela::parse(r, &rules, SymTab::new(&syms, &[])).map(|_| ())) } diff --git a/system/symbolize/Cargo.toml b/system/symbolize/Cargo.toml index cbc71220a47..b912165de8a 100644 --- a/system/symbolize/Cargo.toml +++ b/system/symbolize/Cargo.toml @@ -7,3 +7,10 @@ description = "Names the frames of a killed program's crash record: the file and [dependencies] toyos-symbols = { path = "../../lib/symbols" } +# The one ELF parser. +toyos = { path = "../../sdk/toyos" } + +[dev-dependencies] +# The namer's outside judge: unaligned, since an include_bytes! fixture is +# aligned to 1 and object reads a header in place. +object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] } diff --git a/system/symbolize/src/main.rs b/system/symbolize/src/main.rs index f5ced7e32df..49612b1d5c8 100644 --- a/system/symbolize/src/main.rs +++ b/system/symbolize/src/main.rs @@ -10,8 +10,8 @@ //! the directory that holds the image's files; a record whose name would leave //! that directory is refused. //! -//! **Nothing a line or a file holds makes it fail**: the lookup is -//! `toyos_symbols::name`, the kernel's own and panic-free on any bytes, and +//! **Nothing a line or a file holds makes it fail**: the lookup ([`name`]) is +//! panic-free on any bytes, and //! every other outcome is a sentence on the line it is about. use std::collections::HashMap; @@ -21,7 +21,9 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; use toyos_symbols::frame::{decode, Decoded}; -use toyos_symbols::{name, Unnamed}; + +mod name; +use name::{name, Unnamed}; const USAGE: &str = "usage: symbolize [--root DIR] [FILE]"; diff --git a/system/symbolize/src/name.rs b/system/symbolize/src/name.rs new file mode 100644 index 00000000000..a4e7d8cdef8 --- /dev/null +++ b/system/symbolize/src/name.rs @@ -0,0 +1,71 @@ +//! The function a recorded offset falls in, out of the file the record names, +//! marked when no build-id vouches for it. Panic-free on any bytes: the file +//! is whatever the record names. + +use toyos::elf::{note, section, sym::SymTab, FileHeader}; +use toyos_symbols::demangled; +use toyos_symbols::frame::BuildId; + +#[cfg(test)] +#[path = "name_tests.rs"] +mod tests; + +/// Why a recorded frame names no function. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Unnamed { + /// The file is no ELF image `toyos::elf` reads. + NotElf(toyos::elf::Error), + /// The file's build-id is not the one the record carries: it is another + /// build of that name, or one of the two has none. + OtherBuild { file: Option }, + /// The file has no symbol table. + NoSymbols, + /// No function in the file's symbol table holds the offset. + NoSymbol, +} + +/// Whether a name is known to be of the build that ran. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Check { + /// The file carries the build-id the record does. + BuildId, + /// Neither carries one, so nothing tells this build from another build of + /// that path: the name is printed, and printed as unchecked. + Unchecked, +} + +/// A recorded offset named: the function it falls in, how far into it, and +/// what vouches that the file is the build that ran. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Named<'a> { + pub function: &'a str, + pub within: u64, + pub check: Check, +} + +/// `+`, and ` (unchecked: no build-id)` after a name +/// [`Check::Unchecked`]. +impl core::fmt::Display for Named<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}+{:#x}", demangled(self.function), self.within)?; + match self.check { + Check::BuildId => Ok(()), + Check::Unchecked => f.write_str(" (unchecked: no build-id)"), + } + } +} + +/// The function `offset` falls in, and how far into it, out of `file`'s own +/// symbol table — when `file` is the build `id` names (`None`: a file with no +/// build-id). +pub fn name<'a>(file: &'a [u8], offset: u64, id: Option<&BuildId>) -> Result, Unnamed> { + FileHeader::parse(file).map_err(Unnamed::NotElf)?; + let carried = note::build_id_in(file).and_then(BuildId::new); + if carried.as_ref() != id { + return Err(Unnamed::OtherBuild { file: carried }); + } + let check = if carried.is_some() { Check::BuildId } else { Check::Unchecked }; + let (symtab, strtab) = section::symbol_tables(file).ok_or(Unnamed::NoSymbols)?; + let (function, within) = SymTab::new(symtab, strtab).resolve(offset).ok_or(Unnamed::NoSymbol)?; + Ok(Named { function, within, check }) +} diff --git a/lib/symbols/tests/name.rs b/system/symbolize/src/name_tests.rs similarity index 94% rename from lib/symbols/tests/name.rs rename to system/symbolize/src/name_tests.rs index 1cead501cbb..3b90f28be21 100644 --- a/lib/symbols/tests/name.rs +++ b/system/symbolize/src/name_tests.rs @@ -4,9 +4,10 @@ use object::{Object, ObjectSymbol, SymbolKind}; use toyos_symbols::frame::BuildId; -use toyos_symbols::{name, Check, Named, Unnamed}; -const BINARY: &[u8] = include_bytes!("fixtures/input-test.bin"); +use super::{name, Check, Named, Unnamed}; + +const BINARY: &[u8] = include_bytes!("../../../sdk/toyos/tests/elf/fixtures/input-test.bin"); /// `input-test` with its `PT_GNU_EH_FRAME` turned into a `PT_NOTE` over a /// build-id note appended past its end, far past its header page. @@ -73,7 +74,7 @@ fn the_build_the_record_names_is_the_one_named() { let id = [0x5a; 20]; let file = with_build_id(&id); let id = BuildId::new(&id).unwrap(); - assert_eq!(BuildId::find(&file, |s| file.get(s.offset as usize..(s.offset + s.filesz) as usize)), Some(id)); + assert_eq!(toyos::elf::note::build_id_in(&file).and_then(BuildId::new), Some(id)); assert_eq!(name(&file, 0x13a0, Some(&id)), Ok(Named { function: "main", within: 0, check: Check::BuildId })); } @@ -107,7 +108,7 @@ fn no_corrupted_file_panics_the_namer() { state ^= state << 17; state }; - let (_, strtab) = toyos_symbols::locate(BINARY).unwrap(); + let (_, strtab) = toyos::elf::section::symbol_tables(BINARY).unwrap(); let shoff = u64::from_le_bytes(BINARY[40..48].try_into().unwrap()) as usize; let strtab_at = BINARY.windows(strtab.len().min(64)).position(|w| w == &strtab[..w.len()]).unwrap(); // The file header, the program headers, the section headers and the first diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 04d2fea641e..abe32fa2605 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -939,7 +939,6 @@ name = "toyos" version = "0.18.0" dependencies = [ "toyos-abi", - "toyos-elf", ] [[package]] @@ -957,10 +956,6 @@ dependencies = [ name = "toyos-device-memory" version = "0.1.0" -[[package]] -name = "toyos-elf" -version = "0.1.0" - [[package]] name = "toyos-font" version = "0.2.0" diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs deleted file mode 100644 index 5f9bf2a91c3..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ /dev/null @@ -1,680 +0,0 @@ -//! A crafted ELF must not panic the kernel, and must not place a mapping the -//! kernel's own address-space rules forbid. -//! -//! `abuse_elf_segments` covers the sizes a program header declares. This one -//! covers what the kernel *derives* from them: the address a segment lands at -//! once the image is rebased to `USER_VM_BASE`. Each case is a kernel panic or -//! a forbidden mapping before the fix; each must be an error return after it, -//! with the kernel intact. A library is the program's own to load -//! (`toyos::dl`), so a crafted one is that program's to refuse, and its -//! loader's host tests hold that. -//! -//! An executable's dynamic section and thread-local storage are the program's -//! own (`toyos::relocate`, `toyos::tls`): the spawn cases crafted in them are -//! answered either way, the kernel lives, and a child the kernel starts dies -//! ([`dies`]). A real std image whose dynamic section names what its own -//! `_start` refuses exits [`REFUSED_STATUS`] with the line it says -//! ([`refuses_itself`]). - -#[path = "../image.rs"] -mod image; - -use std::fs; -use std::process::Command; - -use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; -use toyos_abi::RawHandle; -use toyos_abi::end::End; - - -/// Where each case's file is written. A spawn reads it back into a memory -/// object and hands the kernel that, as every spawn of a served program does. -const DIR: &str = "/tmp/abuse_loader_exe"; - -/// The two cases about a table larger than one kernel allocation need a file -/// larger than one kernel allocation, because `read_file_range` clamps a -/// declared length to what the file actually holds. `/tmp` is where that is -/// cheap: its pages are memory, so a 3 MiB file costs no device I/O, and -/// the case under test is the loader rather than the filesystem. -/// -/// Both cases asserted on the *declared* length alone until `/tmp` became -/// loadable — nothing in the tree could produce a >2 MiB file the loader would -/// open, so they passed without ever reaching the heap assert they exist for. -const BIG_DIR: &str = "/tmp/abuse_loader"; - -/// Comfortably past `mm::MAX_HEAP_ALLOC`, and past what dlmalloc rounds a -/// 2 MiB granule request up to. -const BIG: usize = 3 * 1024 * 1024; - -/// Kernel constants this test aims at, by value. -/// -/// `USER_VM_BASE` was where every exe image was rebased to and `ALLOC_FLOOR` -/// the bottom of the arena every library, TLS block and mmap came out of; -/// both are now addresses the space's placement may hand out. `KERNEL_HALF` -/// is where the direct map starts. A segment vaddr is only interesting here -/// relative to one of the three. -const USER_VM_BASE: u64 = 0x100_0000_0000; -const ALLOC_FLOOR: u64 = 0x0002_0000_0000; -const KERNEL_HALF: u64 = 0xFFFF_8000_0000_0000; - -const ET_DYN: u16 = 3; -const EM_X86_64: u16 = 62; -const PT_LOAD: u32 = 1; -const PT_DYNAMIC: u32 = 2; -const PT_TLS: u32 = 7; - -const PF_X: u32 = 1; -const PF_W: u32 = 2; -const PF_R: u32 = 4; - -const DT_NEEDED: i64 = 1; -const DT_STRTAB: i64 = 5; -const DT_SYMTAB: i64 = 6; -const DT_RELA: i64 = 7; -const DT_RELASZ: i64 = 8; -const DT_STRSZ: i64 = 10; -const DT_JMPREL: i64 = 23; -const DT_PLTRELSZ: i64 = 2; -const DT_GNU_HASH: i64 = 0x6fff_fef5u32 as i32 as i64; - -const R_X86_64_GLOB_DAT: u64 = 6; -const R_X86_64_RELATIVE: u64 = 8; -const R_X86_64_TPOFF64: u64 = 18; - -const STT_TLS: u8 = 6; -const STB_GLOBAL: u8 = 1; - -const PH_OFF: usize = 64; -const PH_SIZE: usize = 56; -const SH_SIZE: usize = 64; - -#[derive(Clone, Copy)] -struct Phdr { - kind: u32, - flags: u32, - offset: u64, - vaddr: u64, - filesz: u64, - memsz: u64, - align: u64, -} - -impl Phdr { - fn load(offset: u64, vaddr: u64, filesz: u64, memsz: u64, flags: u32) -> Self { - Self { kind: PT_LOAD, flags, offset, vaddr, filesz, memsz, align: 0x1000 } - } - - fn write(&self, out: &mut [u8]) { - out[0..4].copy_from_slice(&self.kind.to_le_bytes()); - out[4..8].copy_from_slice(&self.flags.to_le_bytes()); - out[8..16].copy_from_slice(&self.offset.to_le_bytes()); - out[16..24].copy_from_slice(&self.vaddr.to_le_bytes()); - out[24..32].copy_from_slice(&self.vaddr.to_le_bytes()); // p_paddr - out[32..40].copy_from_slice(&self.filesz.to_le_bytes()); - out[40..48].copy_from_slice(&self.memsz.to_le_bytes()); - out[48..56].copy_from_slice(&self.align.to_le_bytes()); - } -} - -/// A file the loader will accept as far as the case under test needs it to. -struct Elf { - bytes: Vec, - phdrs: Vec, - entry: u64, - shoff: u64, - shnum: u16, - shentsize: u16, -} - -impl Elf { - fn new(size: usize) -> Self { - Self { - bytes: vec![0u8; size], - phdrs: Vec::new(), - entry: 0x1000, - shoff: 0, - shnum: 0, - shentsize: SH_SIZE as u16, - } - } - - fn ph(mut self, p: Phdr) -> Self { - self.phdrs.push(p); - self - } - - fn entry(mut self, v: u64) -> Self { - self.entry = v; - self - } - - fn sections(mut self, shoff: u64, shnum: u16, shentsize: u16) -> Self { - self.shoff = shoff; - self.shnum = shnum; - self.shentsize = shentsize; - self - } - - fn poke(mut self, off: usize, data: &[u8]) -> Self { - self.bytes[off..off + data.len()].copy_from_slice(data); - self - } - - /// A DT_NULL-terminated dynamic table at `off`. - fn dynamic(mut self, off: usize, tags: &[(i64, u64)]) -> Self { - let mut at = off; - for &(tag, val) in tags { - self.bytes[at..at + 8].copy_from_slice(&tag.to_le_bytes()); - self.bytes[at + 8..at + 16].copy_from_slice(&val.to_le_bytes()); - at += 16; - } - self - } - - /// One Elf64_Rela at file offset `off`. - fn rela(self, off: usize, r_offset: u64, r_info: u64, r_addend: i64) -> Self { - self.poke(off, &r_offset.to_le_bytes()) - .poke(off + 8, &r_info.to_le_bytes()) - .poke(off + 16, &r_addend.to_le_bytes()) - } - - /// One Elf64_Sym at file offset `off`. - fn sym(self, off: usize, st_name: u32, st_info: u8, st_shndx: u16, st_value: u64) -> Self { - self.poke(off, &st_name.to_le_bytes()) - .poke(off + 4, &[st_info, 0]) - .poke(off + 6, &st_shndx.to_le_bytes()) - .poke(off + 8, &st_value.to_le_bytes()) - } - - fn build(mut self) -> Vec { - let phnum = self.phdrs.len(); - assert!(PH_OFF + phnum * PH_SIZE <= self.bytes.len(), "phdrs past the buffer"); - - self.bytes[..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']); - self.bytes[4] = 2; // ELFCLASS64 - self.bytes[5] = 1; // ELFDATA2LSB - self.bytes[6] = 1; // EV_CURRENT - self.bytes[16..18].copy_from_slice(&ET_DYN.to_le_bytes()); - self.bytes[18..20].copy_from_slice(&EM_X86_64.to_le_bytes()); - self.bytes[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version - self.bytes[24..32].copy_from_slice(&self.entry.to_le_bytes()); - self.bytes[32..40].copy_from_slice(&(PH_OFF as u64).to_le_bytes()); - self.bytes[40..48].copy_from_slice(&self.shoff.to_le_bytes()); - self.bytes[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize - self.bytes[54..56].copy_from_slice(&(PH_SIZE as u16).to_le_bytes()); - self.bytes[56..58].copy_from_slice(&(phnum as u16).to_le_bytes()); - self.bytes[58..60].copy_from_slice(&self.shentsize.to_le_bytes()); - self.bytes[60..62].copy_from_slice(&self.shnum.to_le_bytes()); - - for (i, p) in self.phdrs.iter().enumerate() { - let at = PH_OFF + i * PH_SIZE; - p.write(&mut self.bytes[at..at + PH_SIZE]); - } - self.bytes - } -} - -fn write_file(name: &str, bytes: &[u8]) -> String { - write_file_in(DIR, name, bytes) -} - -fn write_file_in(dir: &str, name: &str, bytes: &[u8]) -> String { - let path = format!("{dir}/{name}"); - fs::write(&path, bytes).unwrap_or_else(|e| panic!("write {path}: {e}")); - let got = fs::metadata(&path).map(|m| m.len()).unwrap_or(0); - assert_eq!(got as usize, bytes.len(), "{path}: short write"); - path -} - -fn spawn_path(path: &str) -> Result { - spawn_argv(path, &format!("{path}\0")) -} - -/// `path` read into an image and spawned with `argv`, NUL-separated, and no -/// descriptor of this process's. -fn spawn_argv(path: &str, argv: &str) -> Result { - let image = image::image_of(path); - let (image, image_len) = image.spawn_words(); - unsafe { - syscall::spawn(&SpawnArgs { - path_ptr: path.as_ptr() as u64, - path_len: path.len() as u64, - argv_ptr: argv.as_ptr() as u64, - argv_len: argv.len() as u64, - env_ptr: 0, - env_len: 0, - endow_ptr: 0, - endow_count: 0, - labels_ptr: 0, - labels_len: 0, - image, - image_len, - place: u64::from(toyos_abi::HANDLE_INVALID.0), - pager: u64::from(toyos_abi::HANDLE_INVALID.0), - }) - } -} - -fn spawn_result(name: &str, bytes: &[u8]) -> Result { - spawn_path(&write_file(name, bytes)) -} - -/// Spawn must refuse this file. A success is as much a failure as a panic: -/// every case here describes a process the kernel cannot safely build. -fn refused(name: &str, outcome: Result) { - match outcome { - Ok(child) => panic!("{name}: spawn started {child:?} for an ELF that must be refused"), - Err(e) => assert!( - matches!(e, SyscallError::InvalidArgument | SyscallError::ResourceExhausted), - "{name}: spawn gave {e:?}", - ), - } -} - -fn spawn_refused(name: &str, bytes: &[u8]) { - refused(name, spawn_result(name, bytes)); -} - -/// The kernel answers this spawn and lives, and a child it started dies. These -/// files are crafted in a dynamic section the kernel no longer reads: a -/// program relocates itself (`toyos::relocate`), and each of these enters at -/// bytes that are no program, so the child dies before it reads one. -fn dies(name: &str, outcome: Result) { - match outcome { - Ok(child) => { - // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. - let child = unsafe { toyos::process::Process::from_raw(child) }; - let end = child.wait().unwrap_or_else(|e| panic!("{name}: wait for the child: {e:?}")); - assert!(!end.success(), "{name}: a child entering at bytes that are no program exited 0"); - } - Err(e) => assert!( - matches!(e, SyscallError::InvalidArgument | SyscallError::ResourceExhausted), - "{name}: spawn gave {e:?}", - ), - } -} - -fn spawn_dies(name: &str, bytes: &[u8]) { - dies(name, spawn_result(name, bytes)); -} - -/// A minimal, honest exe: one PT_LOAD covering the whole file at vaddr 0. -fn base_exe(size: usize) -> Elf { - Elf::new(size).ph(Phdr::load(0, 0, size as u64, size as u64, PF_R | PF_X)).entry(0) -} - -fn main() { - // A copy [`refuses_itself`] spawns that reached this far was not refused. - if std::env::args().nth(1).as_deref() == Some(REFUSED_COPY) { - panic!("a copy of this program whose relocations its _start must refuse reached main"); - } - fs::create_dir_all(DIR).expect("create /tmp/abuse_loader_exe"); - fs::create_dir_all(BIG_DIR).expect("create /tmp/abuse_loader"); - - // 1. A DT_* vaddr below every PT_LOAD. - spawn_dies( - "rela_below_image", - &Elf::new(0x4000) - .ph(Phdr::load(0, 0x1000, 0x3000, 0x3000, PF_R | PF_W)) - .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x2000, filesz: 0x200, memsz: 0x200, align: 8 }) - .entry(0x1000) - .dynamic(0x1000, &[(DT_RELA, 0), (DT_RELASZ, 24)]) - .build(), - ); - - // 2. Two PT_LOADs whose page-rounded ranges overlap. Each becomes a VMA, - // and `insert_region` asserts that an address is not already covered — - // a kernel-bug assert reached from a file. - spawn_refused( - "load_overlap", - &base_exe(0x4000) - .ph(Phdr::load(0x1000, 0x800, 0x1000, 0x1000, PF_R | PF_W)) - .build(), - ); - - // 3. A segment whose rebased address lands in the kernel half. The image - // is placed at `USER_VM_BASE - vaddr_min`, and that addition wraps, so - // a file can name any address in the machine. The VMA is demand-paged, - // so the first user touch calls `remap`, which ORs PAGE_USER onto the - // *shared* kernel page tables — the mapping `sys_mmap` refuses a FIXED - // request for, reached through the loader instead. - spawn_refused( - "load_kernel_half", - &base_exe(0x4000) - .ph(Phdr::load(0, KERNEL_HALF.wrapping_sub(USER_VM_BASE), 0, 0x1000, PF_R | PF_W)) - .build(), - ); - - // 4. And the same wrap aimed at the allocation arena rather than the - // kernel: one segment covering everything from ALLOC_FLOOR upwards - // leaves `find_gap` nothing, and the TLS mapping every process gets - // was an `.expect` on that. - spawn_refused( - "load_covers_arena", - &base_exe(0x4000) - // The largest p_memsz that still leaves `p_vaddr + p_memsz` inside - // a u64, which `parse_layout` checks before anything else. - .ph(Phdr::load(0, ALLOC_FLOOR.wrapping_sub(USER_VM_BASE), 0, 0xFD_FFFF_FFFF, PF_R | PF_W)) - .build(), - ); - - // 4b. The whole image above USER_VM_BASE: vaddr_min itself exceeds the base, - // so `USER_VM_BASE - vaddr_min` underflows — a whole-machine trap where - // cases 3 and 4's low vaddr_min and stretched span are caught by the span test. - spawn_refused( - "load_only_above_vm_base", - &Elf::new(0x1000) - .ph(Phdr::load(0, USER_VM_BASE * 2, 0, 0x1000, PF_R | PF_W)) - .entry(USER_VM_BASE * 2) - .build(), - ); - - // 5. A PT_TLS p_align that is no power of two, which the program-header - // parse refuses. - spawn_refused( - "tls_align_wrap", - &base_exe(0x4000) - .ph(Phdr { kind: PT_TLS, flags: PF_R, offset: 0x2000, vaddr: 0x2000, filesz: 0, memsz: 8, align: u64::MAX }) - .build(), - ); - - // 8. A section header table of 2.4 MiB, in a file big enough to hold it, - // which the kernel read into one `Vec` past `mm/alloc.rs`'s assert when - // it looked for an executable's `.rela.dyn` and `.symtab` there. - dies( - "shnum_past_heap", - spawn_path(&write_file_in( - BIG_DIR, - "shnum_past_heap", - &base_exe(BIG).sections(0x1000, 40_000, 64).build(), - )), - ); - - // 9. Same ceiling, reached through DT_STRSZ instead — a size no section - // header has to agree with, in a file that can back it. - dies( - "strsz_past_heap", - spawn_path(&write_file_in( - BIG_DIR, - "strsz_past_heap", - &Elf::new(BIG) - .ph(Phdr::load(0, 0, BIG as u64, BIG as u64, 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, &[(DT_STRTAB, 0x2000), (DT_STRSZ, 2 * 1024 * 1024 + 4096)]) - .build(), - )), - ); - - // 12. Two relocation tables the ceiling accepted, whose derived index did - // not fit: `DT_RELASZ` and `DT_PLTRELSZ` were bounded apart and both - // fed one index the kernel built at spawn, 2.7 MiB in a single `Vec`. - { - const N: usize = 87_210; // MAX_HEAP_ALLOC / 24, the most either table can declare - const SZ: usize = N * 24; - const RELA: usize = 0x4000; - const JMPREL: usize = RELA + SZ; - let total = JMPREL + SZ; - let mut bytes = Elf::new(total) - .ph(Phdr::load(0, 0, total as u64, total as u64, 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, &[ - (DT_RELA, RELA as u64), (DT_RELASZ, SZ as u64), - (DT_JMPREL, JMPREL as u64), (DT_PLTRELSZ, SZ as u64), - ]) - .build(); - for i in 0..N * 2 { - let at = RELA + i * 24; - bytes[at..at + 8].copy_from_slice(&((i as u64) * 8).to_le_bytes()); - bytes[at + 8..at + 16].copy_from_slice(&R_X86_64_RELATIVE.to_le_bytes()); - } - dies( - "rela_index_past_heap", - spawn_path(&write_file_in(BIG_DIR, "rela_index_past_heap", &bytes)), - ); - } - - // 12b. More distinct DT_NEEDED names than the kernel loaded at spawn, each - // one private window. - { - const N: usize = 65; // one past the kernel's 64, all distinct - const STRTAB: usize = 0x2000; - let mut tags: Vec<(i64, u64)> = - vec![(DT_STRTAB, STRTAB as u64), (DT_STRSZ, (N * 4) as u64)]; - 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: 0x800, memsz: 0x800, align: 8 }) - .entry(0); - for i in 0..N { - let off = i * 4; - elf = elf.poke(STRTAB + off, &[b'a' + (i % 26) as u8, b'A' + (i / 26) as u8]); - tags.push((DT_NEEDED, off as u64)); - } - spawn_dies("too_many_needed_libs", &elf.dynamic(0x1000, &tags).build()); - } - - // 13. A RELATIVE write beginning in a page's last seven bytes: r_offset - // 0x1FFE + 8 crosses 0x2000, which the kernel's per-page applier dropped. - spawn_dies( - "reloc_straddles_fill_page", - &Elf::new(0x4000) - .ph(Phdr::load(0, 0, 0x4000, 0x4000, PF_R | PF_W)) - .ph(Phdr { kind: PT_DYNAMIC, flags: PF_R, offset: 0x1000, vaddr: 0x1000, filesz: 0x200, memsz: 0x200, align: 8 }) - .entry(0) - .dynamic(0x1000, &[(DT_RELA, 0x1200), (DT_RELASZ, 24)]) - .rela(0x1200, 0x1FFE, R_X86_64_RELATIVE, 0) - .build(), - ); - - // 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(); - - // 19. A relocation naming a symbol the executable's short-read `.dynsym` does - // not hold, which the kernel refused by name while it read one. - globdat_past_short_dynsym(); - - // 20. Thread-local references an executable's own start refuses. - executable_tls_refs_die(); - - // 21. A program header table no `PT_LOAD` maps: the one segment's file - // bytes start at 0x1000, past the table at 0x40. Its vaddr is 0, so - // `rebase_base` passes it and only the table refuses it. - let unmapped = Elf::new(0x2000).ph(Phdr::load(0x1000, 0, 0x1000, 0x1000, PF_R | PF_X)).entry(0).build(); - spawn_refused("phdrs_unmapped", &unmapped); - - // 22. This program's own image, a `RELR` table in one copy, which its - // `_start` refuses before it writes a word, and in another a library - // named by the empty string, which its start refuses before `main`. - refuses_itself("carries_relr", DT_RELR, REFUSED_LINE); - refuses_itself("needs_a_library", DT_NEEDED as u64, NO_LIBRARY_LINE); - - // 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(); - for i in 0..256 { - blocks.push(vec![(i % 251) as u8; 4096]); - } - for (i, b) in blocks.iter().enumerate() { - assert!(b.iter().all(|&x| x == (i % 251) as u8), "kernel heap corrupted block {i}"); - } - - let status = std::process::Command::new("/system/bin/echo") - .arg("loader still works") - .status() - .expect("spawn /system/bin/echo"); - assert!(status.success(), "/system/bin/echo exited {status:?}"); - - let _ = fs::remove_dir_all(DIR); - let _ = fs::remove_dir_all(BIG_DIR); - println!("crafted ELF derivations rejected, kernel intact"); -} - -/// Every file-chosen value an executable's start 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`. Each child dies; 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_dies( - "relative_addend_past_image", - &exe_with(&[], &[(0x2000, R_X86_64_RELATIVE, i64::MAX)], None).build(), - ); - // TPOFF64 against the executable's own 16-byte PT_TLS, `A = i64::MIN`: - // `tls::Static::tpoff` subtracted with overflow. - spawn_dies( - "tpoff_addend_past_tls", - &exe_with(&[], &[(0x2000, R_X86_64_TPOFF64, i64::MIN)], Some(16)).build(), - ); - // An export at `0xFFFF_FFFF_FFFF_F000` in an executable that needs a - // library: the kernel built a map of the exe's exports for the library's - // slots to bind against, and that map added the value to the load base. - { - let dep = "export_dep.so"; - // `.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_dies("export_past_image", &exe.build()); - } - -} - -const FAR: &str = "far"; -const FAR_VALUE: u64 = 0xFFFF_FFFF_FFFF_F000; -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 -} - -/// Thread-local references an executable's own start refuses: one to its own -/// symbol whose `S + A` leaves its `PT_TLS`, and one to a library's symbol, -/// which no executable may reach with the initial-exec model. -fn executable_tls_refs_die() { - spawn_dies("tls_apply_spawn", &exe_tls_ref_past_segment()); - - let dep = "tpoff_overflow_dep.so"; - // `.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_dies("tpoff_overflow_spawn", &exe.build()); -} - -/// 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"; - // 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_dies("globdat_past_dynsym", &exe.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() -} - -/// What `toyos::relocate` writes to its stderr, `fd:2`, when it refuses its own program. -const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; -/// What std's start writes there for a program whose `DT_NEEDED` names no -/// library. -const NO_LIBRARY_LINE: &str = "this program's libraries could not be loaded: ELF: a DT_NEEDED names no library\n"; -/// The status it exits with then. -const REFUSED_STATUS: i32 = 127; -/// The argument a refused copy is handed, which its `main` panics on. -const REFUSED_COPY: &str = "--refused-copy"; - -const DT_DEBUG: u64 = 21; -const DT_RELR: u64 = 36; - -/// This program's own file with its `DT_DEBUG` entry's tag made `tag`: a std -/// image as lld linked it, whose dynamic section now names `tag` ahead of its -/// `DT_RELA`. -fn own_image_tagged(tag: u64) -> Vec { - let exe = std::env::current_exe().expect("current_exe"); - let mut bytes = fs::read(&exe).unwrap_or_else(|e| panic!("read {exe:?}: {e}")); - let word = |bytes: &[u8], at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); - let phoff = word(&bytes, 32) as usize; - let phnum = usize::from(u16::from_le_bytes([bytes[56], bytes[57]])); - let dynamic = (0..phnum) - .map(|i| phoff + i * PH_SIZE) - .find(|&at| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) == PT_DYNAMIC) - .expect("lld's PT_DYNAMIC"); - let (offset, size) = (word(&bytes, dynamic + 8) as usize, word(&bytes, dynamic + 32) as usize); - let entry = (offset..offset + size).step_by(16).find(|&at| word(&bytes, at) == DT_DEBUG).expect("lld's DT_DEBUG"); - bytes[entry..entry + 8].copy_from_slice(&tag.to_le_bytes()); - bytes -} - -/// A copy of this program tagged `tag` exits [`REFUSED_STATUS`] and says -/// `line` on the stderr it is endowed. Endowed none, it says nothing and -/// exits the same. -fn refuses_itself(name: &str, tag: u64, line: &str) { - let path = write_file(name, &own_image_tagged(tag)); - let out = Command::new(&path) - .arg(REFUSED_COPY) - .output() - .unwrap_or_else(|e| panic!("{name}: spawn: {e}")); - let said = String::from_utf8_lossy(&out.stderr); - assert_eq!(out.status.code(), Some(REFUSED_STATUS), "{name}: exited {:?}, said {said:?}", out.status); - assert_eq!(said, line, "{name}"); - - let child = spawn_argv(&path, &format!("{path}\0{REFUSED_COPY}\0")) - .unwrap_or_else(|e| panic!("{name} without a stderr: spawn gave {e:?}")); - // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. - let child = unsafe { toyos::process::Process::from_raw(child) }; - let end = child.wait().unwrap_or_else(|e| panic!("{name} without a stderr: wait: {e:?}")); - assert_eq!(end, End::Exited(REFUSED_STATUS), "{name} without a stderr"); -} diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs deleted file mode 100644 index 3c231d8fc57..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! The kernel must survive an ELF whose program headers lie about their sizes -//! or place a segment outside the loaded image. -//! -//! `p_filesz` and `p_memsz` are a (copy length, buffer size) pair to a loader, -//! so the spec's `p_filesz <= p_memsz` is memory safety: inverted, a copy -//! overruns its destination with file-chosen length *and* content. Every case -//! must be an error return, with the kernel intact afterwards. A library's -//! headers are the program's own loader's to refuse (`toyos::dl`). - -use std::fs; - -use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; - - -/// Kernel-served, so a path spawn reaches the kernel's own open; every refusal -/// is asked again with the same bytes handed over as an image. -const DIR: &str = "/tmp/abuse_elf"; - -const ET_DYN: u16 = 3; -const EM_X86_64: u16 = 62; -const PT_LOAD: u32 = 1; -const PT_TLS: u32 = 7; - -/// One PT_LOAD covering the whole file plus one extra header the caller fills. -/// `entry` and the load segment are honest; only the extra header lies. -fn elf(extra: Phdr) -> Vec { - let mut out = vec![0u8; 64 + 2 * 56]; - - out[..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']); - out[4] = 2; // ELFCLASS64 - out[5] = 1; // ELFDATA2LSB - out[6] = 1; // EV_CURRENT - out[16..18].copy_from_slice(&ET_DYN.to_le_bytes()); - out[18..20].copy_from_slice(&EM_X86_64.to_le_bytes()); - out[20..24].copy_from_slice(&1u32.to_le_bytes()); - out[24..32].copy_from_slice(&0x1000u64.to_le_bytes()); // e_entry - out[32..40].copy_from_slice(&64u64.to_le_bytes()); // e_phoff - out[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize - out[54..56].copy_from_slice(&56u16.to_le_bytes()); // e_phentsize - out[56..58].copy_from_slice(&2u16.to_le_bytes()); // e_phnum - out[58..60].copy_from_slice(&64u16.to_le_bytes()); // e_shentsize - - let load = Phdr { kind: PT_LOAD, flags: 5, offset: 0, vaddr: 0, filesz: 0x2000, memsz: 0x2000, align: 0x1000 }; - load.write(&mut out[64..120]); - extra.write(&mut out[120..176]); - - // Give the file real content past the headers, so an overrunning copy - // moves a recognizable pattern rather than zeros. - out.resize(0x4000, 0x41); - out -} - -struct Phdr { - kind: u32, - flags: u32, - offset: u64, - vaddr: u64, - filesz: u64, - memsz: u64, - align: u64, -} - -impl Phdr { - fn write(&self, out: &mut [u8]) { - out[0..4].copy_from_slice(&self.kind.to_le_bytes()); - out[4..8].copy_from_slice(&self.flags.to_le_bytes()); - out[8..16].copy_from_slice(&self.offset.to_le_bytes()); - out[16..24].copy_from_slice(&self.vaddr.to_le_bytes()); - out[24..32].copy_from_slice(&self.vaddr.to_le_bytes()); // p_paddr - out[32..40].copy_from_slice(&self.filesz.to_le_bytes()); - out[40..48].copy_from_slice(&self.memsz.to_le_bytes()); - out[48..56].copy_from_slice(&self.align.to_le_bytes()); - } -} - -/// Write `bytes` to `path` and try to spawn it, then spawn the same bytes as an -/// image. Returns the spawn error, which the two routes must agree on. -fn spawn_err(name: &str, bytes: &[u8]) -> SyscallError { - let path = format!("{DIR}/{name}"); - fs::write(&path, bytes).unwrap_or_else(|e| panic!("write {path}: {e}")); - let by_path = spawn_as(&path, &[]); - let by_image = spawn_as(&path, bytes); - assert_eq!(by_path, by_image, "{name}: the path and the image were refused differently"); - by_path -} - -/// A raw spawn of `path`, handing `image` over in a memory object of this -/// process's own when it is not empty. -fn spawn_as(path: &str, image: &[u8]) -> SyscallError { - let argv = format!("{path}\0"); - let object = toyos::shm::SharedMemory::create(image.len().max(1)).expect("a memory object for the image"); - object.bytes().write(0, image); - unsafe { - syscall::spawn(&SpawnArgs { - path_ptr: path.as_ptr() as u64, - path_len: path.len() as u64, - argv_ptr: argv.as_ptr() as u64, - argv_len: argv.len() as u64, - env_ptr: 0, - env_len: 0, - endow_ptr: 0, - endow_count: 0, - labels_ptr: 0, - labels_len: 0, - image: toyos::AsHandle::as_handle(&object).raw().0 as u64, - image_len: image.len() as u64, - place: u64::from(toyos_abi::HANDLE_INVALID.0), - pager: u64::from(toyos_abi::HANDLE_INVALID.0), - }) - } - .map(|pid| panic!("{path}: spawn succeeded (pid {pid:?}) — the header is malformed")) - .unwrap_err() -} - -fn main() { - fs::create_dir_all(DIR).expect("create /tmp/abuse_elf"); - - // 1. PT_TLS filesz > memsz, which the program-header parse refuses. - let err = spawn_err("tls_filesz", &elf(Phdr { - kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x2000, - filesz: 0x4000, memsz: 16, align: 8, - })); - assert_eq!(err, SyscallError::InvalidArgument, "PT_TLS filesz > memsz"); - - // 3. PT_TLS vaddr far outside every PT_LOAD. - let err = spawn_err("tls_vaddr", &elf(Phdr { - kind: PT_TLS, flags: 4, offset: 0x2000, vaddr: 0x8000_0000, - filesz: 8, memsz: 16, align: 8, - })); - assert_eq!(err, SyscallError::InvalidArgument, "PT_TLS vaddr outside the image"); - - // 4. A second PT_LOAD with filesz > memsz. On the exe path the region - // bookkeeping clamps this, but it must still be refused rather than - // reaching the loader with an impossible header. - let err = spawn_err("load_filesz", &elf(Phdr { - kind: PT_LOAD, flags: 6, offset: 0x2000, vaddr: 0x4000, - filesz: 0x2000, memsz: 0x10, align: 0x1000, - })); - assert_eq!(err, SyscallError::InvalidArgument, "PT_LOAD filesz > memsz"); - - // The kernel heap is intact: allocate and touch enough to walk it, then - // prove spawn still works. - let mut blocks: Vec> = Vec::new(); - for i in 0..256 { - blocks.push(vec![(i % 251) as u8; 4096]); - } - for (i, b) in blocks.iter().enumerate() { - assert!(b.iter().all(|&x| x == (i % 251) as u8), "kernel heap corrupted block {i}"); - } - - let status = std::process::Command::new("/system/bin/echo") - .arg("loader still works") - .status() - .expect("spawn /system/bin/echo"); - assert!(status.success(), "/system/bin/echo exited {status:?}"); - - let _ = fs::remove_dir_all(DIR); - println!("malformed ELF program headers rejected, kernel intact"); -} diff --git a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs index 9b84e6a9cfc..a2c533d2ba7 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -75,7 +75,7 @@ fn main() { unsafe { core::ptr::copy_nonoverlapping(ARGV0.as_ptr(), argv, ARGV0.len()) }; let echo = image::image_of("/system/bin/echo"); - let (image, image_len) = echo.spawn_words(); + let laid = image::laid(&echo); let spawn_with = |endow_count: u64| { let args = SpawnArgs { path_ptr: argv as u64, @@ -88,8 +88,12 @@ fn main() { endow_count, labels_ptr: (region as u64) + (REGION / 8) as u64, labels_len: LABELS.len() as u64, - image, - image_len, + maps_ptr: laid.maps().as_ptr() as u64, + maps_count: laid.maps().len() as u64, + entry: laid.entry, + image_base: laid.image_base, + build_id_ptr: laid.build_id().as_ptr() as u64, + build_id_len: laid.build_id().len() as u64, place: u64::from(toyos_abi::HANDLE_INVALID.0), pager: u64::from(toyos_abi::HANDLE_INVALID.0), }; @@ -192,7 +196,7 @@ fn a_pair_is_refused_whole(filled: &mut Vec) { /// `entries` into the blob `labels`. fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result { let argv = format!("{SELF}\0{CHILD}"); - let (image, image_len) = own().spawn_words(); + let laid = image::laid(&own()); // SAFETY: every pointer names a live local for the whole call. unsafe { syscall::spawn(&SpawnArgs { @@ -206,8 +210,12 @@ fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result Result { + let argv = format!("{SELF}\0{CHILD}\0"); + // SAFETY: every pointer names a live local for the whole call. + unsafe { + syscall::spawn(&SpawnArgs { + path_ptr: SELF.as_ptr() as u64, + path_len: SELF.len() as u64, + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + env_ptr: 0, + env_len: 0, + endow_ptr: 0, + endow_count: 0, + labels_ptr: 0, + labels_len: 0, + maps_ptr: maps.as_ptr() as u64, + maps_count: maps.len() as u64, + entry, + image_base, + build_id_ptr: 0, + build_id_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), + pager: u64::from(toyos_abi::HANDLE_INVALID.0), + }) + } +} + +fn runs(what: &str, image: &Image) { + let laid = image::laid(image); + let child = spawn(laid.maps(), laid.entry, laid.image_base).unwrap_or_else(|e| panic!("{what}: an honest spawn: {e:?}")); + let end = syscall::process_wait(child).unwrap_or_else(|e| panic!("{what}: wait: {e:?}")); + assert_eq!(end, End::Exited(0), "{what}: the child ended {end}"); +} + +fn main() { + if std::env::args().nth(1).as_deref() == Some(CHILD) { + return; + } + let image = image::image_of(SELF); + let laid = image::laid(&image); + runs("first", &image); + let honest = laid.maps().to_vec(); + let text = honest.iter().position(|m| m.prot == VmProt::ReadExec as u64).expect("an executable range"); + let refused = |what: &str, maps: Vec, entry: u64, want: SyscallError| { + assert_eq!(spawn(&maps, entry, laid.image_base).err(), Some(want), "{what}"); + println!("abuse_spawn_maps: {what}: refused {want:?}"); + }; + let with = |edit: &dyn Fn(&mut Vec)| { + let mut maps = honest.clone(); + edit(&mut maps); + maps + }; + let invalid = SyscallError::InvalidArgument; + + refused("no ranges", Vec::new(), laid.entry, invalid); + refused("more ranges than a spawn carries", vec![honest[0]; MAX_SPAWN_MAPS + 1], laid.entry, invalid); + refused("an address that is no page boundary", with(&|m| m[0].addr += 8), laid.entry, invalid); + refused("a length that is no whole pages", with(&|m| m[0].len += 8), laid.entry, invalid); + refused("a range past the user half", with(&|m| m[0].addr = 0x7fff_ffff_f000), laid.entry, invalid); + refused("a range in the kernel half", with(&|m| m[0].addr = 0xffff_8000_0000_0000), laid.entry, invalid); + refused("a protection no VmProt names", with(&|m| m[text].prot = 4), laid.entry, invalid); + refused("more bytes copied than the range holds", with(&|m| m[0].copied = m[0].len + 1), laid.entry, invalid); + refused("a copy past the object's end", with(&|m| m[0].offset = 0x4000_0000), laid.entry, invalid); + refused("a copy from no object", with(&|m| m[0].object = NONE), laid.entry, invalid); + refused("a range on the clock page", with(&|m| m.push(SpawnMap { addr: toyos_abi::clock::CLOCK_PAGE, len: 0x1000, object: NONE, ..SpawnMap::default() })), laid.entry, invalid); + refused("two ranges that meet", with(&|m| m.push(m[0])), laid.entry, invalid); + refused("an entry in no executable range", honest.clone(), honest[0].addr + honest[0].len - 8, invalid); + + // A handle without MAP is no right to the object's bytes. + let object = honest[text].object as u32; + // SAFETY: nothing relies on being the source's only holder. + let unmappable = unsafe { syscall::dup_narrowed(RawHandle(object), Rights::DUP.union(Rights::TRANSFER)) }.expect("a duplicate without MAP"); + refused("an object without MAP", with(&|m| m[text].object = u64::from(unmappable.0)), laid.entry, SyscallError::PermissionDenied); + // SAFETY: no type holds this number. + unsafe { syscall::close(unmappable) }; + + runs("after every refusal", &image::image_of(SELF)); + println!("abuse_spawn_maps: PASS"); +} diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 29d29d92b2c..9b82255db4c 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -233,7 +233,7 @@ fn launcher_copy() -> OwnedHandle { fn spawn_under(place: RawHandle) -> Result { let argv = format!("{SELF_PATH}\0c"); let own = image::image_of(SELF_PATH); - let (image, image_len) = own.spawn_words(); + let laid = image::laid(&own); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -245,8 +245,12 @@ fn spawn_under(place: RawHandle) -> Result { endow_count: 0, labels_ptr: 0, labels_len: 0, - image, - image_len, + maps_ptr: laid.maps().as_ptr() as u64, + maps_count: laid.maps().len() as u64, + entry: laid.entry, + image_base: laid.image_base, + build_id_ptr: laid.build_id().as_ptr() as u64, + build_id_len: laid.build_id().len() as u64, place: u64::from(place.0), pager: u64::from(toyos_abi::HANDLE_INVALID.0), }; diff --git a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs index 005736e417a..54df65317fa 100644 --- a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs +++ b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs @@ -100,7 +100,7 @@ fn spawn(own: &Image, role: &str, stdout: Option) -> Result = stdout.into_iter().map(|handle| EndowEntry { label_off: 0, label_len: LABEL.len() as u32, handle, _pad: 0 }).collect(); - let (image, image_len) = own.spawn_words(); + let laid = image::laid(&own); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -112,8 +112,12 @@ fn spawn(own: &Image, role: &str, stdout: Option) -> Result (SharedMemory, u64) { +/// `path` read whole into a memory object of this process's own, and its bytes. +fn object_of(path: &str) -> (SharedMemory, Vec) { let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {path}: {e}")); let object = SharedMemory::create(bytes.len()).expect("a memory object"); object.bytes().write(0, &bytes); - (object, bytes.len() as u64) + (object, bytes) } -fn spawn(image: RawHandle, len: u64) -> Result { +/// Spawn the program `bytes`, laid out from the object `image` names. +fn spawn(image: RawHandle, bytes: &[u8]) -> Result { let argv = format!("{SELF}\0{CHILD}\0"); + let laid = lay_out(bytes, Machine::RUNNING, u64::from(image.0), BASE).expect("this program lays out"); // SAFETY: every pointer names a live local for the whole call. unsafe { syscall::spawn(&SpawnArgs { @@ -41,8 +45,12 @@ fn spawn(image: RawHandle, len: u64) -> Result { endow_count: 0, labels_ptr: 0, labels_len: 0, - image: image.0 as u64, - image_len: len, + maps_ptr: laid.maps().as_ptr() as u64, + maps_count: laid.maps().len() as u64, + entry: laid.entry, + image_base: laid.image_base, + build_id_ptr: laid.build_id().as_ptr() as u64, + build_id_len: laid.build_id().len() as u64, place: u64::from(toyos_abi::HANDLE_INVALID.0), pager: u64::from(toyos_abi::HANDLE_INVALID.0), }) @@ -50,8 +58,8 @@ fn spawn(image: RawHandle, len: u64) -> Result { } fn runs(what: &str) { - let (object, len) = object_of(SELF); - let child = spawn(object.as_handle().raw(), len).unwrap_or_else(|e| panic!("{what}: spawn from an object: {e:?}")); + let (object, bytes) = object_of(SELF); + let child = spawn(object.as_handle().raw(), &bytes).unwrap_or_else(|e| panic!("{what}: spawn from an object: {e:?}")); drop(object); let end = syscall::process_wait(child).unwrap_or_else(|e| panic!("{what}: wait: {e:?}")); assert_eq!(end, End::Exited(0), "{what}: the child spawned from an object ended {end}"); @@ -65,19 +73,16 @@ fn main() { runs("first"); - let (object, len) = object_of(SELF); + let (object, bytes) = object_of(SELF); // SAFETY: nothing relies on being the source's only holder. let unmappable = unsafe { syscall::dup_narrowed(object.as_handle().raw(), Rights::DUP.union(Rights::TRANSFER)) } .expect("a duplicate without MAP"); - assert_eq!(spawn(unmappable, len).err(), Some(SyscallError::PermissionDenied), "an object without MAP"); + assert_eq!(spawn(unmappable, &bytes).err(), Some(SyscallError::PermissionDenied), "an object without MAP"); // SAFETY: no type holds this number. unsafe { syscall::close(unmappable) }; - // Past the whole object, which the kernel rounds up to 2 MiB pages. - let past = (len | ((2 << 20) - 1)) + 2; - assert_eq!(spawn(object.as_handle().raw(), past).err(), Some(SyscallError::InvalidArgument), "a length past the object"); - println!("spawn_image_object: an object without MAP and a length past one are refused"); + println!("spawn_image_object: an object without MAP is refused"); - let child = spawn(object.as_handle().raw(), len).expect("spawn from the object that is then overwritten"); + let child = spawn(object.as_handle().raw(), &bytes).expect("spawn from the object that is then overwritten"); // The kernel reads the object only by copying, which is what this races. object.bytes().write(0, &vec![0xF4; object.len()]); let end = syscall::process_wait(child).expect("the overwritten child is waited for"); diff --git a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs index 13c771280bd..436ca826d59 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -81,7 +81,7 @@ fn test() { fn spawn_under(place: RawHandle) -> Result { let argv = format!("{SELF_PATH}\0child"); let own = image::image_of(SELF_PATH); - let (image, image_len) = own.spawn_words(); + let laid = image::laid(&own); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -93,8 +93,12 @@ fn spawn_under(place: RawHandle) -> Result { endow_count: 0, labels_ptr: 0, labels_len: 0, - image, - image_len, + maps_ptr: laid.maps().as_ptr() as u64, + maps_count: laid.maps().len() as u64, + entry: laid.entry, + image_base: laid.image_base, + build_id_ptr: laid.build_id().as_ptr() as u64, + build_id_len: laid.build_id().len() as u64, place: u64::from(place.0), pager: u64::from(toyos_abi::HANDLE_INVALID.0), }; diff --git a/tests/guest/rust-tests/src/bin/start_refuses_itself.rs b/tests/guest/rust-tests/src/bin/start_refuses_itself.rs new file mode 100644 index 00000000000..64e15b99409 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/start_refuses_itself.rs @@ -0,0 +1,109 @@ +//! A real std program whose dynamic section names what its own start refuses +//! exits [`REFUSED_STATUS`] and says why on the stderr it is endowed, and says +//! nothing and exits the same endowed none: a `RELR` table, which +//! `toyos::relocate` refuses before it writes a word, and a `DT_NEEDED` naming +//! no library, which `toyos::dl::start` refuses before `main`. The kernel +//! parses neither; the parent lays the copy out and the copy refuses itself. + +#[path = "../image.rs"] +mod image; + +use std::fs; +use std::process::Command; + +use toyos_abi::end::End; +use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; +use toyos_abi::RawHandle; + +const DIR: &str = "/tmp/start_refuses_itself"; + +/// What `toyos::relocate` writes to its stderr, `fd:2`, when it refuses its own program. +const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; +/// What the start writes there for a program whose `DT_NEEDED` names no library. +const NO_LIBRARY_LINE: &str = "this program's libraries could not be loaded: ELF: a DT_NEEDED names no library\n"; +/// The status it exits with then. +const REFUSED_STATUS: i32 = 127; +/// The argument a refused copy is handed, which its `main` panics on. +const REFUSED_COPY: &str = "--refused-copy"; + +const PT_DYNAMIC: u32 = 2; +const PH_SIZE: usize = 56; +const DT_NEEDED: u64 = 1; +const DT_DEBUG: u64 = 21; +const DT_RELR: u64 = 36; + +/// This program's own file with its `DT_DEBUG` entry's tag made `tag`: a std +/// image as lld linked it, whose dynamic section now names `tag` ahead of its +/// `DT_RELA`. `DT_NEEDED`'s value, 0, names the empty string. +fn own_image_tagged(tag: u64) -> Vec { + let exe = std::env::current_exe().expect("current_exe"); + let mut bytes = fs::read(&exe).unwrap_or_else(|e| panic!("read {exe:?}: {e}")); + let word = |bytes: &[u8], at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + let phoff = word(&bytes, 32) as usize; + let phnum = usize::from(u16::from_le_bytes([bytes[56], bytes[57]])); + let dynamic = (0..phnum) + .map(|i| phoff + i * PH_SIZE) + .find(|&at| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) == PT_DYNAMIC) + .expect("lld's PT_DYNAMIC"); + let (offset, size) = (word(&bytes, dynamic + 8) as usize, word(&bytes, dynamic + 32) as usize); + let entry = (offset..offset + size).step_by(16).find(|&at| word(&bytes, at) == DT_DEBUG).expect("lld's DT_DEBUG"); + bytes[entry..entry + 8].copy_from_slice(&tag.to_le_bytes()); + bytes +} + +/// `path` read into an image and spawned with `argv`, NUL-separated, endowed nothing. +fn spawn_endowed_nothing(path: &str, argv: &str) -> Result { + let image = image::image_of(path); + let laid = image::laid(&image); + // SAFETY: every pointer names a live local for the whole call. + unsafe { + syscall::spawn(&SpawnArgs { + path_ptr: path.as_ptr() as u64, + path_len: path.len() as u64, + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + env_ptr: 0, + env_len: 0, + endow_ptr: 0, + endow_count: 0, + labels_ptr: 0, + labels_len: 0, + maps_ptr: laid.maps().as_ptr() as u64, + maps_count: laid.maps().len() as u64, + entry: laid.entry, + image_base: laid.image_base, + build_id_ptr: laid.build_id().as_ptr() as u64, + build_id_len: laid.build_id().len() as u64, + place: u64::from(toyos_abi::HANDLE_INVALID.0), + pager: u64::from(toyos_abi::HANDLE_INVALID.0), + }) + } +} + +fn refuses_itself(name: &str, tag: u64, line: &str) { + let path = format!("{DIR}/{name}"); + fs::write(&path, own_image_tagged(tag)).unwrap_or_else(|e| panic!("write {path}: {e}")); + let out = Command::new(&path).arg(REFUSED_COPY).output().unwrap_or_else(|e| panic!("{name}: spawn: {e}")); + let said = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(REFUSED_STATUS), "{name}: exited {:?}, said {said:?}", out.status); + assert_eq!(said, line, "{name}"); + + let child = spawn_endowed_nothing(&path, &format!("{path}\0{REFUSED_COPY}\0")) + .unwrap_or_else(|e| panic!("{name} without a stderr: spawn gave {e:?}")); + // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. + let child = unsafe { toyos::process::Process::from_raw(child) }; + let end = child.wait().unwrap_or_else(|e| panic!("{name} without a stderr: wait: {e:?}")); + assert_eq!(end, End::Exited(REFUSED_STATUS), "{name} without a stderr"); + println!("start_refuses_itself: {name}: refused with 127, and said why where it had a stderr"); +} + +fn main() { + if std::env::args().nth(1).as_deref() == Some(REFUSED_COPY) { + panic!("a copy whose start must refuse it reached main"); + } + fs::create_dir_all(DIR).expect("create the test's directory"); + refuses_itself("carries_relr", DT_RELR, REFUSED_LINE); + refuses_itself("needs_a_library", DT_NEEDED, NO_LIBRARY_LINE); + let _ = fs::remove_dir_all(DIR); + println!("start_refuses_itself: PASS"); +} diff --git a/tests/guest/rust-tests/src/image.rs b/tests/guest/rust-tests/src/image.rs index 1d9de3c22cc..47e449913f8 100644 --- a/tests/guest/rust-tests/src/image.rs +++ b/tests/guest/rust-tests/src/image.rs @@ -1,14 +1,17 @@ -//! Read a served executable into the memory object a raw spawn accepts. +//! Read a served executable into a memory object, and lay it out as the +//! ranges a raw spawn names (`toyos::elf::program`). // Each binary takes the half it needs. #![allow(dead_code)] use std::io::Read as _; +use toyos::elf::program::{lay_out, Program, BASE}; +use toyos::elf::Machine; use toyos::process::Image; /// `path`, read whole into a memory object of this process's own, which a -/// `SpawnArgs` names by [`Image::spawn_words`]. +/// spawn's ranges name ([`laid`]). pub fn image_of(path: &str) -> Image { let mut file = std::fs::File::open(path).unwrap_or_else(|e| panic!("open {path}: {e}")); let len = file.metadata().unwrap_or_else(|e| panic!("stat {path}: {e}")).len(); @@ -22,3 +25,10 @@ pub fn in_tmp(path: &str) -> String { std::fs::copy(path, ©).unwrap_or_else(|e| panic!("copy {path} to {copy}: {e}")); copy } + +/// `image` laid out as the SDK lays out a spawn's program: a raw `SpawnArgs` +/// names its ranges (`maps_ptr`, `maps_count`), `entry`, `image_base` and +/// build-id. +pub fn laid(image: &Image) -> Program { + lay_out(image.bytes(), Machine::RUNNING, image.handle(), BASE).unwrap_or_else(|why| panic!("lay out an image: {why}")) +} From b36b16e12551a1aefb21b2a81a767a47a4c73952 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 18:54:24 +0100 Subject: [PATCH 09/13] Host CI's findings: the merge's conflict markers, the format crate's lints The merge left vm.rs's conflict markers in, where main reworked code this branch deletes. toyos-kimage's writer is part of the crate rather than a feature the model-control gate would read as a control, and the kernel's symbol table, now a borrow of the section, needs no hand-written Send or Sync. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - Cargo.toml | 2 +- build/sourcegate.rs | 1 - kernel/src/syscall/vm.rs | 312 --------------------------------------- lib/kimage/Cargo.toml | 7 - lib/kimage/src/lib.rs | 15 +- 6 files changed, 7 insertions(+), 331 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd472cc83b1..1a670522b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3835,7 +3835,6 @@ name = "toyos-kimage" version = "0.1.0" dependencies = [ "toyos-abi", - "toyos-kimage", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dd31b16327c..f6c3fa40b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,7 +242,7 @@ toyos-abi = { path = "sdk/abi" } # the supervisor's images with. toyos = { path = "sdk/toyos" } # The kernel section the build writes (`build::kimage`). -toyos-kimage = { path = "lib/kimage", features = ["write"] } +toyos-kimage = { path = "lib/kimage" } toyos-symbols = { path = "lib/symbols" } # What `build::fpgate` decodes the linked x86-64 kernel with, to refuse an # instruction that touches the user FP or vector state outside `arch::fpu`: diff --git a/build/sourcegate.rs b/build/sourcegate.rs index 31670a31916..f14b12b11e7 100644 --- a/build/sourcegate.rs +++ b/build/sourcegate.rs @@ -141,7 +141,6 @@ const AUTO_TRAIT_IMPLS: &[(&str, usize)] = &[ ("kernel/src/object/paged.rs", 1), ("kernel/src/process.rs", 1), ("kernel/src/sched/driver.rs", 2), - ("kernel/src/symbols.rs", 2), ]; /// Directories whose Rust is compiled for the guest, by repository-relative diff --git a/kernel/src/syscall/vm.rs b/kernel/src/syscall/vm.rs index c4945e2ce8c..756d998bac0 100644 --- a/kernel/src/syscall/vm.rs +++ b/kernel/src/syscall/vm.rs @@ -107,315 +107,3 @@ pub(super) fn sys_vm_remap(addr: u64, old: u64, new: u64, may_move: u64) -> u64 } -<<<<<<< HEAD -======= -/// 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. - if let Some(idx) = - process::with_process_data(|d| d.elf.lib_paths.iter().position(|p| *p == resolved)) - { - if let Some(out) = init_out { - if ctx.copy_out(out, &[0u64, 0]).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - return idx as 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(); - } - }; - - let pt = process::current_address_space(); - // The module's own program headers decide which pages are writable and - // which executable; mapping the whole image writable would make it - // executable too. - let Some(base) = lib.map_into(&pt) else { - log!("dlopen: {}: out of virtual address space", resolved); - return SyscallError::ResourceExhausted.to_u64(); - }; - if base != lib.user_base { - lib.user_base = base; - crate::elf::rebase_relative_relocs(&lib); - } - - // The mapping is committed before the fallible copy-out below; this guard - // takes it back if the copy-out refuses, so a refused dlopen leaves no - // library mapped or registered. - let span = lib.image.size() as u64; - let mapping = crate::rollback::Rollback::new(move || pt.lock().release(base, span)); - - let lib_tls = lib.tls().and_then(toyos_elf::TlsSegment::occupied); - let data_arc = process::process_data(); - // References no module defines: a count in this load's one record, as a spawn's are. - let mut unresolved; - let init_info = { - let data = data_arc.lock(); - unresolved = crate::elf::resolve_dlopen_relocs(&lib, &data.elf.loaded_libs); - - // 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 initial_exec = if data.elf.tls.total_memsz() > 0 { - crate::elf::apply_tpoff_relocs(&lib, 0, data.elf.tls, &tls_info) - } else { - Ok(0) - }; - let general_dynamic = initial_exec.and_then(|left| match lib_tls { - Some(_) => crate::elf::apply_dtpoff_relocs(&lib, &tls_info).map(|more| left + more), - None => Ok(left), - }); - match general_dynamic { - Ok(left) => unresolved += left, - Err(refused) => { - log!("dlopen: {}: {}", resolved, refused.as_str()); - return SyscallError::InvalidArgument.to_u64(); - } - } - - // 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 - // refused copy-out registers nothing and the mapping guard rolls back. - if let Some(out) = init_out { - if ctx.copy_out(out, &init_info).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - - let mut data = data_arc.lock(); - // Asked again under the guard that registers: the lookup at the top released - // this lock across the whole load, so a sibling can have registered the name - // meanwhile. The loser commits nothing and its mapping goes down with the guard. - if let Some(idx) = data.elf.lib_paths.iter().position(|p| *p == resolved) { - drop(data); - // Empty init, the same answer the lookup at the top of the call gives. - if let Some(out) = init_out { - if ctx.copy_out(out, &[0u64, 0]).is_err() { - return SyscallError::BadAddress.to_u64(); - } - } - return idx as u64; - } - // Asked here for the same reason, so a load past the bound, or one whose - // records the process cannot be charged, is refused having registered - // nothing; its mapping goes down with the guard. - if data.elf.loaded_libs.len() >= MAX_LIBRARIES || data.charge.grow(library_heap(&resolved)).is_err() { - drop(data); - return SyscallError::ResourceExhausted.to_u64(); - } - mapping.commit(); - - let idx = data.elf.loaded_libs.len(); - // 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 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 { - libs: &data.elf.loaded_libs, - modules: &data.elf.tls_modules, - }; - unresolved += 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() as usize, - base_offset: 0, - module_id, - is_static: false, - }); - } - log!("dlopen: {} pid={} unresolved={}", resolved, process::current_process(), unresolved); - data.elf.lib_paths.push(resolved); - data.elf.loaded_libs.push(lib); - idx as u64 -} - -/// The heap a module's records take in its process, which keeps them for its -/// life: its place in the lists of libraries, paths and TLS modules, each of -/// which doubles, and its path. -fn library_heap(path: &str) -> usize { - use core::mem::size_of; - 2 * (size_of::() + size_of::() + size_of::()) - + charge::array::(path.len()) -} - -/// Allocates a TLS block for the current thread's DTV entry for `module_id`, -/// returning its virtual address. `module_id` crosses the trust boundary: -/// every rejection is an error return, never a panic. -pub(super) fn sys_tls_alloc_block(module_id: u64) -> u64 { - match tls_alloc_block(module_id) { - Ok(vaddr) => vaddr, - Err(e) => e.to_u64(), - } -} - -fn tls_alloc_block(module_id: u64) -> Result { - // The valid set is the process's own module list, which the kernel built. - if module_id == 0 { - return Err(SyscallError::InvalidArgument); - } - // Bounded by the kernel's own `DTV_INITIAL_CAPACITY`, never the DTV's own - // `len` field, which the process can rewrite. - if module_id > crate::loader::DTV_INITIAL_CAPACITY as u64 { - return Err(SyscallError::ResourceExhausted); - } - - let owner_arc = process::process_data(); - let (tls_memsz, tls_template) = { - let data = owner_arc.lock(); - let m = data.elf.tls_modules.iter().find(|m| m.module_id == module_id) - .ok_or(SyscallError::InvalidArgument)?; - (m.memsz, m.template) - }; - - // A DTV entry leaves DTV_UNALLOCATED once and never returns; serving a - // fresh block on a repeat call would leave the first mapping present, - // USER and writable, over whatever the PMM hands out next. - let tid = process::current_tid(); - let existing = process::with_process_data(|data| { - data.elf.dynamic_tls_blocks.get(&(tid, module_id)).map(|(b, _)| b.at) - }); - - let tls_vaddr = match existing { - Some(vaddr) => vaddr, - None => { - let place = Charge::current(charge::map_entry::<(process::Tid, u64), (process::KernelMapping, Charge)>())?; - let block = crate::object::shm::SharedMemObject::create(&crate::mm::charge::current(), tls_memsz.max(1) as u64)?; - if let Some(template) = &tls_template { - // SAFETY: the template is a bounds-checked view of the module's - // own image, which its `LoadedLib` keeps alive. - block.write(0, unsafe { template.as_slice() }); - } - let pt = process::current_address_space(); - let len = block.size(); - let vaddr = pt.lock().map(block.clone(), 0, len, kernel::vm::Prot::ReadWrite, crate::mm::space::At::ANYWHERE)?; - // A thread asks for its own blocks alone, so none raced this one in. - process::with_process_data(|data| { - data.elf.dynamic_tls_blocks.insert((tid, module_id), (process::KernelMapping { object: block, at: vaddr }, place)) - }); - vaddr - } - }; - - // Through the thread's own TLS object, never by chasing a pointer out of - // the FS base, which addresses user-writable memory. - let tls = process::with_current_data(|data| { - data.tls.as_ref().map(|tls| alloc::sync::Arc::clone(&tls.object)).expect("sys_tls_alloc_block: thread has no TLS block") - }); - // `module_id` is bounded non-zero and at most `DTV_INITIAL_CAPACITY` at the - // top of this function, so the slot is inside the DTV after its two - // header words, which the block's first bytes hold. - tls.write((2 + (module_id - 1)) * 8, &tls_vaddr.raw().to_ne_bytes()); - Ok(tls_vaddr.raw()) -} - -pub(super) fn sys_dlsym(handle: u64, name: &str) -> u64 { - let data_arc = process::process_data(); - let data = data_arc.lock(); - let idx = handle as usize; - if idx >= data.elf.loaded_libs.len() { - return SyscallError::NotFound.to_u64(); - } - match crate::elf::dlsym(&data.elf.loaded_libs[idx], name) { - Some(addr) => addr.raw(), - None => u64::MAX, - } -} - -/// Describes every loaded module into `buf`, returning the required byte -/// length; nothing is written unless the whole answer fits, and the length -/// never lands in `SyscallError`'s encoded range. -pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 { - use toyos_abi::syscall::ModuleInfo; - let info_size = core::mem::size_of::(); - - // Laid out under the process's lock and copied out after it. - let laid = process::with_process_data(|data| { - let module_count = 1 + data.elf.loaded_libs.len(); - - let exe_path_bytes = data.exe_path.as_bytes(); - let total_path_bytes: usize = exe_path_bytes.len() - + data.elf.lib_paths.iter().map(|p| p.len()).sum::(); - - let required = module_count * info_size + total_path_bytes; - if out.len() < required { - return Err(required as u64); - } - let mut laid = alloc::vec![0u8; required]; - let mut put = |at: usize, bytes: &[u8]| laid[at..at + bytes.len()].copy_from_slice(bytes); - - // Record array ends at the first module's `path_offset`; paths are - // packed after it in module order. - let mut path_offset = (module_count * info_size) as u32; - - let (eh_addr, eh_size) = data.elf.exe_eh_frame_hdr; - let (phdr, phnum) = data.elf.exe_phdrs; - let exe_info = ModuleInfo { - base: data.elf.elf_base.raw(), - text_end: data.elf.exe_vaddr_max, - eh_frame_hdr: eh_addr, - eh_frame_hdr_size: eh_size, - phdr, - phnum: phnum.into(), - path_offset, - path_len: exe_path_bytes.len() as u32, - }; - put(0, toyos_abi::usersafe::bytes(&exe_info)); - put(path_offset as usize, exe_path_bytes); - path_offset += exe_path_bytes.len() as u32; - - for (i, lib) in data.elf.loaded_libs.iter().enumerate() { - let lib_path_bytes = if i < data.elf.lib_paths.len() { - data.elf.lib_paths[i].as_bytes() - } else { - b"" - }; - let lib_info = ModuleInfo { - base: lib.user_base.raw(), - text_end: lib.user_end(), - 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()), - phdr: (lib.user_base + lib.phdrs.image().start().get()).raw(), - phnum: lib.phdrs.count().into(), - path_offset, - path_len: lib_path_bytes.len() as u32, - }; - put((1 + i) * info_size, toyos_abi::usersafe::bytes(&lib_info)); - put(path_offset as usize, lib_path_bytes); - path_offset += lib_path_bytes.len() as u32; - } - Ok(laid) - }); - match laid { - Ok(laid) => match out.write_at(0, &laid) { - Ok(()) => laid.len() as u64, - Err(faulted) => SyscallError::from(faulted).to_u64(), - }, - Err(required) => required, - } -} ->>>>>>> origin/main diff --git a/lib/kimage/Cargo.toml b/lib/kimage/Cargo.toml index a87aadf14f0..04b99abc0ff 100644 --- a/lib/kimage/Cargo.toml +++ b/lib/kimage/Cargo.toml @@ -13,12 +13,5 @@ edition = "2021" license = "MIT OR Apache-2.0" publish = false -[features] -# The build's writer, which allocates. -write = [] - [dependencies] toyos-abi = { path = "../../sdk/abi" } - -[dev-dependencies] -toyos-kimage = { path = ".", features = ["write"] } diff --git a/lib/kimage/src/lib.rs b/lib/kimage/src/lib.rs index d9b79593aae..b809f0ca48a 100644 --- a/lib/kimage/src/lib.rs +++ b/lib/kimage/src/lib.rs @@ -27,12 +27,9 @@ #![no_std] #![forbid(unsafe_code)] -#[cfg(feature = "write")] extern crate alloc; -#[cfg(feature = "write")] mod write; -#[cfg(feature = "write")] pub use write::{Input, Symbol}; use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID, MAX_SPAWN_MAPS}; @@ -115,7 +112,7 @@ impl<'a> Kernel<'a> { let maps = take(bytes, &mut at, maps, MAP).ok_or(SHORT)?; let id_len = u64_at(program, 16).filter(|&n| n <= MAX_BUILD_ID as u64).ok_or("kernel image: a build-id past its bound")?; let image_at = usize::try_from(field(64)?).map_err(|_| SHORT)?; - let image = bytes.get(image_at..).filter(|_| image_at >= at && image_at as u64 % PAGE == 0).ok_or(SHORT)?; + let image = bytes.get(image_at..).filter(|_| image_at >= at && (image_at as u64).is_multiple_of(PAGE)).ok_or(SHORT)?; let kernel = Kernel { machine, entry, @@ -142,7 +139,7 @@ impl<'a> Kernel<'a> { let mut covered = 0; for range in self.ranges() { let range = range.ok_or("kernel image: a range with no protection")?; - if range.lo < covered || range.hi <= range.lo || range.hi > self.memory || range.lo % PAGE != 0 || range.hi % PAGE != 0 { + if range.lo < covered || range.hi <= range.lo || range.hi > self.memory || !range.lo.is_multiple_of(PAGE) || !range.hi.is_multiple_of(PAGE) { return Err("kernel image: ranges that are not ascending whole pages of its memory"); } covered = range.hi; @@ -152,7 +149,7 @@ impl<'a> Kernel<'a> { return Err("kernel image: an entry past its memory"); } let (lo, hi) = self.relro; - if lo > hi || hi > self.memory || lo % PAGE != 0 || hi % PAGE != 0 { + if lo > hi || hi > self.memory || !lo.is_multiple_of(PAGE) || !hi.is_multiple_of(PAGE) { return Err("kernel image: RELRO that is not whole pages of its memory"); } if self.relocations().any(|(offset, addend)| offset.checked_add(8).is_none_or(|end| end > self.memory) || addend > self.memory) { @@ -164,7 +161,7 @@ impl<'a> Kernel<'a> { /// Every range and its protection; `None` for a word that names none. pub fn ranges(&self) -> impl Iterator> + 'a { - self.ranges.chunks_exact(RANGE).map(|r| { + self.ranges.as_chunks::().0.iter().map(|r| { let (lo, hi) = (u64_at(r, 0)?, u64_at(r, 8)?); Some(Range { lo, hi, prot: VmProt::from_u64(u64_at(r, 16)?)? }) }) @@ -181,7 +178,7 @@ impl<'a> Kernel<'a> { /// `(offset, addend)`: the loader writes its base plus `addend` at `offset`. pub fn relocations(&self) -> impl Iterator + 'a { - self.relocations.chunks_exact(RELOCATION).filter_map(|r| Some((u64_at(r, 0)?, u64_at(r, 8)?))) + self.relocations.as_chunks::().0.iter().filter_map(|r| Some((u64_at(r, 0)?, u64_at(r, 8)?))) } } @@ -251,7 +248,7 @@ pub const SUPERVISOR_OBJECT: u64 = 0; impl Supervisor<'_> { /// Its ranges, `object` [`SUPERVISOR_OBJECT`] or `HANDLE_INVALID` for zeros. pub fn maps(&self) -> impl Iterator + '_ { - self.maps.chunks_exact(MAP).filter_map(|m| { + self.maps.as_chunks::().0.iter().filter_map(|m| { Some(SpawnMap { addr: u64_at(m, 0)?, len: u64_at(m, 8)?, From bb1e9ada6dce9301b8b1347d8f8ba190cb178fc3 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 19:59:23 +0100 Subject: [PATCH 10/13] A test image from memoized parts carries the kernel section too build_test_parts returned the memoized kernel ELF on a full memo hit, so the second boot of one config in a process signed an ELF into the slot's kernel section, which the loader refuses (not TOYOSKRN). Guest rows run 42080820355 red virt_random_differs's second boot so. Co-Authored-By: Claude Opus 5.5 --- build/build.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/build.rs b/build/build.rs index 7a3e8a2b21d..247d6821c69 100644 --- a/build/build.rs +++ b/build/build.rs @@ -2214,7 +2214,9 @@ pub fn build_test_image( ) } -/// The three parts one image is made of, each memoized for this process. +/// The three parts one image is made of: the kernel section +/// ([`crate::kimage`]) of the memoized kernel and userland, and the memoized +/// loader and userland. pub struct Parts { pub kernel: Arc>, pub bootloader: Arc>, @@ -2254,7 +2256,7 @@ pub fn build_test_parts( // staleness sweep to protect. let memo = (KERNEL.get(kernel_key), BOOTLOADER.get(bl_key), USERLAND.get(root_image_key)); if let (Some(kernel), Some(bootloader), Some(userland)) = memo { - return Parts { kernel, bootloader, userland }; + return Parts { kernel: Arc::new(crate::kimage::section(&kernel, &userland.supervisor, arch)), bootloader, userland }; } // A cache miss is shared setup, not a property of whichever test happened From bfb75f0567a69207e0db80c89dc9d5e6b60ed978 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 20:24:58 +0100 Subject: [PATCH 11/13] Record a desktop reopened as its session ends being handed the ended one Guest rows run 42089538985 stalled session_lifecycle so: the supervisor answered the compositor's ask with the desktop session whose owner had just exited, before it read the exit. Co-Authored-By: Claude Opus 5.5 --- ...ession-ends-is-handed-the-ended-session.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md diff --git a/issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md b/issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md new file mode 100644 index 00000000000..e56ecff6eb6 --- /dev/null +++ b/issues/a-desktop-reopened-as-its-session-ends-is-handed-the-ended-session.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-11-05 +--- + +# A desktop reopened as its session ends is handed the ended session + +When the desktop's session ends, the compositor asks the supervisor for a new +one at once (`system/compositor/src/signout.rs`). The supervisor answers the +screen's row with its open desktop session, one whose owner it does not yet +see as ended (`system/supervisor/src/sessions.rs`, `serve_login` through +`desktop_session` and `exit_of`). An ask that lands between the owner's exit +and the supervisor reading it is handed the ended session, whose control port +then answers `Gone`; the compositor says `no desktop session: … Gone; the next +launch asks again`, and opens none until something launches. + +Seen once, in guest rows run 42089538985 (`session_lifecycle`, QEMU x86-64, +`tests/images/sessioncase.toml`), which stalled at "the desktop to open +again": the session owner exited at 4.967 s, the supervisor said `session 8 is +signed out` at 4.973 s, and the compositor's refusal is stamped 4.973 s. The +row passed at that branch in runs 42018194698, 42040884795 and 42080820355. + +**Exit**: an ask the supervisor answers is never an ended session's, or the +compositor asks again when the one it was handed is gone; a host test of the +supervisor's decision with the owner's exit not yet read holds it. From 61152480a5123106923f7009ab846df0acd64d2e Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 21:01:46 +0100 Subject: [PATCH 12/13] Review r3: the slot's kernel is kernel.img, and a spawn refuses W+X - The slot's kernel file is `toyos/kernel.img`: it holds the kernel section (`toyos_kimage`), not an ELF. The build and the loader name it through `slots::KERNEL_FILE` alone; the comments that called the handed bytes an ELF say what they are. - `toyos::elf::program::lay_out` refuses a segment both writable and executable by name, as the loader refuses a library's: one policy, and either protection would only have made the program fault later. - `toyos_kimage` owns the `e_machine` numbers the build, the loader and the kernel share, and records the supervisor section's length, which the kernel checks against the section the loader handed it. - `abuse_spawn_maps` refuses a copy from no page boundary of its object, and one whose end wraps, which unchecked would read past the object in the kernel at the first touch. - The spawn record drops `layout=`, which timed nothing any more. - `toyos-symbols` drops its dead `object` dev-dependency, and the comments that still named `toyos-elf`, `lib/elf`, `crate::name`, raw-pointer symbol tables or `spawn_maps` say what is. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - build/kimage.rs | 5 +++-- kernel/src/arch/aarch64/mod.rs | 2 +- kernel/src/arch/x86_64/mod.rs | 2 +- kernel/src/loader/mod.rs | 8 +++----- kernel/src/main.rs | 4 ++++ lib/kimage/src/lib.rs | 13 +++++++++++-- lib/kimage/src/write.rs | 3 +++ lib/kimage/tests/round_trip.rs | 3 ++- lib/symbols/Cargo.toml | 3 --- lib/symbols/src/frame.rs | 5 +++-- lib/symbols/src/lib.rs | 7 ++++--- lib/update/src/slots.rs | 7 ++++--- loader/src/arch/aarch64.rs | 2 +- loader/src/arch/x86_64.rs | 4 ++-- loader/src/main.rs | 4 ++-- sdk/abi/src/boot.rs | 4 ++-- sdk/libc/arch/src/dladdr_symbols.rs | 2 +- sdk/libc/src/elfsym.rs | 2 +- sdk/toyos/src/dl/image/mod.rs | 4 ++-- sdk/toyos/src/dl/mod.rs | 2 +- sdk/toyos/src/elf/program.rs | 9 +++++---- sdk/toyos/tests/elf_note.rs | 2 +- sdk/toyos/tests/elf_program.rs | 10 +++++----- sdk/toyos/tests/elf_real.rs | 12 ++++++------ tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs | 4 ++++ 26 files changed, 72 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a670522b20..0b38dbf6c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4109,7 +4109,6 @@ dependencies = [ name = "toyos-symbols" version = "0.1.0" dependencies = [ - "object", "rustc-demangle", "toyos-abi", "toyos-elide", diff --git a/build/kimage.rs b/build/kimage.rs index 161b2b6479b..7785ac6f7bd 100644 --- a/build/kimage.rs +++ b/build/kimage.rs @@ -16,8 +16,8 @@ use crate::arch::Arch; fn machine(arch: Arch) -> (Machine, u16) { match arch { - Arch::X86_64 => (Machine::X86_64, 62), - Arch::Aarch64 => (Machine::Aarch64, 183), + Arch::X86_64 => (Machine::X86_64, toyos_kimage::EM_X86_64), + Arch::Aarch64 => (Machine::Aarch64, toyos_kimage::EM_AARCH64), } } @@ -93,6 +93,7 @@ pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { supervisor_base: program.image_base, supervisor_build_id: program.build_id().to_vec(), supervisor_maps: program.maps().to_vec(), + supervisor_len: supervisor.len() as u64, image, } .write() diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 90aa056a3b6..9b46c11bbb0 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -55,7 +55,7 @@ pub use irqchip::its as message_unit; pub use smmu as iommu_unit; /// `e_machine` of this kernel's image (`toyos_kimage::Kernel::machine`). -pub const E_MACHINE: u16 = 183; +pub const E_MACHINE: u16 = toyos_kimage::EM_AARCH64; /// Interrupts masked on this CPU for as long as the guard lives, and then put /// back as they were — restored, not enabled — so a guard nests inside a region diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 1e9bd063fa4..7eb387cd74f 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -67,7 +67,7 @@ pub use iommu_unit::interrupt as message_unit; pub use apic::MSI_DOORBELL; /// `e_machine` of this kernel's image (`toyos_kimage::Kernel::machine`). -pub const E_MACHINE: u16 = 62; +pub const E_MACHINE: u16 = toyos_kimage::EM_X86_64; /// Interrupts masked on this CPU for as long as the guard lives, and then put /// back as they were — restored, not enabled — so a guard nests inside a region diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 51db7c80786..dc7f2504d44 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -44,7 +44,7 @@ const USER_STACK_SIZE: usize = 8 * 1024 * 1024; const STACK_GUARD: u64 = 1024 * 1024; /// One range of a child's memory, its numbers checked where the request was -/// read (`syscall::proc::spawn_maps`): whole pages, and a copy's bytes inside +/// read (`syscall::proc::spawn_program`): whole pages, and a copy's bytes inside /// an object of ordinary memory. pub struct Map { pub at: UserAddr, @@ -173,7 +173,7 @@ pub fn spawn( let t0 = crate::clock::nanos_since_boot(); let Program { maps, entry, image_base, build_id } = program; - // Inside a range the child may execute, which `spawn_maps` placed in the user half. + // Inside a range the child may execute, which `spawn_program` placed in the user half. let executes = |map: &Map| map.prot == kernel::vm::Prot::ReadExec && (map.at.raw()..map.at.raw() + map.len).contains(&entry); let Some(entry) = maps.iter().any(executes).then(|| toyos_userbound::Entry::new(entry)).flatten() else { log!("spawn: {}: the entry {:#x} is in no executable range", path, entry); @@ -182,8 +182,6 @@ pub fn spawn( let start = maps.iter().map(|map| map.at.raw()).min().ok_or(SyscallError::InvalidArgument)?; let end = maps.iter().map(|map| map.at.raw() + map.len).max().ok_or(SyscallError::InvalidArgument)?; - let t1 = crate::clock::nanos_since_boot(); - let Some(child_pt) = crate::mm::space::Space::new_user(&job) else { log!("spawn: {}: no address space: every user PCID is held, or no frame is left for its root", path); return Err(SyscallError::ResourceExhausted.into()); @@ -287,7 +285,7 @@ pub fn spawn( crate::process::debug_hold_marked_spawn(parent, &object); let t3 = crate::clock::nanos_since_boot(); - log!("spawn: {} pid={} (layout={}ms total={}ms)", path, pid, (t1 - t0) / 1_000_000, (t3 - t0) / 1_000_000); + log!("spawn: {} pid={} (total={}ms)", path, pid, (t3 - t0) / 1_000_000); Ok((pid, held)) } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 2781a100365..a8e4538e8a3 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -318,6 +318,10 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { ); let kernel_image = toyos_kimage::Kernel::parse(kernel_section).unwrap_or_else(|why| panic!("boot: the loader's {why}")); assert_eq!(kernel_image.machine, arch::E_MACHINE, "boot: the loader's kernel image is another machine's"); + assert_eq!( + kernel_image.supervisor.len, kernel_args.supervisor_image.len, + "boot: the supervisor section is not the one the build laid the supervisor's ranges out of" + ); // After the boot's own lines, so its refusal reaches a channel that says // what machine this is, and while the loader's arguments are still mapped. random::key(loader_args, &mut kernel_args); diff --git a/lib/kimage/src/lib.rs b/lib/kimage/src/lib.rs index b809f0ca48a..5752a73e5a9 100644 --- a/lib/kimage/src/lib.rs +++ b/lib/kimage/src/lib.rs @@ -15,7 +15,8 @@ //! symbols [addr u64 | size u64 | name u32 | len u32], ascending by addr //! names the symbols' names //! supervisor entry u64 | image_base u64 | build-id len u64 | build-id [32] | maps u64 -//! [SpawnMap], `object` 0 for the supervisor section +//! | section len u64, then [SpawnMap], `object` 0 for the supervisor +//! section, which was `section len` bytes when it was laid out //! image at image_at, a page boundary: the image's bytes from address 0, //! zeros past them to `memory` //! ``` @@ -35,12 +36,16 @@ pub use write::{Input, Symbol}; use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID, MAX_SPAWN_MAPS}; pub const MAGIC: [u8; 8] = *b"TOYOSKRN"; +/// `e_machine` of each machine a kernel image is built for: what the build +/// writes and the loader and the kernel each check is their own. +pub const EM_X86_64: u16 = 62; +pub const EM_AARCH64: u16 = 183; pub const FORMAT: u32 = 1; const HEADER: usize = 72; const RANGE: usize = 24; const RELOCATION: usize = 16; const SYMBOL: usize = 24; -const PROGRAM: usize = 64; +const PROGRAM: usize = 72; const MAP: usize = core::mem::size_of::(); pub const PAGE: u64 = 4096; @@ -125,6 +130,7 @@ impl<'a> Kernel<'a> { entry: u64_at(program, 0).ok_or(SHORT)?, image_base: u64_at(program, 8).ok_or(SHORT)?, build_id: &program[24..24 + id_len as usize], + len: u64_at(program, 64).ok_or(SHORT)?, maps, }, image, @@ -239,6 +245,9 @@ pub struct Supervisor<'a> { pub entry: u64, pub image_base: u64, pub build_id: &'a [u8], + /// Bytes of the supervisor section the build laid out: a section of + /// other bytes is another program, which these ranges do not describe. + pub len: u64, maps: &'a [u8], } diff --git a/lib/kimage/src/write.rs b/lib/kimage/src/write.rs index 0093ba38d89..38b372d539c 100644 --- a/lib/kimage/src/write.rs +++ b/lib/kimage/src/write.rs @@ -30,6 +30,8 @@ pub struct Input { pub supervisor_base: u64, pub supervisor_build_id: Vec, pub supervisor_maps: Vec, + /// Bytes of the supervisor section the maps were laid out of. + pub supervisor_len: u64, /// The image's bytes from address 0. pub image: Vec, } @@ -78,6 +80,7 @@ impl Input { id[..self.supervisor_build_id.len()].copy_from_slice(&self.supervisor_build_id); out.extend(id); out.extend((self.supervisor_maps.len() as u64).to_le_bytes()); + out.extend(self.supervisor_len.to_le_bytes()); for map in &self.supervisor_maps { for word in [map.addr, map.len, map.object, map.offset, map.copied, map.prot] { out.extend(word.to_le_bytes()); diff --git a/lib/kimage/tests/round_trip.rs b/lib/kimage/tests/round_trip.rs index dbc4f1443c3..16742be141c 100644 --- a/lib/kimage/tests/round_trip.rs +++ b/lib/kimage/tests/round_trip.rs @@ -21,6 +21,7 @@ fn input() -> Input { supervisor_base: 0x2_0000_0000, supervisor_build_id: vec![0x5a; 20], supervisor_maps: vec![SpawnMap { addr: 0x2_0000_0000, len: 0x1000, object: 0, offset: 0, copied: 0x800, prot: VmProt::ReadExec as u64 }], + supervisor_len: 0x1234, image: vec![0xcc; 0x2100], } } @@ -33,7 +34,7 @@ fn a_section_reads_back_as_written() { assert_eq!(kernel.text().unwrap().lo, 0x1000); assert_eq!(kernel.relocations().collect::>(), [(0x2000, 0x1010), (0x2008, 0)]); assert_eq!(kernel.image, &[0xcc; 0x2100][..]); - assert_eq!((kernel.supervisor.entry, kernel.supervisor.build_id), (0x2_0000_1000, &[0x5a; 20][..])); + assert_eq!((kernel.supervisor.entry, kernel.supervisor.build_id, kernel.supervisor.len), (0x2_0000_1000, &[0x5a; 20][..], 0x1234)); assert_eq!(kernel.supervisor.maps().collect::>(), input().supervisor_maps); } diff --git a/lib/symbols/Cargo.toml b/lib/symbols/Cargo.toml index 3fa9a370143..1e6dc65e4eb 100644 --- a/lib/symbols/Cargo.toml +++ b/lib/symbols/Cargo.toml @@ -17,6 +17,3 @@ rustc-demangle = "0.1" toyos-abi = { path = "../../sdk/abi" } toyos-elide = { path = "../elide" } -[dev-dependencies] -# unaligned: an include_bytes! fixture is aligned to 1, and object reads a header in place. -object = { version = "0.38", default-features = false, features = ["read_core", "elf", "std", "unaligned"] } diff --git a/lib/symbols/src/frame.rs b/lib/symbols/src/frame.rs index 5084d6a30ea..41d0ce90bd0 100644 --- a/lib/symbols/src/frame.rs +++ b/lib/symbols/src/frame.rs @@ -1,9 +1,10 @@ //! A killed program's frame, as the kernel records it and userland reads it //! back: ` + id=`. //! -//! The kernel names no user frame: it records the file it opened, the offset +//! The kernel names no user frame: it records the program's file, the offset //! of the instruction in flight in that file's own addresses, and the build-id -//! the file carries, and [`crate::name`] names the function from them. One +//! the parent handed the spawn (`SpawnArgs::build_id_ptr`), and +//! `/system/bin/symbolize` names the function from them. One //! encoder ([`UserFrame`]'s `Display`) and one decoder ([`decode`]), so the //! grammar has no second spelling. //! diff --git a/lib/symbols/src/lib.rs b/lib/symbols/src/lib.rs index 1606848cb72..a63bee91bb1 100644 --- a/lib/symbols/src/lib.rs +++ b/lib/symbols/src/lib.rs @@ -4,9 +4,10 @@ //! symbols or build-id is `toyos::elf`'s, which the kernel does not link. //! //! `kernel/src/symbols.rs` keeps the rest of the kernel's own frames — a -//! `SymbolTable` holding raw pointers into the kernel image, because the -//! resolve path is reached from the fault handler and the panic handler and -//! may not allocate, take a lock or do I/O. Nothing here needs any of that: +//! `SymbolTable` over the build's table in the kernel section +//! (`toyos_kimage::Symbols`), because the resolve path is reached from the +//! fault handler and the panic handler and may not allocate, take a lock or do +//! I/O. Nothing here needs any of that: //! every function is of the bytes a caller already has. //! //! `no_std`, no allocation, no `unsafe`, and no panic on any input: the kernel diff --git a/lib/update/src/slots.rs b/lib/update/src/slots.rs index 195c8735591..d12fea7e390 100644 --- a/lib/update/src/slots.rs +++ b/lib/update/src/slots.rs @@ -31,9 +31,10 @@ const BODY_BYTES: usize = 8 + 4 + 4 + 8 + 2 * SLOT_BYTES; /// A copy's bytes, checksum included; the rest of its block is zero. pub const TABLE_BYTES: usize = BODY_BYTES + 4; -/// Where each slot's kernel, boot parameter, two programs and signed header -/// are on its FAT partition, as paths from the volume's root. -pub const KERNEL_FILE: &str = "toyos/kernel.elf"; +/// Where each slot's kernel section (`toyos_kimage`), boot parameter, two +/// programs and signed header are on its FAT partition, as paths from the +/// volume's root. +pub const KERNEL_FILE: &str = "toyos/kernel.img"; pub const CMDLINE_FILE: &str = "toyos/cmdline"; pub const SUPERVISOR_FILE: &str = "toyos/supervisor.elf"; pub const FILESERVER_FILE: &str = "toyos/fileserver.elf"; diff --git a/loader/src/arch/aarch64.rs b/loader/src/arch/aarch64.rs index 8a7a4ad9baf..7276b3c7d36 100644 --- a/loader/src/arch/aarch64.rs +++ b/loader/src/arch/aarch64.rs @@ -4,7 +4,7 @@ use toyos_abi::boot::KernelArgs; use toyos_bootmap::Typing; /// `e_machine` of the kernel this loader starts (`toyos_kimage::Kernel::machine`). -pub const E_MACHINE: u16 = 183; +pub const E_MACHINE: u16 = toyos_kimage::EM_AARCH64; /// How the boot map's descriptors are encoded. pub use toyos_bootmap::aarch64 as encoding; diff --git a/loader/src/arch/x86_64.rs b/loader/src/arch/x86_64.rs index de3b7fd7435..4f3882df802 100644 --- a/loader/src/arch/x86_64.rs +++ b/loader/src/arch/x86_64.rs @@ -3,7 +3,7 @@ use toyos_abi::boot::KernelArgs; /// `e_machine` of the kernel this loader starts (`toyos_kimage::Kernel::machine`). -pub const E_MACHINE: u16 = 62; +pub const E_MACHINE: u16 = toyos_kimage::EM_X86_64; /// How the boot map's entries are encoded. pub use toyos_bootmap::x86_64 as encoding; @@ -133,7 +133,7 @@ pub unsafe fn enter_kernel(image: (u64, u64), entry_offset: u64, args: &KernelAr // SAFETY: the caller's contract: the switch keeps this code and stack // mapped, and the jump lands in the image it mapped. unsafe { core::arch::asm!("mov cr3, {}", in(reg) root, options(nostack)) }; - // SAFETY: `kernel.elf`'s entry point takes `&KernelArgs` in `rdi` by the boot + // SAFETY: the kernel image's entry point takes `&KernelArgs` in `rdi` by the boot // protocol `toyos-abi::boot` and the kernel side of it define between them — // `sysv64`, because this target's own `"C"` is the Microsoft convention — // and this bootloader has no way to check the callee's signature, only to diff --git a/loader/src/main.rs b/loader/src/main.rs index f9cd4c9d89b..1288247d498 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -456,7 +456,7 @@ fn report_reach(what: &str, at: u64, len: u64) { ); } -/// What the chosen slot hands the kernel beside its ELF and parameter. +/// What the chosen slot hands the kernel beside its own image and parameter. struct Handed { root: rootimage::RootImage, supervisor: payload::Payload, @@ -890,7 +890,7 @@ fn main(handle: Handle, system_table: SystemTable) -> Status { println!("Log partition: signature {:02x?}", log_guid); - // Every byte the kernel is handed below — its ELF, its parameter, the two + // Every byte the kernel is handed below — its image, its parameter, the two // programs and ROOT — is one the chosen slot's signed header names. ROOT is read before the // kernel is loaded and not after: it is the allocation the image pages come // from, and a slot whose ROOT is refused has no use for the kernel's. diff --git a/sdk/abi/src/boot.rs b/sdk/abi/src/boot.rs index 5ffffc85c25..a4fdf5e6ab1 100644 --- a/sdk/abi/src/boot.rs +++ b/sdk/abi/src/boot.rs @@ -48,8 +48,8 @@ pub struct KernelArgs { /// No presence flag, unlike the boot partition above, and not because the /// state cannot arise but because it is not a machine. A machine really can /// have no boot partition to be named — PXE, an unpartitioned disk. But - /// this GUID comes from a file `create_esp_volume` writes beside - /// `kernel.elf`, so a volume carrying that one and not this one was not + /// this GUID comes from a file `create_esp_volume` writes beside the + /// loader, so a volume carrying that one and not this one was not /// built by this project, and the bootloader refuses it by name rather than /// starting a kernel that would silently have nowhere to put its log. /// diff --git a/sdk/libc/arch/src/dladdr_symbols.rs b/sdk/libc/arch/src/dladdr_symbols.rs index a75fedd8e2b..f2b7fa5ab1c 100644 --- a/sdk/libc/arch/src/dladdr_symbols.rs +++ b/sdk/libc/arch/src/dladdr_symbols.rs @@ -1,5 +1,5 @@ //! `dladdr`'s walk of an image laid out here: the segment holding an address, -//! the image's first byte, and the tables it hands `toyos-elf`'s choice, as +//! the image's first byte, and the tables it hands `toyos::elf`'s choice, as //! many symbols as `.gnu.hash` describes, or with none as the gap to the //! string table holds. diff --git a/sdk/libc/src/elfsym.rs b/sdk/libc/src/elfsym.rs index 4094f808e02..1cd2c056283 100644 --- a/sdk/libc/src/elfsym.rs +++ b/sdk/libc/src/elfsym.rs @@ -1,5 +1,5 @@ //! What `dladdr` reads of a loaded image: whether an address lies in it, where -//! its first mapped byte is, and the tables `toyos-elf` chooses its symbol +//! its first mapped byte is, and the tables `toyos::elf` chooses its symbol //! from, each a slice of what the image maps. It reads nothing but the image //! it is handed, so the host tests it on an image it lays out //! (`toyos-libc-copies`). diff --git a/sdk/toyos/src/dl/image/mod.rs b/sdk/toyos/src/dl/image/mod.rs index 97e615f9da1..fe4ee0870bc 100644 --- a/sdk/toyos/src/dl/image/mod.rs +++ b/sdk/toyos/src/dl/image/mod.rs @@ -1,10 +1,10 @@ //! An image, as the loader lays it out and relocates it: every table it -//! reads, through `toyos-elf`, out of the image's own mapped bytes. It touches +//! reads, through [`crate::elf`], out of the image's own mapped bytes. It touches //! nothing but the memory it is handed, so the host tests it on images it lays //! out. //! //! An image is the process's own input: a crafted one corrupts only the -//! process that loads it, and is refused by name wherever `toyos-elf` or the +//! process that loads it, and is refused by name wherever [`crate::elf`] or the //! rules here can tell. #[cfg(test)] diff --git a/sdk/toyos/src/dl/mod.rs b/sdk/toyos/src/dl/mod.rs index 21a87b892ac..5e4d24ce700 100644 --- a/sdk/toyos/src/dl/mod.rs +++ b/sdk/toyos/src/dl/mod.rs @@ -49,7 +49,7 @@ pub enum Refused { Read(String, ImageRefused), /// No memory to lay a library out in. Memory(SyscallError), - /// An image `toyos-elf` or the loader's own rules refuse. + /// An image [`crate::elf`] or the loader's own rules refuse. Elf(&'static str), /// A relocation the loader does not apply. Relocation(RelocError), diff --git a/sdk/toyos/src/elf/program.rs b/sdk/toyos/src/elf/program.rs index a0370763429..d47139b738c 100644 --- a/sdk/toyos/src/elf/program.rs +++ b/sdk/toyos/src/elf/program.rs @@ -44,10 +44,8 @@ impl Program { /// at `at`, each `PT_LOAD` copied from `object`, the memory object holding /// `file`. Refused by name where the kernel would refuse a range, or the /// program could not start: no `PT_LOAD` maps its program header table, which -/// it reads to relocate itself. -/// -/// A segment both writable and executable is mapped `ReadExec`: taking the -/// execution away would let a hostile file run as data instead. +/// it reads to relocate itself, or a segment asks to be written and executed, +/// which no page is, as the loader refuses a library's (`crate::dl`). pub fn lay_out(file: &[u8], machine: Machine, object: u64, at: u64) -> Result { let layout = Layout::parse(file, machine).map_err(|e| e.as_str())?; if layout.program_headers().is_none() { @@ -80,6 +78,9 @@ pub fn lay_out(file: &[u8], machine: Machine, object: u64, at: u64) -> Result Option> { /// Ours, and `object`'s on the same bytes, which must agree. fn agreed(notes: &[u8], align: u64) -> Option> { let ours = note::build_id(notes, align).map(<[u8]>::to_vec); - assert_eq!(ours, objects(notes, align), "toyos-elf and object disagree on {notes:02x?}"); + assert_eq!(ours, objects(notes, align), "toyos::elf and object disagree on {notes:02x?}"); ours } diff --git a/sdk/toyos/tests/elf_program.rs b/sdk/toyos/tests/elf_program.rs index d9e178469b1..0fc3b733883 100644 --- a/sdk/toyos/tests/elf_program.rs +++ b/sdk/toyos/tests/elf_program.rs @@ -38,7 +38,7 @@ fn lld_images_are_laid_out_as_object_reads_them() { let lo = segment.address() & !0xfff; let hi = (segment.address() + segment.size()).next_multiple_of(0x1000); let prot = match p_flags & 3 { - 1 | 3 => VmProt::ReadExec, + 1 => VmProt::ReadExec, 2 => VmProt::ReadWrite, _ => VmProt::Read, }; @@ -75,12 +75,12 @@ fn the_image_moves_with_its_base() { ); } -/// A segment both writable and executable is mapped `ReadExec`: no range a -/// spawn asks for is both. +/// A segment both writable and executable is refused by name, as the loader +/// refuses a library's: no page is both. #[test] -fn a_writable_executable_segment_runs_but_is_not_written() { +fn a_writable_executable_segment_is_refused() { let file = Elf::new(0x2000).ph(Phdr::load(0, 0, 0x2000, 0x2000, PF_R | PF_W | PF_X)).build(); - assert_eq!(lay_out(&file, Machine::X86_64, OBJECT, BASE).unwrap().maps()[0].prot, VmProt::ReadExec as u64); + assert_eq!(lay_out(&file, Machine::X86_64, OBJECT, BASE), Err("ELF: a PT_LOAD is both writable and executable")); } /// A segment's copy stops at the file's end: what the file does not hold is diff --git a/sdk/toyos/tests/elf_real.rs b/sdk/toyos/tests/elf_real.rs index 404844d861f..5f4d5fcd98d 100644 --- a/sdk/toyos/tests/elf_real.rs +++ b/sdk/toyos/tests/elf_real.rs @@ -1,13 +1,13 @@ //! The positive control: the parser agrees with a real artifact. //! -//! Every other test in this crate hands the parser bytes no linker would emit, -//! and a parser that refused everything would pass all of them. The fixture is -//! the first 4 KiB of `/system/bin/shell` as rust-lld linked it with -//! `--build-id` — which is all [`Layout::parse`] ever reads — so this is the -//! shape the loader actually meets at every boot. +//! Every other test of this module hands the parser bytes no linker would +//! emit, and a parser that refused everything would pass all of them. The +//! fixture is the first 4 KiB of `/system/bin/shell` as rust-lld linked it +//! with `--build-id` — which is all [`Layout::parse`] ever reads — so this is +//! the shape a spawn's layout meets for every program a parent starts. //! //! Refresh it with `dd if=target/x86_64-unknown-toyos/toyos/shell -//! of=lib/elf/tests/fixtures/lld-headers.bin bs=4096 count=1`, and expect +//! of=sdk/toyos/tests/elf/fixtures/lld-headers.bin bs=4096 count=1`, and expect //! every number below to move: each is `readelf -hlnW` of the fixture. use toyos::elf::{Layout, Machine}; diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs index 23c504b0d84..e257d318d6a 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs @@ -82,6 +82,10 @@ fn main() { refused("more bytes copied than the range holds", with(&|m| m[0].copied = m[0].len + 1), laid.entry, invalid); refused("a copy past the object's end", with(&|m| m[0].offset = 0x4000_0000), laid.entry, invalid); refused("a copy from no object", with(&|m| m[0].object = NONE), laid.entry, invalid); + refused("a copy from no page boundary of the object", with(&|m| m[text].offset += 8), laid.entry, invalid); + // Unchecked, the sum wraps past the object's size and the first touch + // reads past the object's end in the kernel. + refused("a copy whose end wraps", with(&|m| (m[text].offset, m[text].copied) = (0xffff_ffff_ffff_f000, 0x1000)), laid.entry, invalid); refused("a range on the clock page", with(&|m| m.push(SpawnMap { addr: toyos_abi::clock::CLOCK_PAGE, len: 0x1000, object: NONE, ..SpawnMap::default() })), laid.entry, invalid); refused("two ranges that meet", with(&|m| m.push(m[0])), laid.entry, invalid); refused("an entry in no executable range", honest.clone(), honest[0].addr + honest[0].len - 8, invalid); From 842f5950b7bfefd8f8ee251c5b546526912b9c96 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 5 Nov 2026 22:27:48 +0100 Subject: [PATCH 13/13] Review r4: the wrap case's stake, and records that name what is - `abuse_spawn_maps`' wrapping copy would panic the kernel at its first touch, where `SharedMemObject` bounds every read: the comment says so. - The loader track's stage 5 names `load_kernel` and the kernel section's parse, not the `load_kernel_elf` this change replaced. - `build::kimage`'s refusals say "the kernel's ELF", which names no file. - `toyos_kimage::EM_*`'s comment parses. Co-Authored-By: Claude Opus 5.5 --- build/kimage.rs | 20 +++++++++---------- ...oes-only-what-must-precede-the-handover.md | 5 +++-- lib/kimage/src/lib.rs | 2 +- .../rust-tests/src/bin/abuse_spawn_maps.rs | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/build/kimage.rs b/build/kimage.rs index 7785ac6f7bd..8e49a71aac1 100644 --- a/build/kimage.rs +++ b/build/kimage.rs @@ -24,21 +24,21 @@ fn machine(arch: Arch) -> (Machine, u16) { /// The section of `kernel` and the supervisor `supervisor`, both for `arch`. pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { let (machine, e_machine) = machine(arch); - let layout = Layout::parse(kernel, machine).unwrap_or_else(|e| panic!("kernel.elf: {e}")); + let layout = Layout::parse(kernel, machine).unwrap_or_else(|e| panic!("the kernel's ELF: {e}")); let extent = layout.extent(); // The image sits at its own addresses: an image offset is moved by the // extent's start, which the kernel links at a page boundary. let min = extent.min(); - assert!(min.is_multiple_of(PAGE), "kernel.elf: its lowest address {min:#x} is no page boundary"); + assert!(min.is_multiple_of(PAGE), "the kernel's ELF: its lowest address {min:#x} is no page boundary"); if let Some((a, b)) = layout.overlapping_load_pages(PAGE) { - panic!("kernel.elf: PT_LOAD segments {a} and {b} share a page, which cannot have both their protections"); + panic!("the kernel's ELF: PT_LOAD segments {a} and {b} share a page, which cannot have both their protections"); } let memory = extent.max().next_multiple_of(PAGE); let mut image = Vec::new(); let mut ranges = Vec::new(); for segment in layout.segments() { let bytes = file_range(kernel, segment.file_offset(), segment.filesz()) - .expect("kernel.elf: a PT_LOAD's file bytes are past the file's end"); + .expect("the kernel's ELF: a PT_LOAD's file bytes are past the file's end"); let at = (min + segment.image().start().get()) as usize; if image.len() < at + bytes.len() { image.resize(at + bytes.len(), 0); @@ -46,7 +46,7 @@ pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { image[at..at + bytes.len()].copy_from_slice(bytes); let (lo, hi) = segment.page_range(PAGE); let flags = segment.flags(); - assert!(!(flags.executable() && flags.writable()), "kernel.elf: a segment both writable and executable"); + assert!(!(flags.executable() && flags.writable()), "the kernel's ELF: a segment both writable and executable"); let prot = if flags.executable() { VmProt::ReadExec } else if flags.writable() { @@ -65,17 +65,17 @@ pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { .section_headers() .and_then(|table| file_range(kernel, table.file_offset, table.byte_len() as u64)) .map(SectionTable::new) - .expect("kernel.elf: no section header table inside the file"); - let rela_sections = sections.rela_sections().unwrap_or_else(|form| panic!("kernel.elf: {form} is not supported")); + .expect("the kernel's ELF: no section header table inside the file"); + let rela_sections = sections.rela_sections().unwrap_or_else(|form| panic!("the kernel's ELF: {form} is not supported")); let rules = Rules { extent, window: (0, memory), tls: None }; let mut relocations = Vec::new(); for section in rela_sections { - let table = file_range(kernel, section.offset, section.size).expect("kernel.elf: SHT_RELA section is past the file's end"); + let table = file_range(kernel, section.offset, section.size).expect("the kernel's ELF: SHT_RELA section is past the file's end"); for raw in RelaTable::new(table, machine).iter() { - match rela::parse(raw, &rules, SymTab::empty()).unwrap_or_else(|e| panic!("kernel.elf: {e}")).map(|r| (r.offset(), r.op())) { + match rela::parse(raw, &rules, SymTab::empty()).unwrap_or_else(|e| panic!("the kernel's ELF: {e}")).map(|r| (r.offset(), r.op())) { Some((offset, Op::Relative(target))) => relocations.push((offset, min + target.get())), None => {} - Some(other) => panic!("kernel.elf: a relocation the loader does not apply: {other:?}"), + Some(other) => panic!("the kernel's ELF: a relocation the loader does not apply: {other:?}"), } } } diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 28723f65726..48f7f635fdf 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -209,8 +209,9 @@ Each stage lands on its own, in this order. held before. - Any failure after `verify` in `start_kernel` that the image causes is a refusal of that slot, like a bad signature. Stage 5's PR lists which - `start_kernel` refusals count as the image's — every `load_kernel_elf` - refusal of the kernel's bytes — and which count as the loader's own. + `start_kernel` refusals count as the image's — every `load_kernel` + refusal of the kernel section (`toyos_kimage::Kernel::parse`) — and + which count as the loader's own. - The health gate: `system.toml`'s `[boot] up` names the services that make the image up. The tree has no readiness signal (a swap's probation only asks whether a process still runs), so this stage adds the smallest diff --git a/lib/kimage/src/lib.rs b/lib/kimage/src/lib.rs index 5752a73e5a9..672cc23f8c0 100644 --- a/lib/kimage/src/lib.rs +++ b/lib/kimage/src/lib.rs @@ -37,7 +37,7 @@ use toyos_abi::syscall::{SpawnMap, VmProt, MAX_BUILD_ID, MAX_SPAWN_MAPS}; pub const MAGIC: [u8; 8] = *b"TOYOSKRN"; /// `e_machine` of each machine a kernel image is built for: what the build -/// writes and the loader and the kernel each check is their own. +/// writes, and what the loader and the kernel each check against their own. pub const EM_X86_64: u16 = 62; pub const EM_AARCH64: u16 = 183; pub const FORMAT: u32 = 1; diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs index e257d318d6a..55fde282bf9 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs @@ -84,7 +84,7 @@ fn main() { refused("a copy from no object", with(&|m| m[0].object = NONE), laid.entry, invalid); refused("a copy from no page boundary of the object", with(&|m| m[text].offset += 8), laid.entry, invalid); // Unchecked, the sum wraps past the object's size and the first touch - // reads past the object's end in the kernel. + // panics the kernel (`SharedMemObject`'s bound on every read). refused("a copy whose end wraps", with(&|m| (m[text].offset, m[text].copied) = (0xffff_ffff_ffff_f000, 0x1000)), laid.entry, invalid); refused("a range on the clock page", with(&|m| m.push(SpawnMap { addr: toyos_abi::clock::CLOCK_PAGE, len: 0x1000, object: NONE, ..SpawnMap::default() })), laid.entry, invalid); refused("two ranges that meet", with(&|m| m.push(m[0])), laid.entry, invalid);