From bd903f92927ccc4e7a83cf1e2c77ac5237bda928 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 00:03:38 +0200 Subject: [PATCH 01/11] A child's arguments and environment are memory its parent lays out The kernel copied a spawn's argv and environment, wrote the argv onto the child's stack, kept the environment for the child's life and answered it through SYS_GET_ENV. None of it was the kernel's to read. A spawn is already the parent handing the kernel ranges to map, so what a parent tells its child is now one of them: a startup block (toyos::startup) of argc, the argv and envp pointers and their strings, which the parent writes into a memory object and lays out directly below the program. Every copy of the SDK in the child reads it at that one address, so a library's own std sees the arguments and the environment too. PWD, the starting directory, travels in it as before. The kernel loses SpawnArgs' argv and env fields, SYS_GET_ENV, ProcessData::env, the stack block it wrote and the charge for copying them; AddressSpace::fill and wrote_code, whose one caller was that stack write; and the working directory's MAX_PATH charge #1182 left behind. A child's stack starts empty. A process's name is the last component of its path, which the spawn's record and a crash's frames already name the file by: the kernel keeps that one label and no other text of the child. lay_out lays every program at BASE with an empty block below it, so the supervisor, which the build lays out and the kernel starts, and a raw spawn that says nothing start with no argument and no environment. Program::starting_with names a block in its place. The block's pages and the reserved ones above them keep a program's ranges one span, which a crash record's frames are judged against. Co-Authored-By: Claude Fable 5.2 --- build/kimage.rs | 4 +- docs/src/architecture/kernel.md | 4 +- docs/src/using/interface.md | 13 +- issues/a-spawn-drops-an-empty-argument.md | 22 +-- ...-small-interrupts-post-and-threads-wait.md | 12 +- kernel/src/arch/aarch64/paging.rs | 7 - kernel/src/arch/x86_64/paging.rs | 5 - kernel/src/loader/mod.rs | 57 ++---- kernel/src/loader/start.rs | 5 +- kernel/src/mm/space.rs | 46 ----- kernel/src/process.rs | 13 +- kernel/src/syscall/dispatch.rs | 40 +---- kernel/src/syscall/proc.rs | 4 +- sdk/abi/src/syscall.rs | 19 +- sdk/libc/src/arch/aarch64.rs | 9 +- sdk/libc/src/arch/x86_64.rs | 8 +- sdk/libc/src/env.rs | 15 +- sdk/libc/src/lib.rs | 7 +- sdk/libc/src/misc.rs | 6 +- sdk/libc/src/spawn.rs | 4 +- sdk/std/sys/args.rs | 26 +-- sdk/std/sys/env.rs | 20 +-- sdk/std/sys/pal/mod.rs | 20 +-- sdk/std/sys/process.rs | 2 +- sdk/toyos/src/elf/program.rs | 53 ++++-- sdk/toyos/src/files.rs | 28 +-- sdk/toyos/src/files/owner.rs | 5 +- sdk/toyos/src/launch.rs | 5 +- sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/spawn.rs | 25 +-- sdk/toyos/src/startup.rs | 169 ++++++++++++++++++ sdk/toyos/tests/elf_program.rs | 32 ++-- .../rust-tests/src/bin/abuse_handle_table.rs | 22 +-- .../rust-tests/src/bin/abuse_page_straddle.rs | 14 +- .../rust-tests/src/bin/abuse_spawn_argv.rs | 109 ----------- .../rust-tests/src/bin/abuse_spawn_maps.rs | 15 +- .../rust-tests/src/bin/abuse_spawn_path.rs | 57 ++++++ .../guest/rust-tests/src/bin/process_tree.rs | 6 +- .../src/bin/spawn_child_ends_first.rs | 6 +- .../rust-tests/src/bin/spawn_image_object.rs | 12 +- .../rust-tests/src/bin/spawn_lands_claimed.rs | 6 +- .../src/bin/start_refuses_itself.rs | 6 +- .../guest/rust-tests/src/bin/startup_block.rs | 86 +++++++++ tests/guest/rust-tests/src/image.rs | 30 +++- 44 files changed, 543 insertions(+), 512 deletions(-) create mode 100644 sdk/toyos/src/startup.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs create mode 100644 tests/guest/rust-tests/src/bin/abuse_spawn_path.rs create mode 100644 tests/guest/rust-tests/src/bin/startup_block.rs diff --git a/build/kimage.rs b/build/kimage.rs index 8e49a71aac1..2558109b98c 100644 --- a/build/kimage.rs +++ b/build/kimage.rs @@ -4,7 +4,7 @@ //! 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::program::lay_out; use toyos::elf::rela::{self, Op, RelaTable, Rules}; use toyos::elf::section::{symbol_tables, SectionTable}; use toyos::elf::sym::{SymTab, STT_FUNC}; @@ -80,7 +80,7 @@ pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec { } } - let program = lay_out(supervisor, machine, SUPERVISOR_OBJECT, BASE).unwrap_or_else(|why| panic!("supervisor: {why}")); + let program = lay_out(supervisor, machine, SUPERVISOR_OBJECT).unwrap_or_else(|why| panic!("supervisor: {why}")); Input { machine: e_machine, entry: min + layout.entry().get(), diff --git a/docs/src/architecture/kernel.md b/docs/src/architecture/kernel.md index 6495d82bab9..8246e96ade1 100644 --- a/docs/src/architecture/kernel.md +++ b/docs/src/architecture/kernel.md @@ -15,7 +15,9 @@ The kernel is small, fast and safe, and does what userland cannot: It holds no filesystem, no storage, input, network, audio or USB driver, no network protocol, no audio mixing, no login and no naming policy, and parses -nothing one program writes for another. The firmware's AML is interpreted by a +nothing one program writes for another. What a parent tells the child it +starts, its arguments and environment, is memory the parent lays out in the +child: the kernel keeps none of it and knows a process by a label. The firmware's AML is interpreted by a userland ACPI service, which tells the kernel the little it needs, such as how to power off. diff --git a/docs/src/using/interface.md b/docs/src/using/interface.md index 8e2310616b0..e4503e4406d 100644 --- a/docs/src/using/interface.md +++ b/docs/src/using/interface.md @@ -22,7 +22,7 @@ Every system call is one of these families: | Own address space | Reserve, commit, map, protect and release memory | | Handles | Duplicate with narrower rights, close, move | | Waiting | Watch any set of objects for readiness, and wait | -| Processes | Spawn from an image with endowments; wait for and stop a child | +| Processes | Spawn from memory ranges the parent lays out, with endowments; wait for and stop a child | | Communication | Pipes, service ports, connections carrying bytes and handles, inboxes | | Shared and paged memory | Create, map and lend memory; supply pages as a pager | | Devices | Claim through the system capability, map device memory, grant DMA, wait for interrupts | @@ -62,9 +62,18 @@ userland library over the SDK. endowments ──> standard streams (descriptors 0, 1, 2 and those named) ──> a namespace of services it may connect to ──> directory capabilities: its view - ──> environment: HOME, the starting directory, ... ──> anything else its row or its parent gave it + + its memory ──> its program, in the ranges its parent laid out + ──> a startup block below it: its arguments and its + environment (HOME, the starting directory, ...) ``` +A parent reads the child's program and lays out the child's memory. What it +tells the child goes in the startup block, which the kernel copies as it +copies the program and never reads; the SDK, std and libc read it where it +lies. A child whose parent wrote none starts with no argument and an empty +environment. + A program holds nothing it was not given. It asks for more only by connecting to a service in its namespace, whose server decides. diff --git a/issues/a-spawn-drops-an-empty-argument.md b/issues/a-spawn-drops-an-empty-argument.md index d9c150ecff9..bd39d411a11 100644 --- a/issues/a-spawn-drops-an-empty-argument.md +++ b/issues/a-spawn-drops-an-empty-argument.md @@ -4,28 +4,28 @@ kind: defect opened: 2026-10-20 --- -# A spawn drops an empty argument and refuses one that is not UTF-8 +# A spawn drops an empty argument -`SYS_SPAWN` reads `argv` as one `NUL`-separated string with `user_str`, which -refuses bytes that are not UTF-8 (`InvalidArgument`), and splits it with -`text.split('\0').filter(|s| !s.is_empty())` (`kernel/src/syscall/dispatch.rs`). -So a child is never handed an empty argument, and a program asked to run -`cmd "" x` sees `cmd x`; and an argument C or std carries as bytes, which POSIX -and `OsStr` allow, refuses the spawn. Read from the code while building +A spawn's `argv` is one `NUL`-separated run of bytes (`toyos::spawn::Spawn`), +which the startup block's writer splits skipping every empty string +(`toyos::startup::block`), as the kernel did while it copied `argv`. So a +child is never handed an empty argument, and a program asked to run +`cmd "" x` sees `cmd x`. An argument that is not UTF-8 reaches the block and a +C child as given, and a Rust child's `std::env::args_os` as an empty one +(`sdk/std/sys/args.rs`). Read from the code while building `posix_spawn` (stage 3 of `issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`), not reproduced: `tests/guest/libc` passes `""` only where the child reads no argument. Build tools pass empty arguments (`make`'s recipes, `sh -c ''`). -A launch through the supervisor carries the same blob (`toyos::launch`'s -request names it "exactly the blobs `SYS_SPAWN` takes") and loses the same -arguments before any spawn: `serve_launch` (`system/supervisor/src/main.rs`) +A launch through the supervisor carries the same blob (`toyos::launch`) and +loses the same arguments before any spawn: `serve_launch` (`system/supervisor/src/main.rs`) splits `request.argv` on `NUL`, filters out every empty argument, and skips an argument that is not UTF-8 without refusing the launch, so a launched child sees neither. *Exit*: a child spawned with `argv` `["a", "", "b"]` and with a non-UTF-8 -argument reads both as given, in a guest test, red with the kernel's filter put +argument reads both as given, in a guest test, red with the writer's filter put back; and a host test of the supervisor's decode of a launch request's `argv` keeps an empty and a non-UTF-8 argument, red with its filter put back. The blob needs a representation that says where an argument ends without 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 012de414ccd..50ee0ba2869 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 @@ -170,10 +170,14 @@ Beside that order, each waiting on something outside it: its windows** (#1152, #1182; item 1 above). The kernel keeps no working directory: a spawn states its child's in the child's `PWD`, which the SDK writes and the child's SDK starts from - (`toyos::files::starting_in`). The kernel still holds userland data - it never reads, path text among it: the environment - (`ProcessData::env`, answered by `SYS_GET_ENV`), whose likely exit - is a startup memory object the parent fills for the child. `/tmp` is the + (`toyos::files::starting_in`). A child's arguments and environment, + that `PWD` among it, are a startup block its parent lays out below + its program (`toyos::startup`), which the kernel neither stores nor + answers (#PRNUM): `SYS_GET_ENV` (73) and `ProcessData::env` are + gone. The kernel still keeps one userland text of each process, the + path its parent read the program from (`SpawnArgs::path_ptr`), which + the spawn's record and a crash's frames name the file by and + `symbolize` opens; its last component is the process's name. `/tmp` is the MEMORY file server's, one for the machine, of which each installed package's `/tmp` is its own directory (#1181); per session, as the owner ruled ("Per session", 2026-10-03), is the isolation track's stage 3. diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs index b937a1d8c4d..9bc3ad447b2 100644 --- a/kernel/src/arch/aarch64/paging.rs +++ b/kernel/src/arch/aarch64/paging.rs @@ -401,13 +401,6 @@ pub fn direct_map_policy(phys: u64) -> Option { high().as_ref()?.leaf(DirectMap::from_phys(phys).as_ptr::() as u64).map(|(leaf, _)| policy_of(leaf)) } -/// The kernel wrote instructions into the frame at `phys` that a user leaf -/// executes: the data it wrote is cleaned to the point of unification and the -/// stale instructions invalidated, as the Arm ARM requires before they run. -pub fn wrote_code(phys: u64) { - super::cache::make_executable(DirectMap::from_phys(phys).as_ptr::() as u64, PAGE_4K as usize); -} - /// Marks a user leaf whose frame the space owns: one of the bits [58:55] a /// descriptor leaves to software (Arm ARM K.a, D8.3.2). Kept on a leaf that is /// not valid, so a frame no access may reach is still recorded. diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index fd111aba2e1..0b6d8ffb125 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -645,11 +645,6 @@ pub fn guard_4k(phys: u64) { KERNEL.lock().guard_4k(phys); } -/// The kernel wrote instructions into the frame at `phys` that a user leaf -/// executes: nothing to do, as the instruction and data caches are coherent -/// (SDM Vol. 3A §11.6). -pub fn wrote_code(_phys: u64) {} - /// Marks a user leaf whose frame the space owns: one of the bits a PTE leaves /// to software (SDM Vol. 3A §4.5, Table 4-19). Kept on a leaf that is not /// present, so a frame no access may reach is still recorded. diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 9ce4108d97c..00436378904 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -2,9 +2,10 @@ //! //! 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 +//! each checked as an ordinary mapping is, and the entry point; the kernel +//! builds the space, places an empty stack and starts the main thread. What +//! the parent tells the child is bytes of a range, which the kernel never +//! reads. 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 (`toyos::dl`). //! @@ -115,61 +116,29 @@ fn lay(space: &mut crate::mm::space::AddressSpace, maps: &[Map]) -> Result<(), S Ok(()) } -/// The bytes a program's stack starts with below `top`, and where its stack -/// pointer starts: argc, the argv pointers and a null, 16-aligned, then the -/// strings they point at. -fn argv_block(top: u64, args: &[&str]) -> (u64, Vec) { - let strings: usize = args.iter().map(|a| a.len() + 1).sum(); - let words = args.len() + 2; - let sp = (top - strings as u64 - words as u64 * 8) & !15; - let mut block = alloc::vec![0u8; (top - sp) as usize]; - let mut word = |i: usize, value: u64| block[i * 8..i * 8 + 8].copy_from_slice(&value.to_ne_bytes()); - word(0, args.len() as u64); - let mut at = top - strings as u64; - let mut pointers = Vec::with_capacity(args.len()); - for arg in args { - pointers.push(at); - at += arg.len() as u64 + 1; - } - for (i, pointer) in pointers.iter().enumerate() { - word(1 + i, *pointer); - } - for (arg, pointer) in args.iter().zip(&pointers) { - let off = (pointer - sp) as usize; - block[off..off + arg.len()].copy_from_slice(arg.as_bytes()); - } - (sp, block) -} - /// 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 program; the kernel resolves no path. `argv[0]` is the -/// name the child goes by. +/// `path` labels the program, and its last component is the name the child +/// goes by; the kernel resolves no path. /// /// `Refusal`, not `-> !`, is the error type: every failure below owns a /// partly built process (address space, stack, kernel stack), and nothing /// unwinds, so the error must travel out as a value rather than strand it. pub fn spawn( path: &str, - argv: &[&str], commit: C, - env: Vec, program: Program, parent: Parent, ) -> Result<(Pid, C::Held), crate::object::Refusal> { let job = Arc::clone(commit.job()); let pager = if commit.pager() { crate::process::PagerRole::May } else { crate::process::PagerRole::None }; - // An argv of only separators survives sys_spawn's split as an empty slice. - let Some(&name) = argv.first() else { - return Err(SyscallError::InvalidArgument.into()); - }; // Before anything is built; dropped on every way out below but the insert. let admission = Admission::ask(parent)?; - let charge = ProcessData::charge(&job, path, &env).map_err(SyscallError::from)?; + let charge = ProcessData::charge(&job, path).map_err(SyscallError::from)?; let t0 = crate::clock::nanos_since_boot(); let Program { maps, entry, image_base, build_id } = program; @@ -200,12 +169,7 @@ pub fn spawn( let stack = space.reserve(STACK_GUARD + USER_STACK_SIZE as u64, At::ANYWHERE, Prot::None)?; let stack_base = stack + STACK_GUARD; space.reserve(USER_STACK_SIZE as u64, At::Fixed(stack_base), Prot::ReadWrite)?; - let (sp, block) = argv_block(stack_base.raw() + USER_STACK_SIZE as u64, argv); - if block.len() > USER_STACK_SIZE { - return Err(SyscallError::InvalidArgument.into()); - } - space.fill(sp, &block); - (stack_base, sp) + (stack_base, stack_base.raw() + USER_STACK_SIZE as u64) }; // What a crash record names a frame by. @@ -227,7 +191,6 @@ pub fn spawn( let (handles, endowments, held) = commit.commit(start::Own::new(&object, ¬ice))?; let proc_data = Arc::new(Lock::new(ProcessData { handles, - env, space: Arc::clone(&child_pt), spawn_ns: crate::clock::nanos_since_boot(), accounting: ProcessAccounting::default(), @@ -249,7 +212,7 @@ pub fn spawn( let ((), retire) = admission.land(guard.as_mut().unwrap(), |table, node| { table.insert(ProcessEntry::new( Arc::clone(&object), - start::make_name(name), + start::make_name(path), proc_data, // Two holders: a crash report on this thread reads it without the process table. Some(Arc::clone(&image)), @@ -370,7 +333,7 @@ pub fn spawn_supervisor(images: crate::bootimage::Objects, laid: &toyos_kimage:: }; // 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(), program, Parent::Root) { + match spawn(SUPERVISOR, table, 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/loader/start.rs b/kernel/src/loader/start.rs index 8f64cb98a38..2a30528bf07 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -26,7 +26,7 @@ use toyos_abi::syscall::{ /// Where a new context first runs. pub(crate) enum Start { - /// A program's first instruction, on the stack its loader wrote. + /// A program's first instruction, on its empty stack. Process { entry: toyos_userbound::Entry, sp: u64 }, /// A thread's, on a stack its process chose, with its argument. Thread { entry: toyos_userbound::Entry, sp: u64, arg: u64 }, @@ -47,8 +47,7 @@ impl KernelStack { /// on the one watch it may wait on, whose list doubles, its place in its /// process's thread table, its own data, and what one of its calls copies /// in and may hold while it is preempted — two strings at their bound, as - /// a rename's paths, and a page besides, as a namespace's names. A spawn - /// charges what it copies past that. + /// a rename's paths, and a page besides, as a namespace's names. fn heap() -> usize { let waiter = 2 * core::mem::size_of::<(usize, u64)>(); let call = 2 * crate::user_ptr::MAX_USER_STR as usize + crate::mm::PAGE_BYTES; diff --git a/kernel/src/mm/space.rs b/kernel/src/mm/space.rs index f93904c1e53..65e662cfea7 100644 --- a/kernel/src/mm/space.rs +++ b/kernel/src/mm/space.rs @@ -826,52 +826,6 @@ impl AddressSpace { Leaf::Held(_) | Leaf::Empty => None, } } - - /// Write `bytes` at `va`, into private memory this space committed: - /// what the kernel lays out in a space before any thread runs in it, or - /// in a range no thread has been told of. - pub fn fill(&mut self, va: u64, bytes: &[u8]) { - let mut done = 0; - while done < bytes.len() { - let at = va + done as u64; - let n = ((PAGE_SIZE - at % PAGE_SIZE) as usize).min(bytes.len() - done); - let phys = self.populate_private(at).unwrap_or_else(|| panic!("space: {at:#x} is no committed private memory")); - // SAFETY: `populate_private` answered a frame this space owns and - // maps at `at`, and `n` bytes from there stay inside its page; the - // kernel's own buffer cannot overlap it. - unsafe { - core::ptr::copy_nonoverlapping(bytes.as_ptr().add(done), super::DirectMap::from_phys(phys).as_mut_ptr(), n); - } - if self.inventory.find(at).is_some_and(|(_, entry)| entry.prot == Prot::ReadExec) { - super::paging::wrote_code(phys & !(PAGE_SIZE - 1)); - } - done += n; - } - } - - /// `va`'s frame, populated if empty, where `va` is committed private memory. - fn populate_private(&mut self, va: u64) -> Option { - let (_, entry) = self.inventory.find(va)?; - if !matches!(entry.kind, Kind::Private { committed: true }) { - return None; - } - let page = va & !(PAGE_SIZE - 1); - let phys = match self.tables.get(page) { - Leaf::Mapped(m) => m.phys, - Leaf::Held(phys) => phys, - Leaf::Empty => { - let phys = pmm::alloc_promised().into_raw(); - let leaf = match leaf_prot(entry.prot) { - Some(prot) => Leaf::Mapped(Mapping { phys, prot, owned: true, cache: CachePolicy::Normal }), - None => Leaf::Held(phys), - }; - self.tables.set(page, leaf); - self.usage.populated(1); - phys - } - }; - Some(phys + va % PAGE_SIZE) - } } /// Whether a range's pages are memory the direct map reaches. diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 212cc60a380..a8152622d54 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -262,9 +262,6 @@ impl Processes for ProcessTable { pub struct ProcessData { /// Every kernel object this process can name; stdio is handles `0`, `1`, `2`. pub handles: HandleTable, - /// Inherited environment variables (KEY=VALUE\0KEY2=VALUE2\0...) - pub env: Vec, - /// The space every thread of it runs in. pub space: PageTables, @@ -302,8 +299,8 @@ impl ProcessData { /// The heap a process of the program at `path` takes beside what is /// charged on its own — its handles, threads, ranges and objects: its /// records, its place in the process table and among its parent's - /// children, its strings at their bound, and its endowments at theirs. - pub fn charge(job: &Arc, path: &str, env: &[u8]) -> Result { + /// children, its label, and its endowments at their bound. + pub fn charge(job: &Arc, path: &str) -> Result { // The process table doubles, and its control byte beside each. let entry = 2 * (core::mem::size_of::<(Pid, ProcessEntry)>() + 1); // A thread table of one thread is a whole leaf. @@ -315,12 +312,10 @@ impl ProcessData { + entry + threads + 2 * core::mem::size_of::(); - let strings = charge::array::(path.len()) - + charge::array::(toyos_abi::syscall::MAX_PATH) - + charge::array::(env.len()); + let label = charge::array::(path.len()); let endowments = charge::array::(toyos_abi::syscall::MAX_ENDOWMENTS) + charge::array::(toyos_abi::syscall::MAX_LABELS_LEN); - Charge::new(job, records + strings + endowments) + Charge::new(job, records + label + endowments) } } diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index bd7b8d3d927..bddfde463b0 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -124,21 +124,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Ok(pager) => pager, Err(refused) => return refused, }; - // What is copied below past what a thread is charged for one - // call's copies, and the list of argv's strings, a pointer pair - // for every two bytes at most, held until the call returns; each - // length is refused past its bound below, so this charges no more - // than that. - let copies = [args.path_len, args.argv_len, args.labels_len, args.env_len] - .map(|len| len.min(crate::user_ptr::MAX_USER_STR) as usize); - let argv = crate::mm::charge::array::<&str>(copies[1] / 2 + 1); - let past = (copies.iter().sum::() + argv).saturating_sub(2 * crate::user_ptr::MAX_USER_STR as usize); - let _copies = match crate::mm::charge::Charge::current(past) { - Ok(charge) => charge, - Err(e) => return SyscallError::from(e).to_u64(), - }; let path = match ctx.user_str(UserAddr::new(args.path_ptr), args.path_len) { Ok(s) => s, Err(e) => return e.to_u64() }; - let text = match ctx.user_str(UserAddr::new(args.argv_ptr), args.argv_len) { Ok(s) => s, Err(e) => return e.to_u64() }; if args.endow_count as usize > toyos_abi::syscall::MAX_SPAWN_ENDOWMENTS || args.labels_len > toyos_abi::syscall::MAX_SPAWN_LABELS_LEN as u64 { @@ -160,21 +146,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Ok(built) => built, Err(e) => return e.refuse(), }; - // Copied and bounded like argv: env is kept for the child's whole life, - // and `user_vec` is the one accessor that puts a userland size on the allocator. - let env = if args.env_len > 0 { - if args.env_len > crate::user_ptr::MAX_USER_STR { - return SyscallError::InvalidArgument.to_u64(); - } - match ctx.user_vec(UserAddr::new(args.env_ptr), args.env_len) { - Ok(bytes) => bytes, - Err(e) => return e.to_u64(), - } - } else { - 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, program, parent) + sys_spawn(&path, pending, program, parent) } SYS_PROCESS_WAIT => sys_process_wait(RawHandle(a1 as u32), a2), SYS_PROCESS_KILL => { @@ -280,16 +252,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> sys_write_nonblock(RawHandle(a1 as u32), &buf) } SYS_EXIT => sys_exit(a1 as i32), - SYS_GET_ENV => { - let env = process::with_process_data(|d| d.env.clone()); - if a2 == 0 { - env.len() as u64 - } else { - let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a1), a2) else { return bad_addr }; - let copy_len = env.len().min(buf.len()); - buf.write_at(0, &env[..copy_len]).map_or(bad_addr, |()| copy_len as u64) - } - } // What a claimed PCI function's driver is built out of. Each is // gated by the claim handle alone: nothing here takes a bus/device/ // function, so a process cannot name a device it was not given. diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 2ddf4046cc4..4f9104ca938 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -105,14 +105,12 @@ pub(super) fn spawn_program(ctx: &crate::user_ptr::SyscallContext<'_>, args: &Sp /// 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, 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, program, parent) { + match process::spawn(path, pending, program, parent) { Ok((_, handle)) => u64::from(handle.0), Err(e) => e.refuse(), } diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index a27d64aafb3..71a33aa0f8e 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -86,7 +86,6 @@ pub const SYS_VM_RELEASE: u64 = 64; pub const SYS_READ_NONBLOCK: u64 = 66; pub const SYS_WRITE_NONBLOCK: u64 = 67; pub const SYS_EXIT: u64 = 72; -pub const SYS_GET_ENV: u64 = 73; pub const SYS_CLOCK_EPOCH: u64 = 75; /// Accept a queued connection from an [`Acceptor`] handle. /// @@ -394,14 +393,10 @@ crate::user_safe! { #[derive(Clone, Copy)] pub struct SpawnArgs { /// 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. + /// spawn's record and a crash's, and whose last component is the name + /// the child goes by. Nothing opens it. pub path_ptr: u64, pub path_len: u64, - pub argv_ptr: u64, - pub argv_len: u64, - pub env_ptr: u64, - pub env_len: u64, /// `[EndowEntry]`, moved out of the parent's table. pub endow_ptr: u64, pub endow_count: u64, @@ -411,7 +406,9 @@ crate::user_safe! { /// 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. + /// else of the space is empty until the child maps it, but its stack, + /// which starts empty: what a parent tells its child, its arguments + /// and environment among it, is bytes of a range. pub maps_ptr: u64, pub maps_count: u64, /// Where the child's main thread starts: inside a [`VmProt::ReadExec`] @@ -1082,12 +1079,6 @@ pub fn handle_wait(handle: RawHandle, interest: u32, timeout_nanos: u64) -> Resu check(syscall(SYS_HANDLE_WAIT, handle.0 as u64, interest as u64, timeout_nanos, 0)).map(|bit| bit as u32) } -/// Read the inherited environment variables into `buf`. -/// Returns the number of bytes written, or the required size if buf is too small. -pub fn get_env(buf: &mut [u8]) -> usize { - syscall(SYS_GET_ENV, buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0) as usize -} - /// Spawn a new process. /// /// Answers a `Process` handle carrying `WAIT|MANAGE|READ|DUP|TRANSFER`. A diff --git a/sdk/libc/src/arch/aarch64.rs b/sdk/libc/src/arch/aarch64.rs index 2d7c0c27ce2..894edd40073 100644 --- a/sdk/libc/src/arch/aarch64.rs +++ b/sdk/libc/src/arch/aarch64.rs @@ -2,11 +2,8 @@ //! byte tail, and the FP square roots. An unaligned `ldp`/`stp` is legal on the //! Normal memory every user buffer is. -/// Entry point for C programs. Stack layout at entry (set up by kernel), with -/// the stack pointer 16-byte aligned: -/// [sp] = argc -/// [sp+8] = argv[0], argv[1], ..., NULL -/// The image relocates itself first, from its own header's address. +/// Entry point for C programs, on an empty stack. The image relocates itself +/// first, from its own header's address. #[cfg(not(feature = "std-runtime"))] #[unsafe(no_mangle)] #[unsafe(naked)] @@ -15,8 +12,6 @@ unsafe extern "C" fn _start() -> ! { "adrp x0, __ehdr_start", "add x0, x0, :lo12:__ehdr_start", "bl {relocate}", - "ldr x0, [sp]", // argc - "add x1, sp, #8", // argv // The outermost frame record: a backtrace ends here. "mov x29, xzr", "mov x30, xzr", diff --git a/sdk/libc/src/arch/x86_64.rs b/sdk/libc/src/arch/x86_64.rs index e676c8dd482..1e8cbb6acad 100644 --- a/sdk/libc/src/arch/x86_64.rs +++ b/sdk/libc/src/arch/x86_64.rs @@ -1,9 +1,7 @@ //! x86-64: the entry, the string instructions, and SSE2's square roots. -/// Entry point for C programs. Stack layout at entry (set up by kernel): -/// [RSP] = argc -/// [RSP+8] = argv[0], argv[1], ..., NULL -/// The image relocates itself first, from its own header's address. +/// Entry point for C programs, on an empty stack. The image relocates itself +/// first, from its own header's address. #[cfg(not(feature = "std-runtime"))] #[unsafe(no_mangle)] #[unsafe(naked)] @@ -11,8 +9,6 @@ unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( "lea rdi, [rip + __ehdr_start]", "call {relocate}", - "mov rdi, [rsp]", // argc - "lea rsi, [rsp + 8]", // argv "call {start_c}", "ud2", relocate = sym toyos::relocate::relocate_self, diff --git a/sdk/libc/src/env.rs b/sdk/libc/src/env.rs index 2dd0cba0f14..93bfe90939f 100644 --- a/sdk/libc/src/env.rs +++ b/sdk/libc/src/env.rs @@ -1,5 +1,5 @@ //! The environment: `environ`, `getenv`, `setenv` and `unsetenv`, read from -//! the block the kernel started this process with. +//! the block this process started with (`toyos::startup`). //! //! **A string `setenv` replaces or `unsetenv` removes is never freed**: a //! program that keeps what `getenv` answered across a later call, which POSIX @@ -50,16 +50,7 @@ fn is(entry: &[u8], name: &[u8]) -> bool { impl Env { fn read() -> Env { - let mut block = Vec::new(); - loop { - let need = toyos_abi::syscall::get_env(&mut block); - if need <= block.len() { - block.truncate(need); - break; - } - block.resize(need, 0); - } - let strings: Vec<*mut u8> = block.split(|&b| b == 0).filter(|e| !e.is_empty()).map(leak).collect(); + let strings: Vec<*mut u8> = toyos::startup::env().map(leak).collect(); let mut env = Env { strings, array: Vec::new() }; env.publish(); env @@ -151,7 +142,7 @@ pub unsafe extern "C" fn unsetenv(name: *const u8) -> i32 { 0 } -/// The environment as `SYS_SPAWN` takes one: each `KEY=VALUE`, ended by `NUL`. +/// The environment as a spawn takes one: each `KEY=VALUE`, ended by `NUL`. pub(crate) fn blob() -> Vec { with(|env| { let mut out = Vec::new(); diff --git a/sdk/libc/src/lib.rs b/sdk/libc/src/lib.rs index 41022a9866e..31358976b83 100644 --- a/sdk/libc/src/lib.rs +++ b/sdk/libc/src/lib.rs @@ -64,15 +64,16 @@ mod runtime { // goes through libc's `exit` rather than the syscall: the atexit table and // `fflush(NULL)` are what stand between a program's last unterminated line // and the fd. - pub(crate) extern "C" fn start_c(argc: i32, argv: *const *const u8) -> ! { + pub(crate) extern "C" fn start_c() -> ! { unsafe extern "C" { fn main(argc: i32, argv: *const *const u8) -> i32; static __ehdr_start: u8; } + let (argc, argv) = toyos::startup::args(); // The program goes by its `argv[0]`. let name = match argc { 0 => &[][..], - // SAFETY: the kernel's argument block, NUL-terminated strings. + // SAFETY: the startup block's first argument, a NUL-terminated string. _ => unsafe { crate::posix_io::c_str_to_bytes(*argv) }, }; // SAFETY: the executable's own header, and this is its main thread. @@ -89,7 +90,7 @@ mod runtime { hook(); } } - let code = unsafe { main(argc, argv) }; + let code = unsafe { main(argc as i32, argv) }; unsafe { crate::misc::exit(code) } } diff --git a/sdk/libc/src/misc.rs b/sdk/libc/src/misc.rs index ef91427e182..421a00f48cc 100644 --- a/sdk/libc/src/misc.rs +++ b/sdk/libc/src/misc.rs @@ -257,9 +257,9 @@ pub unsafe extern "C" fn sysconf(name: i32) -> i64 { _SC_PAGESIZE => crate::memreq::PAGE as i64, _SC_NPROCESSORS_ONLN => syscall::cpu_count() as i64, _SC_CLK_TCK => 100, - // What `SYS_SPAWN` takes of each of `argv` and the environment - // (`kernel/src/user_ptr.rs`'s `MAX_USER_STR`), so both together fit. - _SC_ARG_MAX => 64 * 1024, + // What a child's startup block holds: its arguments, its + // environment and a pointer to each. + _SC_ARG_MAX => toyos::startup::MAX_BLOCK as i64, // POSIX's -1 with `errno` untouched: the `getpw*_r` lookups it bounds // are ones ToyOS does not have. _SC_GETPW_R_SIZE_MAX => -1, diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 72749fc06d5..f9eef3e33ac 100644 --- a/sdk/libc/src/spawn.rs +++ b/sdk/libc/src/spawn.rs @@ -195,8 +195,8 @@ pub unsafe extern "C" fn posix_spawnattr_getsigdefault(attr: *const SpawnAttr, s 0 } -/// `strs`, a null-terminated array of C strings, as `SYS_SPAWN`'s blob of -/// them, each ended by `NUL`. +/// `strs`, a null-terminated array of C strings, as a spawn's blob of them +/// (`toyos::spawn::Spawn`), each ended by `NUL`. /// /// # Safety /// `strs` is null or a null-terminated array of C strings. diff --git a/sdk/std/sys/args.rs b/sdk/std/sys/args.rs index bf25baf56e1..651029152c6 100644 --- a/sdk/std/sys/args.rs +++ b/sdk/std/sys/args.rs @@ -1,24 +1,14 @@ -use core::sync::atomic::Ordering; - pub use super::common::Args; use crate::ffi::{CStr, OsString}; pub fn args() -> Args { - let argc = crate::sys::pal::ARGC.load(Ordering::Relaxed); - let argv = core::ptr::with_exposed_provenance::<*const u8>( - crate::sys::pal::ARGV.load(Ordering::Relaxed), - ); - - let mut vec = Vec::with_capacity(argc); - - for i in 0..argc { - let ptr = unsafe { *argv.add(i) }; - if ptr.is_null() { - break; - } - let cstr = unsafe { CStr::from_ptr(ptr.cast()) }; - vec.push(OsString::from(cstr.to_str().unwrap_or(""))); - } - + let (argc, argv) = toyos::startup::args(); + let vec = (0..argc) + .map(|i| { + // SAFETY: one of the block's `argc` arguments, a NUL-terminated string. + let arg = unsafe { CStr::from_ptr((*argv.add(i)).cast()) }; + OsString::from(arg.to_str().unwrap_or("")) + }) + .collect(); Args::new(vec) } diff --git a/sdk/std/sys/env.rs b/sdk/std/sys/env.rs index 41cb92c194f..0174ec6fdc2 100644 --- a/sdk/std/sys/env.rs +++ b/sdk/std/sys/env.rs @@ -6,25 +6,17 @@ use crate::sync::{Mutex, MutexGuard}; static ENV: Mutex>> = Mutex::new(None); -/// Lock ENV and lazily initialize from the kernel if needed. +/// Lock ENV and lazily initialize from the startup block if needed. /// This ensures env works from cdylib .so files whose std was never explicitly initialized. fn lock_env() -> MutexGuard<'static, Option>> { let mut guard = ENV.lock().unwrap(); if guard.is_none() { let mut map = HashMap::new(); - let size = toyos_abi::syscall::get_env(&mut []); - if size > 0 { - let mut buf = vec![0u8; size]; - toyos_abi::syscall::get_env(&mut buf); - for entry in buf.split(|&b| b == 0) { - if entry.is_empty() { - continue; - } - if let Some(eq) = entry.iter().position(|&b| b == b'=') { - let key = OsString::from(crate::str::from_utf8(&entry[..eq]).unwrap_or("")); - let val = OsString::from(crate::str::from_utf8(&entry[eq + 1..]).unwrap_or("")); - map.insert(key, val); - } + for entry in toyos::startup::env() { + if let Some(eq) = entry.iter().position(|&b| b == b'=') { + let key = OsString::from(crate::str::from_utf8(&entry[..eq]).unwrap_or("")); + let val = OsString::from(crate::str::from_utf8(&entry[eq + 1..]).unwrap_or("")); + map.insert(key, val); } } *guard = Some(map); diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index a964c7282d8..564496d2cf1 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -7,7 +7,6 @@ pub mod tls; #[path = "../../../../rust/library/std/src/sys/pal/unsupported/common.rs"] mod unsupported_common; -use core::sync::atomic::{AtomicUsize, Ordering}; pub use unsupported_common::{cleanup, init}; @@ -165,14 +164,8 @@ pub fn fd_error(e: toyos::fd::Error) -> crate::io::Error { } } -// argc/argv stored by _start for std::env::args() -pub(crate) static ARGC: AtomicUsize = AtomicUsize::new(0); -pub(crate) static ARGV: AtomicUsize = AtomicUsize::new(0); // *const *const u8 as usize - -// Stack layout at entry (set up by kernel), with the stack pointer 16-byte aligned: -// [sp] = argc -// [sp+8] = argv[0], argv[1], ..., NULL -// The image relocates itself first, from its own header's address. +// The entry, on an empty stack. The image relocates itself first, from its +// own header's address. #[cfg(target_arch = "x86_64")] #[unsafe(no_mangle)] #[unsafe(naked)] @@ -180,8 +173,6 @@ unsafe extern "C" fn _start() -> ! { core::arch::naked_asm!( "lea rdi, [rip + __ehdr_start]", "call {relocate}", - "mov rdi, [rsp]", - "lea rsi, [rsp + 8]", "call {start_rust}", "ud2", relocate = sym toyos::relocate::relocate_self, @@ -197,8 +188,6 @@ unsafe extern "C" fn _start() -> ! { "adrp x0, __ehdr_start", "add x0, x0, :lo12:__ehdr_start", "bl {relocate}", - "ldr x0, [sp]", - "add x1, sp, #8", // The outermost frame record: a backtrace ends here. "mov x29, xzr", "mov x30, xzr", @@ -219,11 +208,12 @@ extern "C" fn init_eh_frame() { #[unsafe(link_section = ".init_array")] static INIT_EH_FRAME: extern "C" fn() = init_eh_frame; -extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { +extern "C" fn start_rust() -> ! { unsafe extern "C" { fn main(argc: i32, argv: *const *const u8) -> i32; static __ehdr_start: u8; } + let (argc, argv) = toyos::startup::args(); // The program goes by its `argv[0]`. let name = match argc { 0 => &[][..], @@ -231,8 +221,6 @@ extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { }; // 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); eh_frame::init(); crate::sys::env::init(); diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs index 7023a2e9157..28ddca4b463 100644 --- a/sdk/std/sys/process.rs +++ b/sdk/std/sys/process.rs @@ -86,7 +86,7 @@ struct Prepared { cwd: String, } -/// `KEY=VALUE\0` for each variable: the blob `SYS_SPAWN` and a launch both take. +/// `KEY=VALUE\0` for each variable: the blob a spawn and a launch both take. fn env_blob(env: &BTreeMap) -> Vec { let mut blob = Vec::new(); for (key, value) in env { diff --git a/sdk/toyos/src/elf/program.rs b/sdk/toyos/src/elf/program.rs index d47139b738c..ff33f78a1aa 100644 --- a/sdk/toyos/src/elf/program.rs +++ b/sdk/toyos/src/elf/program.rs @@ -1,19 +1,23 @@ //! 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. +//! ordinary mapping and builds the child from them. Below the program lies +//! what its parent tells it (`crate::startup`), zeros until a spawn names a +//! block. 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}; +use crate::startup::{self, Block}; /// 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. +/// page (`toyos_abi::clock::CLOCK_PAGE`) and the startup block, 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 @@ -29,8 +33,9 @@ pub struct Program { } impl Program { - /// The ranges, each a `PT_LOAD` copied from the object or a hole between - /// two, reserved, in address order. + /// The ranges in address order, none apart from the next: the startup + /// block's pages and the reserved ones above them, then each `PT_LOAD` + /// copied from the object or a hole between two, reserved. pub fn maps(&self) -> &[SpawnMap] { &self.maps[..self.count] } @@ -38,15 +43,28 @@ impl Program { pub fn build_id(&self) -> &[u8] { &self.build_id[..self.build_id_len] } + + /// The program, its child starting with `block` in place of zeros. + pub fn starting_with(mut self, block: &Block) -> Program { + let pages = (block.len() as u64).next_multiple_of(PAGE).max(PAGE); + self.maps[0] = SpawnMap { addr: startup::AT, len: pages, object: block.object(), offset: 0, copied: block.len() as u64, prot: VmProt::ReadWrite as u64 }; + self.maps[1] = reserved(startup::AT + pages, BASE); + self + } +} + +/// `[lo, hi)` reserved: no page of it is mapped. +fn reserved(lo: u64, hi: u64) -> SpawnMap { + SpawnMap { addr: lo, len: hi - lo, object: u64::from(HANDLE_INVALID.0), ..SpawnMap::default() } } /// 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 +/// at [`BASE`], each `PT_LOAD` copied from `object`, the memory object holding +/// `file`, below it a startup block that says nothing. 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, 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 { +pub fn lay_out(file: &[u8], machine: Machine, object: 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"); @@ -54,14 +72,14 @@ pub fn lay_out(file: &[u8], machine: Machine, object: u64, at: u64) -> Result Result covered { - let hole = SpawnMap { addr: address(covered)?, len: lo - covered, object: u64::from(HANDLE_INVALID.0), ..SpawnMap::default() }; - push(hole)?; + push(reserved(address(covered)?, address(lo)?))?; } 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 })?; diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index 07bd7eb27f2..20600243147 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -488,11 +488,12 @@ pub fn starting_in(env: &[u8], dir: &str) -> Option> { Some(out) } -/// The directory a process starts in, read from the environment its spawn -/// gave it: its [`START`], lexically clean, or `/`, its view's root, where it -/// states none. `None` for one [`starting_in`] would not have written. -pub fn starting_directory(env: &[u8]) -> Option { - let Some(dir) = env.split(|&b| b == 0).find_map(stated) else { return Some(String::from("/")) }; +/// The directory a process starts in, read from `env`, the entries of the +/// environment its spawn gave it: its [`START`], lexically clean, or `/`, its +/// view's root, where it states none. `None` for one [`starting_in`] would +/// not have written. +pub fn starting_directory<'a>(mut env: impl Iterator) -> Option { + let Some(dir) = env.find_map(stated) else { return Some(String::from("/")) }; let dir = core::str::from_utf8(dir).ok().filter(|d| d.starts_with('/') && d.len() <= fs::MAX_PATH)?; Some(absolute("/", dir)) } @@ -530,6 +531,11 @@ mod owner; mod tests { use super::*; + /// Where a child given `env` starts. + fn starts(env: &[u8]) -> Option { + starting_directory(env.split(|&b| b == 0)) + } + #[test] fn a_path_is_made_absolute_and_lexically_clean() { assert_eq!(absolute("/home/a", "b/./c"), "/home/a/b/c"); @@ -544,16 +550,16 @@ mod tests { fn a_child_starts_where_its_spawn_says_and_nowhere_its_environment_said() { let given = b"HOME=/home/toy\0PWD=/elsewhere\0PWDX=/x\0TERM=toyos\0"; let env = starting_in(given, "/home/toy/Games").expect("an absolute directory"); - assert_eq!(starting_directory(&env).as_deref(), Some("/home/toy/Games")); + assert_eq!(starts(&env).as_deref(), Some("/home/toy/Games")); assert_eq!(env, b"HOME=/home/toy\0PWDX=/x\0TERM=toyos\0PWD=/home/toy/Games\0"); - assert_eq!(starting_directory(&starting_in(b"", "/").unwrap()).as_deref(), Some("/")); - assert_eq!(starting_directory(b"PWD=/a/./b/../c\0").as_deref(), Some("/a/c")); + assert_eq!(starts(&starting_in(b"", "/").unwrap()).as_deref(), Some("/")); + assert_eq!(starts(b"PWD=/a/./b/../c\0").as_deref(), Some("/a/c")); } #[test] fn a_spawn_stating_no_directory_starts_its_child_at_the_root() { - assert_eq!(starting_directory(b"").as_deref(), Some("/")); - assert_eq!(starting_directory(b"PWDX=/x\0HOME=/home\0").as_deref(), Some("/")); + assert_eq!(starts(b"").as_deref(), Some("/")); + assert_eq!(starts(b"PWDX=/x\0HOME=/home\0").as_deref(), Some("/")); } #[test] @@ -564,7 +570,7 @@ mod tests { assert_eq!(starting_in(b"", dir), None, "{dir:?}"); } for env in [&b"PWD=home\0"[..], b"PWD=\0", b"PWD=/\xff\0"] { - assert_eq!(starting_directory(env), None, "{env:?}"); + assert_eq!(starts(env), None, "{env:?}"); } } diff --git a/sdk/toyos/src/files/owner.rs b/sdk/toyos/src/files/owner.rs index 59736282758..b65dc3c59e4 100644 --- a/sdk/toyos/src/files/owner.rs +++ b/sdk/toyos/src/files/owner.rs @@ -202,10 +202,7 @@ static CWD: Lock> = Lock::new(None); fn cwd() -> String { let mut cwd = CWD.lock(); cwd.get_or_insert_with(|| { - let mut env = alloc::vec![0; syscall::get_env(&mut [])]; - let n = syscall::get_env(&mut env); - assert_eq!(n, env.len(), "the spawn's environment changed size"); - super::starting_directory(&env).expect("the spawn stated a starting directory that is not absolute UTF-8") + super::starting_directory(crate::startup::env()).expect("the spawn stated a starting directory that is not absolute UTF-8") }).clone() } diff --git a/sdk/toyos/src/launch.rs b/sdk/toyos/src/launch.rs index 000ca18dadc..194c768f9aa 100644 --- a/sdk/toyos/src/launch.rs +++ b/sdk/toyos/src/launch.rs @@ -114,8 +114,9 @@ pub enum Parent { /// What a caller asks the supervisor to start. /// -/// `argv` and `env` are exactly the blobs `SYS_SPAWN` takes, so the launcher -/// path and the direct path build one thing rather than two. +/// `argv` and `env` are exactly the blobs a spawn takes +/// (`crate::spawn::Spawn`), so the launcher path and the direct path build one +/// thing rather than two. /// /// **It owns every handle it names** (`H`, an [`OwnedHandle`] but where a /// test encodes one), and [`launch`] consumes them: each is moved to the diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index ef4ba7a5783..c0e95c935c9 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -43,6 +43,7 @@ pub mod relocate; pub mod surface; pub mod shm; pub mod spawn; +pub mod startup; pub mod syscap; pub mod sync; pub mod system; diff --git a/sdk/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index 7698b2d8ec3..f312e6c22ff 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -196,20 +196,27 @@ impl<'a> Spawn<'a> { } impl Helper<'_> { - /// Spawn the child here, carrying the spawn's environment and running - /// `image`, which this process lays out ([`crate::elf::program`]) and the - /// kernel pages the child from. Refused [`Refused::Invalid`] without one, + /// Spawn the child here, running `image`, which this process lays out + /// ([`crate::elf::program`]) and the kernel pages the child from, and + /// starting with the spawn's arguments and environment + /// ([`crate::startup`]). 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"))?; + let said = crate::startup::block(s.argv, &env) + .ok_or(Refused::Invalid("arguments and an environment past what a child starts with"))?; + // Held until the spawn answers: the child copies its pages from it. + let job = s.endow.iter().find(|(label, _)| *label == JOB_LABEL).map(|(_, job)| job.as_handle()); + let block = crate::startup::Block::new(&said, job).map_err(Refused::Syscall)?; + let program = crate::elf::program::lay_out(image.bytes(), crate::elf::Machine::RUNNING, image.handle()) + .map_err(Refused::Invalid)? + .starting_with(&block); + let maps = program.maps(); + let build_id = program.build_id(); let names_svc = s.endow.iter().any(|(label, _)| *label == SVC_LABEL); if s.endow.len() + s.fds.len() + usize::from(!names_svc) > MAX_SPAWN_ENDOWMENTS { return Err(Refused::Invalid("more endowments and descriptors than a spawn carries")); @@ -265,10 +272,6 @@ impl Helper<'_> { let args = SpawnArgs { path_ptr: path.as_ptr() as u64, path_len: path.len() as u64, - argv_ptr: s.argv.as_ptr() as u64, - argv_len: s.argv.len() as u64, - env_ptr: env.as_ptr() as u64, - env_len: env.len() as u64, endow_ptr: entries.as_ptr() as u64, endow_count: count as u64, labels_ptr: labels.as_ptr() as u64, diff --git a/sdk/toyos/src/startup.rs b/sdk/toyos/src/startup.rs new file mode 100644 index 00000000000..5e7df7d1ba9 --- /dev/null +++ b/sdk/toyos/src/startup.rs @@ -0,0 +1,169 @@ +//! What a parent tells the child it starts: its arguments and its +//! environment, a block the parent writes into memory of its own and lays out +//! in the child at [`AT`], directly below the program +//! (`crate::elf::program`). The kernel copies its pages as it copies the +//! program's and reads none of it; every copy of this crate in the child +//! reads it where it lies. +//! +//! ```text +//! AT argc | argv[0] .. argv[argc - 1] | 0 | envp[0] .. | 0 | the strings +//! ``` +//! +//! Eight-byte words, each pointer the address in the child of a string ended +//! by a NUL, an environment's `KEY=VALUE`. A page of zeros is a block that +//! says nothing, which a program laid out and given no block starts with. +//! +//! **The child trusts it as it trusts its own program**, which the same +//! parent laid out: nothing here checks a pointer. + +use alloc::vec::Vec; +use core::ffi::CStr; + +use toyos_abi::syscall::{SyscallError, PAGE_SIZE}; + +use crate::elf::program::BASE; +use crate::shm::SharedMemory; +use crate::{AsHandle, BorrowedHandle}; + +/// The bytes below the program kept for the block: its pages, and reserved +/// ones up to the program. +pub const RESERVED: u64 = 2 * 1024 * 1024; + +/// Where the block lies in every process. +pub const AT: u64 = BASE - RESERVED; + +/// The most bytes of a block: a reserved page is left below the program. +pub const MAX_BLOCK: usize = (RESERVED - PAGE_SIZE as u64) as usize; + +fn strings(run: &[u8]) -> impl Iterator { + run.split(|&b| b == 0).filter(|s| !s.is_empty()) +} + +/// The block that says `args` and `env`, each a run of strings a NUL +/// separates, an empty one skipped. `None` past [`MAX_BLOCK`]. +pub fn block(args: &[u8], env: &[u8]) -> Option> { + let argc = strings(args).count(); + let words = argc + strings(env).count() + 3; + let text: usize = strings(args).chain(strings(env)).map(|s| s.len() + 1).sum(); + if words * 8 + text > MAX_BLOCK { + return None; + } + let mut block = alloc::vec![0u8; words * 8]; + block.reserve_exact(text); + block[..8].copy_from_slice(&(argc as u64).to_ne_bytes()); + let mut word = 1; + for run in [args, env] { + for string in strings(run) { + let at = AT + block.len() as u64; + block[word * 8..word * 8 + 8].copy_from_slice(&at.to_ne_bytes()); + block.extend_from_slice(string); + block.push(0); + word += 1; + } + // The null that ends the run's pointers. + word += 1; + } + Some(block) +} + +/// A block in a memory object, which a spawn names as the pages its child +/// starts with at [`AT`] (`Program::starting_with`) and may drop once the +/// spawn has answered. +pub struct Block { + memory: SharedMemory, + len: usize, +} + +impl Block { + /// `bytes`, which [`block`] made, in memory counted to the job `job` is a + /// handle to carrying `WRITE`, the one the child runs in, or else to this + /// process's own. + pub fn new(bytes: &[u8], job: Option>) -> Result { + let memory = match job { + Some(job) => SharedMemory::create_for(job, bytes.len())?, + None => SharedMemory::create(bytes.len())?, + }; + memory.bytes().write(0, bytes); + Ok(Block { memory, len: bytes.len() }) + } + + /// The object's handle, as a `SpawnMap` names it. + pub fn object(&self) -> u64 { + u64::from(self.memory.as_handle().raw().0) + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +/// This process's argument count and its arguments, a null after the last. +/// A program may write them, as C lets `main` write its `argv`. +pub fn args() -> (usize, *const *const u8) { + let at = AT as *const u64; + // SAFETY: every process starts with the block's first page mapped at + // `AT`, and a block's pointers follow its count. + unsafe { (at.read() as usize, at.add(1).cast()) } +} + +/// The environment this process started with, each entry without its NUL. +/// Nothing hands out a pointer to these strings, so nothing writes them. +pub fn env() -> impl Iterator { + let (argc, argv) = args(); + // SAFETY: the environment's pointers follow the null that ends the + // arguments', are ended by a null, and each names a string of the block. + let envp = unsafe { argv.add(argc + 1) }; + (0..) + .map(move |i| unsafe { envp.add(i).read() }) + .take_while(|entry| !entry.is_null()) + .map(|entry| unsafe { CStr::from_ptr(entry.cast()) }.to_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The string a block's word `word` points at. + fn string(block: &[u8], word: usize) -> Option<&[u8]> { + let at = u64::from_ne_bytes(block[word * 8..word * 8 + 8].try_into().unwrap()); + if at == 0 { + return None; + } + let rest = &block[(at - AT) as usize..]; + Some(&rest[..rest.iter().position(|&b| b == 0).expect("a string ends inside the block")]) + } + + /// The count, then each run's pointers in order and a null after each, + /// every pointer the address its string lies at once the block is at `AT`. + #[test] + fn a_block_s_pointers_name_its_strings_in_order() { + let block = block(b"/bin/echo\0-n\0hello world", b"HOME=/home/toy\0PWD=/\0").unwrap(); + assert_eq!(u64::from_ne_bytes(block[..8].try_into().unwrap()), 3); + let words: Vec<_> = (1..8).map(|word| string(&block, word)).collect(); + assert_eq!( + words, + [Some(&b"/bin/echo"[..]), Some(b"-n"), Some(b"hello world"), None, Some(b"HOME=/home/toy"), Some(b"PWD=/"), None] + ); + assert_eq!(block.len(), 8 * 8 + 10 + 3 + 12 + 15 + 6); + } + + /// Nothing said is a count of zero and two nulls: what a page of zeros reads as. + #[test] + fn a_block_that_says_nothing_is_zeros() { + assert_eq!(block(b"", b"\0\0").unwrap(), [0u8; 24]); + } + + /// A block ends below the program with a page to spare, and one byte more is refused. + #[test] + fn a_block_past_its_reservation_is_refused() { + let fits = alloc::vec![b'a'; MAX_BLOCK - 4 * 8 - 1]; + assert_eq!(block(&fits, b"").map(|block| block.len()), Some(MAX_BLOCK)); + let mut past = fits; + past.push(b'a'); + assert_eq!(block(&past, b""), None); + } +} diff --git a/sdk/toyos/tests/elf_program.rs b/sdk/toyos/tests/elf_program.rs index 0fc3b733883..38966925424 100644 --- a/sdk/toyos/tests/elf_program.rs +++ b/sdk/toyos/tests/elf_program.rs @@ -9,6 +9,7 @@ use common::*; use object::{Object, ObjectSegment, SegmentFlags}; use toyos::elf::program::{lay_out, BASE}; use toyos::elf::Machine; +use toyos::startup::AT; use toyos_abi::syscall::{SpawnMap, VmProt}; use toyos_abi::HANDLE_INVALID; @@ -25,7 +26,7 @@ fn lld_images_are_laid_out_as_object_reads_them() { (&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 program = lay_out(file, machine, OBJECT).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); @@ -49,26 +50,29 @@ fn lld_images_are_laid_out_as_object_reads_them() { 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) { + for hole in program.maps()[1..].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. +/// The lowest page goes at the base, the image base is that less the image's +/// own lowest address, and below the program lies a startup block that says +/// nothing: a page of zeros at its address, and reserved pages up to the base. #[test] -fn the_image_moves_with_its_base() { +fn the_image_lies_at_the_base_above_an_empty_startup_block() { 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(); + let program = lay_out(&file, Machine::X86_64, OBJECT).unwrap(); assert_eq!((program.image_base, program.entry), (BASE - 0x10_0000, BASE + 0x40)); assert_eq!( program.maps(), [ + SpawnMap { addr: AT, len: 0x1000, object: NONE, offset: 0, copied: 0, prot: VmProt::ReadWrite as u64 }, + SpawnMap { addr: AT + 0x1000, len: BASE - AT - 0x1000, object: NONE, offset: 0, copied: 0, prot: VmProt::None as u64 }, 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 }, ] @@ -80,7 +84,7 @@ fn the_image_moves_with_its_base() { #[test] 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), Err("ELF: a PT_LOAD is both writable and executable")); + assert_eq!(lay_out(&file, Machine::X86_64, OBJECT), 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 @@ -88,31 +92,29 @@ fn a_writable_executable_segment_is_refused() { #[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); + let map = lay_out(&file[..0x1200], Machine::X86_64, OBJECT); // The header page alone is held; the parse needs only it. - assert_eq!(map.map(|p| p.maps()[0].copied), Ok(0x1200)); + assert_eq!(map.map(|p| p.maps()[2].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")); + assert_eq!(lay_out(&unmapped, Machine::X86_64, OBJECT), 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")); + assert_eq!(lay_out(&shared, Machine::X86_64, OBJECT), 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()); + assert!(lay_out(&Elf::honest(0x1000).build(), Machine::Aarch64, OBJECT).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(&[])); + assert_eq!(lay_out(file, Machine::X86_64, OBJECT).unwrap().build_id(), toyos::elf::note::build_id_in(file).unwrap_or(&[])); } 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 a2c533d2ba7..360e17fb614 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -70,20 +70,16 @@ fn main() { }; assert!(!region.is_null(), "mmap failed"); - let argv = unsafe { region.add(REGION / 2) }; - const ARGV0: &[u8] = b"/system/bin/echo\0"; - unsafe { core::ptr::copy_nonoverlapping(ARGV0.as_ptr(), argv, ARGV0.len()) }; + let path = unsafe { region.add(REGION / 2) }; + const PATH: &str = "/system/bin/echo"; + unsafe { core::ptr::copy_nonoverlapping(PATH.as_ptr(), path, PATH.len()) }; let echo = image::image_of("/system/bin/echo"); - let laid = image::laid(&echo); + let laid = image::laid(&echo, PATH); let spawn_with = |endow_count: u64| { let args = SpawnArgs { - path_ptr: argv as u64, - path_len: (ARGV0.len() - 1) as u64, - argv_ptr: argv as u64, - argv_len: ARGV0.len() as u64, - env_ptr: 0, - env_len: 0, + path_ptr: path as u64, + path_len: PATH.len() as u64, endow_ptr: (region as u64) + (REGION / 4) as u64, endow_count, labels_ptr: (region as u64) + (REGION / 8) as u64, @@ -196,16 +192,12 @@ 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 laid = image::laid(&own()); + let laid = image::laid(&own(), &argv); // 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: entries.as_ptr() as u64, endow_count: entries.len() as u64, labels_ptr: labels.as_ptr() as u64, diff --git a/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs b/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs index a99575121e7..0cbb165323b 100644 --- a/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs +++ b/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs @@ -6,7 +6,7 @@ //! value its own process may store. //! //! Two are reachable from userland: `fstat`'s `Stat`, a kernel write, and -//! `spawn`'s `SpawnArgs`, a kernel read whose `argv_ptr` and `argv_len` the +//! `spawn`'s `SpawnArgs`, a kernel read whose `path_ptr` and ranges the //! kernel then acts on. //! //! **The verdict is the bytes**: each straddling value is read back field by @@ -84,19 +84,15 @@ fn main() { unsafe { syscall::close(handle) }; // 2. `SpawnArgs` straddling, read whole: the child it names starts. - let argv = b"/system/bin/echo\0straddle\0"; + const PATH: &str = "/system/bin/echo"; let echo = image::image_of("/system/bin/echo"); - let laid = image::laid(&echo); + let laid = image::laid(&echo, "/system/bin/echo\0straddle"); unsafe { - core::ptr::copy_nonoverlapping(argv.as_ptr(), base as *mut u8, argv.len()); + core::ptr::copy_nonoverlapping(PATH.as_ptr(), base as *mut u8, PATH.len()); } let args = SpawnArgs { path_ptr: base, - path_len: "/system/bin/echo".len() as u64, - argv_ptr: base, - argv_len: argv.len() as u64, - env_ptr: 0, - env_len: 0, + path_len: PATH.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs deleted file mode 100644 index 3cb14b511ff..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! The kernel must survive a process that hands `SYS_SPAWN` absurd path, argv -//! or env, and refuse each at its boundary. -//! -//! `path_len`, `argv_len` and `env_len` are raw `u64` fields of a user-supplied -//! `SpawnArgs`. The path is copied, the argv blob is split into a `Vec<&str>` -//! (16 bytes per token) and the env blob is copied with `to_vec()`, so each one -//! sizes a kernel allocation. An argv with no non-empty token also has no `argv[0]`. -//! -//! Every case carries a real image, and the same arguments with an honest argv -//! start a child, so each refusal is its own field's. - -#[path = "../image.rs"] -mod image; - -use std::process::Command; - -use toyos::process::Process; -use toyos_abi::syscall::{self, VmAt, VmProt, SpawnArgs, SyscallError}; - -/// The program every case hands over, whatever its argv names. -const PROGRAM: &str = "/system/bin/echo"; - -/// One contiguous mmap region — `user_slice` requires physical contiguity, -/// so a heap `String` would risk a vacuous pass on `BadAddress`. -const REGION: usize = 4 * 1024 * 1024; -/// "a\0" repeated: 1,048,576 argv tokens at 16 bytes each. -const BLOB: usize = 2 * 1024 * 1024; - -fn main() { - let region = unsafe { - syscall::vm_reserve(REGION, VmProt::ReadWrite, VmAt::ANYWHERE).unwrap_or(core::ptr::null_mut()) - }; - assert!(!region.is_null(), "mmap failed"); - for i in 0..BLOB / 2 { - unsafe { - region.add(i * 2).write(b'a'); - region.add(i * 2 + 1).write(0); - } - } - - let program = image::image_of(PROGRAM); - let laid = image::laid(&program); - let base = SpawnArgs { - path_ptr: region as u64, - path_len: 1, - argv_ptr: region as u64, - argv_len: 0, - 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), - }; - - let err = unsafe { - syscall::spawn(&SpawnArgs { argv_len: BLOB as u64, ..base }) - } - .expect_err("a 2 MiB argv blob must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for oversized argv"); - - let err = unsafe { - syscall::spawn(&SpawnArgs { - argv_len: 2, - env_ptr: region as u64, - env_len: BLOB as u64, - ..base - }) - } - .expect_err("a 2 MiB env blob must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for oversized env"); - - let err = unsafe { syscall::spawn(&SpawnArgs { path_len: BLOB as u64, argv_len: 2, ..base }) } - .expect_err("a 2 MiB path must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for oversized path"); - - // The bound is a limit, not a ban: the same arguments with an honest - // argv start a child. - let child = unsafe { syscall::spawn(&SpawnArgs { argv_len: 2, ..base }) }.expect("an honest argv starts a child"); - // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. - unsafe { Process::from_raw(child) }.wait().expect("wait for the honest child"); - - // No non-empty token: there is no argv[0] to load. - let err = unsafe { syscall::spawn(&base) }.expect_err("an empty argv must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for empty argv"); - - // The tail of the region past the blob is still mmap's zero fill. - let nuls = SpawnArgs { argv_ptr: region as u64 + BLOB as u64, argv_len: 8, ..base }; - let err = unsafe { syscall::spawn(&nuls) }.expect_err("an all-NUL argv must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for all-NUL argv"); - - // Spawn still works, so the rejections left the kernel intact. - let status = Command::new("/system/bin/echo") - .arg("spawn still works") - .status() - .expect("spawn /system/bin/echo"); - assert!(status.success(), "/system/bin/echo exited {status:?}"); - - unsafe { syscall::vm_release(region, REGION) }.expect("munmap"); - println!("oversized spawn path/argv/env and unreadable working directories rejected, spawn still usable"); -} 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 55fde282bf9..c211126a812 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs @@ -19,16 +19,11 @@ const NONE: u64 = toyos_abi::HANDLE_INVALID.0 as u64; /// Spawn this program with `maps` and `entry`, its image laid out otherwise. fn spawn(maps: &[SpawnMap], entry: u64, image_base: u64) -> 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, @@ -46,7 +41,7 @@ fn spawn(maps: &[SpawnMap], entry: u64, image_base: u64) -> Result OwnedHandle { fn spawn_under(place: RawHandle) -> Result { let argv = format!("{SELF_PATH}\0c"); let own = image::image_of(SELF_PATH); - let laid = image::laid(&own); + let laid = image::laid(&own, &argv); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_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, 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 54df65317fa..7c54124f8b5 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,14 +100,10 @@ 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 laid = image::laid(&own); + let laid = image::laid(&own, &argv); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_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: endow.as_ptr() as u64, endow_count: endow.len() as u64, labels_ptr: LABEL.as_ptr() as u64, diff --git a/tests/guest/rust-tests/src/bin/spawn_image_object.rs b/tests/guest/rust-tests/src/bin/spawn_image_object.rs index 8acac1489e4..ebaa8ea0621 100644 --- a/tests/guest/rust-tests/src/bin/spawn_image_object.rs +++ b/tests/guest/rust-tests/src/bin/spawn_image_object.rs @@ -9,9 +9,10 @@ //! runs what it had copied and faults on the rest, and the machine goes on — //! the next spawn from a fresh object runs. -use toyos::elf::program::{lay_out, BASE}; +use toyos::elf::program::lay_out; use toyos::elf::Machine; use toyos::shm::SharedMemory; +use toyos::startup::{self, Block}; use toyos::AsHandle; use toyos_abi::handle::{RawHandle, Rights}; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; @@ -30,17 +31,14 @@ fn object_of(path: &str) -> (SharedMemory, Vec) { /// 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"); + let said = startup::block(format!("{SELF}\0{CHILD}").as_bytes(), b"").expect("arguments a child starts with"); + let block = Block::new(&said, None).expect("a startup block"); + let laid = lay_out(bytes, Machine::RUNNING, u64::from(image.0)).expect("this program lays out").starting_with(&block); // 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, 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 436ca826d59..b6412ae66c4 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -81,14 +81,10 @@ fn test() { fn spawn_under(place: RawHandle) -> Result { let argv = format!("{SELF_PATH}\0child"); let own = image::image_of(SELF_PATH); - let laid = image::laid(&own); + let laid = image::laid(&own, &argv); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_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, diff --git a/tests/guest/rust-tests/src/bin/start_refuses_itself.rs b/tests/guest/rust-tests/src/bin/start_refuses_itself.rs index 64e15b99409..07ca3bd7c4b 100644 --- a/tests/guest/rust-tests/src/bin/start_refuses_itself.rs +++ b/tests/guest/rust-tests/src/bin/start_refuses_itself.rs @@ -54,16 +54,12 @@ fn own_image_tagged(tag: u64) -> Vec { /// `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); + let laid = image::laid(&image, argv); // 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, diff --git a/tests/guest/rust-tests/src/bin/startup_block.rs b/tests/guest/rust-tests/src/bin/startup_block.rs new file mode 100644 index 00000000000..e2b861e5510 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/startup_block.rs @@ -0,0 +1,86 @@ +//! A child reads what its parent laid out for it, and nothing the kernel +//! kept: its arguments and environment are bytes of the startup block +//! (`toyos::startup`), so they are whole past any bound a kernel copy had, +//! and a child whose parent laid out no block starts with neither. +//! +//! A guest test because the block is read at an address of the child's own +//! space, through the pages the kernel copies at their first touch. + +#[path = "../image.rs"] +mod image; + +use std::process::Command; + +use toyos::elf::program::lay_out; +use toyos::elf::Machine; +use toyos_abi::end::End; +use toyos_abi::syscall::{self, SpawnArgs}; + +const SELF: &str = "/system/bin/test_rs_startup_block"; +const SAID: &str = "said"; +/// Past the 64 KiB the kernel once copied of each, and past a page many times over. +const LONG: usize = 300 * 1024; + +fn long(byte: u8) -> String { + String::from_utf8(vec![byte; LONG]).expect("ASCII") +} + +/// The child given no block: no argument, not even its name, and no variable. +fn unsaid() { + assert_eq!(std::env::vars_os().count(), 0, "a child given no block has an environment"); +} + +/// The child given the parent's block: each argument and variable whole. +fn said() { + let args: Vec = std::env::args().collect(); + assert_eq!(args.len(), 4, "arguments: {}", args.len()); + assert_eq!((args[0].as_str(), args[1].as_str(), args[3].as_str()), (SELF, SAID, "last")); + assert!(args[2] == long(b'a'), "the long argument is {} bytes, or not the parent's", args[2].len()); + assert!(std::env::var("LONG").is_ok_and(|v| v == long(b'e')), "the long variable is not the parent's"); + assert_eq!(std::env::var("AFTER").as_deref(), Ok("the variable after it")); + assert_eq!(std::env::current_dir().expect("a working directory").to_str(), Some("/tmp")); +} + +/// This program started from its ranges alone, as `lay_out` leaves them. +fn spawn_unsaid() -> End { + let own = image::image_of(SELF); + let laid = lay_out(own.bytes(), Machine::RUNNING, own.handle()).expect("this program lays out"); + let args = SpawnArgs { + path_ptr: SELF.as_ptr() as u64, + path_len: SELF.len() as u64, + 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), + }; + // SAFETY: every pointer names a live local for the whole call. + let child = unsafe { syscall::spawn(&args) }.expect("a spawn with no block"); + syscall::process_wait(child).expect("wait for the child given no block") +} + +fn main() { + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + _ if args.is_empty() => return unsaid(), + Some(SAID) => return said(), + _ => {} + } + let status = Command::new(SELF) + .args([SAID, &long(b'a'), "last"]) + .env("LONG", long(b'e')) + .env("AFTER", "the variable after it") + .current_dir("/tmp") + .status() + .expect("spawn the child given a block"); + assert!(status.success(), "the child given a block: {status:?}"); + assert_eq!(spawn_unsaid(), End::Exited(0), "the child given no block"); + println!("startup_block: PASS"); +} diff --git a/tests/guest/rust-tests/src/image.rs b/tests/guest/rust-tests/src/image.rs index 47e449913f8..ad97466f5a6 100644 --- a/tests/guest/rust-tests/src/image.rs +++ b/tests/guest/rust-tests/src/image.rs @@ -6,9 +6,10 @@ use std::io::Read as _; -use toyos::elf::program::{lay_out, Program, BASE}; +use toyos::elf::program::{lay_out, Program}; use toyos::elf::Machine; use toyos::process::Image; +use toyos::startup::{self, Block}; /// `path`, read whole into a memory object of this process's own, which a /// spawn's ranges name ([`laid`]). @@ -26,9 +27,26 @@ pub fn in_tmp(path: &str) -> String { 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}")) +/// A program laid out, and the startup block its ranges name. +pub struct Laid { + program: Program, + _block: Block, +} + +impl std::ops::Deref for Laid { + type Target = Program; + + fn deref(&self) -> &Program { + &self.program + } +} + +/// `image` laid out as the SDK lays out a spawn's program, its child starting +/// with `argv`, NUL-separated, and no environment: a raw `SpawnArgs` names +/// its ranges (`maps_ptr`, `maps_count`), `entry`, `image_base` and build-id. +pub fn laid(image: &Image, argv: &str) -> Laid { + let said = startup::block(argv.as_bytes(), b"").expect("arguments a child starts with"); + let block = Block::new(&said, None).unwrap_or_else(|e| panic!("a startup block: {e:?}")); + let program = lay_out(image.bytes(), Machine::RUNNING, image.handle()).unwrap_or_else(|why| panic!("lay out an image: {why}")); + Laid { program: program.starting_with(&block), _block: block } } From 405f58253f0f95ada6bf8e34f691bda8151ad769 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 00:46:46 +0200 Subject: [PATCH 02/11] The track names #1200 and measures the kernel it leaves kernel/src holds 215 files and 55,245 lines, from 215 and 55,386. The startup block's reservation is the module's own constant. Co-Authored-By: Claude Fable 5.2 --- issues/the-kernel-is-small-interrupts-post-and-threads-wait.md | 3 ++- sdk/toyos/src/startup.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 50ee0ba2869..83c83698cfd 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 @@ -23,6 +23,7 @@ kernel's part of a restart, and named module tests hold each program's What the loader named has left since (#1210): 214 files and 55,303 lines, 75 fewer than the inventory below counts, and no link to the device-path parser. + A child's arguments and environment have left since (#1200). 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 @@ -173,7 +174,7 @@ Beside that order, each waiting on something outside it: (`toyos::files::starting_in`). A child's arguments and environment, that `PWD` among it, are a startup block its parent lays out below its program (`toyos::startup`), which the kernel neither stores nor - answers (#PRNUM): `SYS_GET_ENV` (73) and `ProcessData::env` are + answers (#1200): `SYS_GET_ENV` (73) and `ProcessData::env` are gone. The kernel still keeps one userland text of each process, the path its parent read the program from (`SpawnArgs::path_ptr`), which the spawn's record and a crash's frames name the file by and diff --git a/sdk/toyos/src/startup.rs b/sdk/toyos/src/startup.rs index 5e7df7d1ba9..3b80a5a5ede 100644 --- a/sdk/toyos/src/startup.rs +++ b/sdk/toyos/src/startup.rs @@ -27,7 +27,7 @@ use crate::{AsHandle, BorrowedHandle}; /// The bytes below the program kept for the block: its pages, and reserved /// ones up to the program. -pub const RESERVED: u64 = 2 * 1024 * 1024; +const RESERVED: u64 = 2 * 1024 * 1024; /// Where the block lies in every process. pub const AT: u64 = BASE - RESERVED; From 980f29a3fa48bbe709ad394302ae9c9c420167cd Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 01:15:34 +0200 Subject: [PATCH 03/11] Review r1 of #1200: the block's two children cannot pass as each other startup_block's children exit with a code of their own once their checks hold, and the parent asserts each: a child told to read 300 KiB that found nothing took the other child's role and exited 0, which was accepted. issues/no-user-address-is-drawn-per-spawn.md records the startup block as a fourth constant address, the one whose bytes another party chooses, with its exit: it moves with the image. toyos::startup has one constructor, from arguments and environment, so no caller can hand Program::starting_with a block past its reservation. The track says the kernel keeps two userland texts, the endowment labels being the other. abuse_spawn_maps moves a program's range past the user half, not the block's page. Co-Authored-By: Claude Fable 5.2 --- docs/src/architecture/kernel.md | 6 +-- issues/a-spawn-drops-an-empty-argument.md | 2 +- issues/no-user-address-is-drawn-per-spawn.md | 8 ++++ ...-small-interrupts-post-and-threads-wait.md | 7 ++- sdk/std/sys/pal/mod.rs | 1 - sdk/toyos/src/elf/program.rs | 9 ++-- sdk/toyos/src/spawn.rs | 7 +-- sdk/toyos/src/startup.rs | 46 ++++++++++++------- .../rust-tests/src/bin/abuse_spawn_maps.rs | 2 +- .../rust-tests/src/bin/spawn_image_object.rs | 5 +- .../guest/rust-tests/src/bin/startup_block.rs | 17 ++++--- tests/guest/rust-tests/src/image.rs | 5 +- 12 files changed, 71 insertions(+), 44 deletions(-) diff --git a/docs/src/architecture/kernel.md b/docs/src/architecture/kernel.md index 8246e96ade1..b1fc1d9ea4f 100644 --- a/docs/src/architecture/kernel.md +++ b/docs/src/architecture/kernel.md @@ -17,9 +17,9 @@ It holds no filesystem, no storage, input, network, audio or USB driver, no network protocol, no audio mixing, no login and no naming policy, and parses nothing one program writes for another. What a parent tells the child it starts, its arguments and environment, is memory the parent lays out in the -child: the kernel keeps none of it and knows a process by a label. The firmware's AML is interpreted by a -userland ACPI service, which tells the kernel the little it needs, such as how -to power off. +child: the kernel keeps none of it and knows a process by a label. The +firmware's AML is interpreted by a userland ACPI service, which tells the +kernel the little it needs, such as how to power off. ## Interrupts post, threads wait diff --git a/issues/a-spawn-drops-an-empty-argument.md b/issues/a-spawn-drops-an-empty-argument.md index bd39d411a11..05ea24c1e7c 100644 --- a/issues/a-spawn-drops-an-empty-argument.md +++ b/issues/a-spawn-drops-an-empty-argument.md @@ -8,7 +8,7 @@ opened: 2026-10-20 A spawn's `argv` is one `NUL`-separated run of bytes (`toyos::spawn::Spawn`), which the startup block's writer splits skipping every empty string -(`toyos::startup::block`), as the kernel did while it copied `argv`. So a +(`toyos::startup::Block::new`), as the kernel did while it copied `argv`. So a child is never handed an empty argument, and a program asked to run `cmd "" x` sees `cmd x`. An argument that is not UTF-8 reaches the block and a C child as given, and a Rust child's `std::env::args_os` as an empty one diff --git a/issues/no-user-address-is-drawn-per-spawn.md b/issues/no-user-address-is-drawn-per-spawn.md index 62e413f2c6c..62ae4264afe 100644 --- a/issues/no-user-address-is-drawn-per-spawn.md +++ b/issues/no-user-address-is-drawn-per-spawn.md @@ -17,3 +17,11 @@ draws 32 bits for the image and the mmap base (`CONFIG_ARCH_MMAP_RND_BITS`, three bases is set 88 to 168 times, and each base claims Linux's bits or its shortfall is filed as a defect. **Mutation**: `BASE + n * 2 MiB`; a constant seed. **Oracle**: Linux's bits. + +A fourth address is constant since #1200: the startup block +(`toyos::startup::AT`), below the image. It is the only one of the four whose +bytes another party chooses: writable, up to 2 MiB, written by the parent, +and in a launched program holding strings of a caller with less authority than +the row. **Exit**: the block has no address of its own. It moves with the +image, found by every SDK copy through the process's module list +(`toyos::tls`), and the exit above counts its base with the other three. 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 83c83698cfd..fb7ff0bb9a6 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 @@ -175,10 +175,13 @@ Beside that order, each waiting on something outside it: that `PWD` among it, are a startup block its parent lays out below its program (`toyos::startup`), which the kernel neither stores nor answers (#1200): `SYS_GET_ENV` (73) and `ProcessData::env` are - gone. The kernel still keeps one userland text of each process, the + gone. The kernel still keeps two userland texts of each process: the path its parent read the program from (`SpawnArgs::path_ptr`), which the spawn's record and a crash's frames name the file by and - `symbolize` opens; its last component is the process's name. `/tmp` is the + `symbolize` opens, its last component the process's name; and its + endowment labels, kept for its life and answered through + `SYS_ENDOWMENTS` as the environment was, the next candidate for the + block. `/tmp` is the MEMORY file server's, one for the machine, of which each installed package's `/tmp` is its own directory (#1181); per session, as the owner ruled ("Per session", 2026-10-03), is the isolation track's stage 3. diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index 564496d2cf1..260af060069 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -7,7 +7,6 @@ pub mod tls; #[path = "../../../../rust/library/std/src/sys/pal/unsupported/common.rs"] mod unsupported_common; - pub use unsupported_common::{cleanup, init}; /// The kernel's word as the `ErrorKind` a caller can act on. diff --git a/sdk/toyos/src/elf/program.rs b/sdk/toyos/src/elf/program.rs index ff33f78a1aa..80a5a428f9a 100644 --- a/sdk/toyos/src/elf/program.rs +++ b/sdk/toyos/src/elf/program.rs @@ -60,10 +60,11 @@ fn reserved(lo: u64, hi: u64) -> SpawnMap { /// The program `file`, an image for `machine`, laid out with its lowest page /// at [`BASE`], each `PT_LOAD` copied from `object`, the memory object holding -/// `file`, below it a startup block that says nothing. 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, or a segment asks to be written and executed, -/// which no page is, as the loader refuses a library's (`crate::dl`). +/// `file`, below it a startup block that says nothing. 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, +/// 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) -> Result { let layout = Layout::parse(file, machine).map_err(|e| e.as_str())?; if layout.program_headers().is_none() { diff --git a/sdk/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index f312e6c22ff..06e3b030fe8 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -207,11 +207,12 @@ impl Helper<'_> { 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"))?; - let said = crate::startup::block(s.argv, &env) - .ok_or(Refused::Invalid("arguments and an environment past what a child starts with"))?; // Held until the spawn answers: the child copies its pages from it. let job = s.endow.iter().find(|(label, _)| *label == JOB_LABEL).map(|(_, job)| job.as_handle()); - let block = crate::startup::Block::new(&said, job).map_err(Refused::Syscall)?; + let block = crate::startup::Block::new(s.argv, &env, job).map_err(|why| match why { + crate::startup::Refused::TooLarge => Refused::Invalid("arguments and an environment past what a child starts with"), + crate::startup::Refused::Memory(e) => Refused::Syscall(e), + })?; let program = crate::elf::program::lay_out(image.bytes(), crate::elf::Machine::RUNNING, image.handle()) .map_err(Refused::Invalid)? .starting_with(&block); diff --git a/sdk/toyos/src/startup.rs b/sdk/toyos/src/startup.rs index 3b80a5a5ede..f46d76145e1 100644 --- a/sdk/toyos/src/startup.rs +++ b/sdk/toyos/src/startup.rs @@ -39,9 +39,8 @@ fn strings(run: &[u8]) -> impl Iterator { run.split(|&b| b == 0).filter(|s| !s.is_empty()) } -/// The block that says `args` and `env`, each a run of strings a NUL -/// separates, an empty one skipped. `None` past [`MAX_BLOCK`]. -pub fn block(args: &[u8], env: &[u8]) -> Option> { +/// The bytes of the block that says `args` and `env`. `None` past [`MAX_BLOCK`]. +fn bytes(args: &[u8], env: &[u8]) -> Option> { let argc = strings(args).count(); let words = argc + strings(env).count() + 3; let text: usize = strings(args).chain(strings(env)).map(|s| s.len() + 1).sum(); @@ -66,6 +65,15 @@ pub fn block(args: &[u8], env: &[u8]) -> Option> { Some(block) } +/// Why no block was made. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refused { + /// The arguments and environment are past [`MAX_BLOCK`]. + TooLarge, + /// The kernel made no memory for it. + Memory(SyscallError), +} + /// A block in a memory object, which a spawn names as the pages its child /// starts with at [`AT`] (`Program::starting_with`) and may drop once the /// spawn has answered. @@ -75,15 +83,18 @@ pub struct Block { } impl Block { - /// `bytes`, which [`block`] made, in memory counted to the job `job` is a - /// handle to carrying `WRITE`, the one the child runs in, or else to this - /// process's own. - pub fn new(bytes: &[u8], job: Option>) -> Result { + /// The block that says `args` and `env`, each a run of strings a NUL + /// separates, an empty one skipped, in memory counted to the job `job` is + /// a handle to carrying `WRITE`, the one the child runs in, or else to + /// this process's own. + pub fn new(args: &[u8], env: &[u8], job: Option>) -> Result { + let bytes = bytes(args, env).ok_or(Refused::TooLarge)?; let memory = match job { - Some(job) => SharedMemory::create_for(job, bytes.len())?, - None => SharedMemory::create(bytes.len())?, - }; - memory.bytes().write(0, bytes); + Some(job) => SharedMemory::create_for(job, bytes.len()), + None => SharedMemory::create(bytes.len()), + } + .map_err(Refused::Memory)?; + memory.bytes().write(0, &bytes); Ok(Block { memory, len: bytes.len() }) } @@ -105,8 +116,9 @@ impl Block { /// A program may write them, as C lets `main` write its `argv`. pub fn args() -> (usize, *const *const u8) { let at = AT as *const u64; - // SAFETY: every process starts with the block's first page mapped at - // `AT`, and a block's pointers follow its count. + // SAFETY: a program `crate::elf::program::lay_out` laid out starts with + // the block's first page mapped at `AT`, and a block's pointers follow + // its count. unsafe { (at.read() as usize, at.add(1).cast()) } } @@ -141,7 +153,7 @@ mod tests { /// every pointer the address its string lies at once the block is at `AT`. #[test] fn a_block_s_pointers_name_its_strings_in_order() { - let block = block(b"/bin/echo\0-n\0hello world", b"HOME=/home/toy\0PWD=/\0").unwrap(); + let block = bytes(b"/bin/echo\0-n\0hello world", b"HOME=/home/toy\0PWD=/\0").unwrap(); assert_eq!(u64::from_ne_bytes(block[..8].try_into().unwrap()), 3); let words: Vec<_> = (1..8).map(|word| string(&block, word)).collect(); assert_eq!( @@ -154,16 +166,16 @@ mod tests { /// Nothing said is a count of zero and two nulls: what a page of zeros reads as. #[test] fn a_block_that_says_nothing_is_zeros() { - assert_eq!(block(b"", b"\0\0").unwrap(), [0u8; 24]); + assert_eq!(bytes(b"", b"\0\0").unwrap(), [0u8; 24]); } /// A block ends below the program with a page to spare, and one byte more is refused. #[test] fn a_block_past_its_reservation_is_refused() { let fits = alloc::vec![b'a'; MAX_BLOCK - 4 * 8 - 1]; - assert_eq!(block(&fits, b"").map(|block| block.len()), Some(MAX_BLOCK)); + assert_eq!(bytes(&fits, b"").map(|block| block.len()), Some(MAX_BLOCK)); let mut past = fits; past.push(b'a'); - assert_eq!(block(&past, b""), None); + assert_eq!(bytes(&past, b""), None); } } 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 c211126a812..41e8356dfeb 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs @@ -71,7 +71,7 @@ fn main() { 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 past the user half", with(&|m| m[text].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[text].copied = m[text].len + 1), laid.entry, invalid); diff --git a/tests/guest/rust-tests/src/bin/spawn_image_object.rs b/tests/guest/rust-tests/src/bin/spawn_image_object.rs index ebaa8ea0621..85ba945688b 100644 --- a/tests/guest/rust-tests/src/bin/spawn_image_object.rs +++ b/tests/guest/rust-tests/src/bin/spawn_image_object.rs @@ -12,7 +12,7 @@ use toyos::elf::program::lay_out; use toyos::elf::Machine; use toyos::shm::SharedMemory; -use toyos::startup::{self, Block}; +use toyos::startup::Block; use toyos::AsHandle; use toyos_abi::handle::{RawHandle, Rights}; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; @@ -31,8 +31,7 @@ fn object_of(path: &str) -> (SharedMemory, Vec) { /// Spawn the program `bytes`, laid out from the object `image` names. fn spawn(image: RawHandle, bytes: &[u8]) -> Result { - let said = startup::block(format!("{SELF}\0{CHILD}").as_bytes(), b"").expect("arguments a child starts with"); - let block = Block::new(&said, None).expect("a startup block"); + let block = Block::new(format!("{SELF}\0{CHILD}").as_bytes(), b"", None).expect("a startup block"); let laid = lay_out(bytes, Machine::RUNNING, u64::from(image.0)).expect("this program lays out").starting_with(&block); // SAFETY: every pointer names a live local for the whole call. unsafe { diff --git a/tests/guest/rust-tests/src/bin/startup_block.rs b/tests/guest/rust-tests/src/bin/startup_block.rs index e2b861e5510..360c9f1f544 100644 --- a/tests/guest/rust-tests/src/bin/startup_block.rs +++ b/tests/guest/rust-tests/src/bin/startup_block.rs @@ -18,6 +18,9 @@ use toyos_abi::syscall::{self, SpawnArgs}; const SELF: &str = "/system/bin/test_rs_startup_block"; const SAID: &str = "said"; +/// What each child exits with once its checks hold, so neither can pass as the other. +const SAID_HELD: i32 = 41; +const UNSAID_HELD: i32 = 42; /// Past the 64 KiB the kernel once copied of each, and past a page many times over. const LONG: usize = 300 * 1024; @@ -26,12 +29,13 @@ fn long(byte: u8) -> String { } /// The child given no block: no argument, not even its name, and no variable. -fn unsaid() { +fn unsaid() -> ! { assert_eq!(std::env::vars_os().count(), 0, "a child given no block has an environment"); + std::process::exit(UNSAID_HELD); } /// The child given the parent's block: each argument and variable whole. -fn said() { +fn said() -> ! { let args: Vec = std::env::args().collect(); assert_eq!(args.len(), 4, "arguments: {}", args.len()); assert_eq!((args[0].as_str(), args[1].as_str(), args[3].as_str()), (SELF, SAID, "last")); @@ -39,6 +43,7 @@ fn said() { assert!(std::env::var("LONG").is_ok_and(|v| v == long(b'e')), "the long variable is not the parent's"); assert_eq!(std::env::var("AFTER").as_deref(), Ok("the variable after it")); assert_eq!(std::env::current_dir().expect("a working directory").to_str(), Some("/tmp")); + std::process::exit(SAID_HELD); } /// This program started from its ranges alone, as `lay_out` leaves them. @@ -69,8 +74,8 @@ fn spawn_unsaid() -> End { fn main() { let args: Vec = std::env::args().collect(); match args.get(1).map(String::as_str) { - _ if args.is_empty() => return unsaid(), - Some(SAID) => return said(), + _ if args.is_empty() => unsaid(), + Some(SAID) => said(), _ => {} } let status = Command::new(SELF) @@ -80,7 +85,7 @@ fn main() { .current_dir("/tmp") .status() .expect("spawn the child given a block"); - assert!(status.success(), "the child given a block: {status:?}"); - assert_eq!(spawn_unsaid(), End::Exited(0), "the child given no block"); + assert_eq!(status.code(), Some(SAID_HELD), "the child given a block: {status:?}"); + assert_eq!(spawn_unsaid(), End::Exited(UNSAID_HELD), "the child given no block"); println!("startup_block: PASS"); } diff --git a/tests/guest/rust-tests/src/image.rs b/tests/guest/rust-tests/src/image.rs index ad97466f5a6..e489d74225d 100644 --- a/tests/guest/rust-tests/src/image.rs +++ b/tests/guest/rust-tests/src/image.rs @@ -9,7 +9,7 @@ use std::io::Read as _; use toyos::elf::program::{lay_out, Program}; use toyos::elf::Machine; use toyos::process::Image; -use toyos::startup::{self, Block}; +use toyos::startup::Block; /// `path`, read whole into a memory object of this process's own, which a /// spawn's ranges name ([`laid`]). @@ -45,8 +45,7 @@ impl std::ops::Deref for Laid { /// with `argv`, NUL-separated, and no environment: a raw `SpawnArgs` names /// its ranges (`maps_ptr`, `maps_count`), `entry`, `image_base` and build-id. pub fn laid(image: &Image, argv: &str) -> Laid { - let said = startup::block(argv.as_bytes(), b"").expect("arguments a child starts with"); - let block = Block::new(&said, None).unwrap_or_else(|e| panic!("a startup block: {e:?}")); + let block = Block::new(argv.as_bytes(), b"", None).unwrap_or_else(|e| panic!("a startup block: {e:?}")); let program = lay_out(image.bytes(), Machine::RUNNING, image.handle()).unwrap_or_else(|why| panic!("lay out an image: {why}")); Laid { program: program.starting_with(&block), _block: block } } From 5dd0711f1cfac60909552aedbe548c3b5585b75a Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 01:34:26 +0200 Subject: [PATCH 04/11] tests: three T14 members that the startup block reddened The T14's boots at cf06504fa passed as boots and three members did not: - spawn_child_ends_first and abuse_handle_table aborted (exit 134): each fills its handle table and then spawns, and the helper made the child's startup block inside that spawn, where no slot is left for its object. Each now lays its child out, block and all, before the table fills, as it already read its image. - process_tree panicked: it looks in the roster for the `cat` its shell detached, and a launched process now goes by its row's file, `toybox`, which `cat` is a link to. Co-Authored-By: Claude Fable 5.2 --- .../rust-tests/src/bin/abuse_handle_table.rs | 20 ++++++++++++------- .../guest/rust-tests/src/bin/process_tree.rs | 3 ++- .../src/bin/spawn_child_ends_first.rs | 19 +++++++++--------- 3 files changed, 24 insertions(+), 18 deletions(-) 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 360e17fb614..94ea5d4ce31 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -53,11 +53,18 @@ const REGION: usize = 4 * 1024 * 1024; /// be in range or the refusal would be the label check rather than the repeat. const LABELS: &[u8] = b"twice"; -/// This binary as an image, read before the table is filled: a full table has -/// no slot for the object a read makes. -fn own() -> &'static toyos::process::Image { - static OWN: std::sync::OnceLock = std::sync::OnceLock::new(); - OWN.get_or_init(|| image::image_of(SELF)) +/// This binary laid out to start as [`CHILD`], read before the table is +/// filled: a full table has no slot for the objects a read and a startup +/// block make. +fn own() -> &'static image::Laid { + // The image is kept: the ranges name its object. + static OWN: std::sync::OnceLock<(toyos::process::Image, image::Laid)> = std::sync::OnceLock::new(); + let (_, laid) = OWN.get_or_init(|| { + let image = image::image_of(SELF); + let laid = image::laid(&image, &format!("{SELF}\0{CHILD}")); + (image, laid) + }); + laid } fn main() { @@ -191,8 +198,7 @@ fn a_pair_is_refused_whole(filled: &mut Vec) { /// `SYS_SPAWN` of this binary as a child that exits at once, carrying /// `entries` into the blob `labels`. fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result { - let argv = format!("{SELF}\0{CHILD}"); - let laid = image::laid(&own(), &argv); + let laid = own(); // SAFETY: every pointer names a live local for the whole call. unsafe { syscall::spawn(&SpawnArgs { diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 0c4a7eb260e..474c7f93747 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -368,7 +368,8 @@ fn a_detached_program_outlives_its_shell() { let input = shell.stdin.take().expect("the input the shell and cat read"); let status = shell.wait().expect("wait the shell"); assert!(status.success(), "the shell's detach failed: {status:?}"); - let live = live_named(&["cat"]); + // A launched process goes by its row's file, which `cat` is a link to. + let live = live_named(&["toybox"]); assert_eq!(live.len(), 1, "the cat the shell detached ended with the shell: {live:?}"); drop(input); println!(" a detached cat outlived its shell"); 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 7c54124f8b5..4b09629d7cd 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 @@ -20,7 +20,6 @@ mod image; use toyos_abi::syscall::{self, debug_action, EndowEntry, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; use toyos_abi::end::End; -use toyos::process::Image; const SELF_PATH: &str = "/system/bin/test_rs_spawn_child_ends_first"; @@ -40,10 +39,12 @@ fn main() { } fn test() { - // Read once, before the table is filled: reading it takes handles. + // Laid out once, before the table is filled: an image and a startup block take handles. let own = image::image_of(SELF_PATH); + let laid = |role: &str| image::laid(&own, &format!("{SELF_PATH}\0{role}")); + let (exit, speak) = (laid("exit"), laid("speak")); hold_the_next_spawn(); - let child = spawn(&own, "exit", None).expect("a spawn whose child ended inside it"); + let child = spawn(&exit, None).expect("a spawn whose child ended inside it"); assert_eq!( syscall::process_wait_nonblock(child), Ok(End::Exited(CODE)), @@ -51,14 +52,14 @@ fn test() { ); // SAFETY: no type holds this number. unsafe { syscall::close(child) }; - a_spawn_from_a_full_table_starts_no_child(&own); + a_spawn_from_a_full_table_starts_no_child(&speak); println!("spawn_child_ends_first: the spawn answered a child that had already ended, and a refused one started none"); } /// A spawn whose caller's table has no slot for its answer is refused with /// nothing of the child started: the pipe its child would have spoken into /// ends empty. The mark is left standing, since the spawn never lands. -fn a_spawn_from_a_full_table_starts_no_child(own: &Image) { +fn a_spawn_from_a_full_table_starts_no_child(speak: &image::Laid) { let ends = syscall::pipe().expect("a pipe for the child to speak into"); let mut filled = Vec::new(); let full = loop { @@ -70,7 +71,7 @@ fn a_spawn_from_a_full_table_starts_no_child(own: &Image) { }; assert_eq!(full, SyscallError::ResourceExhausted, "the table did not fill"); hold_the_next_spawn(); - let spawned = spawn(own, "speak", Some(ends.write)); + let spawned = spawn(speak, Some(ends.write)); for handle in filled { // SAFETY: no type holds this number. unsafe { syscall::close(handle) }; @@ -93,14 +94,12 @@ fn hold_the_next_spawn() { ); } -/// `SYS_SPAWN` of `own`, this binary, in `role`, endowed `stdout` as its +/// `SYS_SPAWN` of `laid`, this binary in one role, endowed `stdout` as its /// descriptor 1, which moves only on a spawn that happens. -fn spawn(own: &Image, role: &str, stdout: Option) -> Result { - let argv = format!("{SELF_PATH}\0{role}"); +fn spawn(laid: &image::Laid, stdout: Option) -> Result { const LABEL: &str = "fd:1"; let endow: Vec = stdout.into_iter().map(|handle| EndowEntry { label_off: 0, label_len: LABEL.len() as u32, handle, _pad: 0 }).collect(); - let laid = image::laid(&own, &argv); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, From cda513c65836d1821a72471b4664f6e084a90a18 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 02:06:00 +0200 Subject: [PATCH 05/11] A process's name is a bounded label its parent chooses Naming a process for the last component of its path made every launched applet `toybox` in the roster, its exit record and a crash dump, which is a loss for `ps` and for reading a crash. SpawnArgs carries the name instead: 1 to 27 bytes of UTF-8 holding no control character and no white space (toyos_abi::syscall::process_name), bounded before it is copied and refused InvalidArgument otherwise, so a name is one token of one line wherever a record prints it. The SDK names a child for its argv[0]'s last component, as the kernel did, cut and cleaned to what the kernel takes. The name is a label and nothing trusts it. The kernel prints it and answers it in the roster. Log attribution is by the ring a record came out of, which the supervisor registers; the supervisor's rows, rights and restarts go by the handles it holds. A thread could already rename itself. process_tree looks for `cat` again. abuse_spawn_path is abuse_spawn_labels and holds the name's bound and shape beside the path's. Co-Authored-By: Claude Fable 5.2 --- docs/src/architecture/kernel.md | 7 +- ...-small-interrupts-post-and-threads-wait.md | 9 ++- kernel/src/loader/mod.rs | 11 +-- kernel/src/loader/start.rs | 9 --- kernel/src/syscall/dispatch.rs | 10 ++- kernel/src/syscall/proc.rs | 3 +- sdk/abi/src/syscall.rs | 38 +++++++++- sdk/toyos/src/spawn.rs | 46 +++++++++++- .../rust-tests/src/bin/abuse_handle_table.rs | 6 ++ .../rust-tests/src/bin/abuse_page_straddle.rs | 4 + .../rust-tests/src/bin/abuse_spawn_labels.rs | 74 +++++++++++++++++++ .../rust-tests/src/bin/abuse_spawn_maps.rs | 4 + .../rust-tests/src/bin/abuse_spawn_path.rs | 57 -------------- .../guest/rust-tests/src/bin/process_tree.rs | 7 +- .../src/bin/spawn_child_ends_first.rs | 4 + .../rust-tests/src/bin/spawn_image_object.rs | 4 + .../rust-tests/src/bin/spawn_lands_claimed.rs | 4 + .../src/bin/start_refuses_itself.rs | 4 + .../guest/rust-tests/src/bin/startup_block.rs | 4 + 19 files changed, 219 insertions(+), 86 deletions(-) create mode 100644 tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_spawn_path.rs diff --git a/docs/src/architecture/kernel.md b/docs/src/architecture/kernel.md index b1fc1d9ea4f..39302c18aac 100644 --- a/docs/src/architecture/kernel.md +++ b/docs/src/architecture/kernel.md @@ -17,9 +17,10 @@ It holds no filesystem, no storage, input, network, audio or USB driver, no network protocol, no audio mixing, no login and no naming policy, and parses nothing one program writes for another. What a parent tells the child it starts, its arguments and environment, is memory the parent lays out in the -child: the kernel keeps none of it and knows a process by a label. The -firmware's AML is interpreted by a userland ACPI service, which tells the -kernel the little it needs, such as how to power off. +child: the kernel keeps none of it. It knows a process by a short name its +parent gives it, a label that carries no authority. The firmware's AML is +interpreted by a userland ACPI service, which tells the kernel the little it +needs, such as how to power off. ## Interrupts post, threads wait 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 fb7ff0bb9a6..f89849d29ad 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 @@ -175,10 +175,11 @@ Beside that order, each waiting on something outside it: that `PWD` among it, are a startup block its parent lays out below its program (`toyos::startup`), which the kernel neither stores nor answers (#1200): `SYS_GET_ENV` (73) and `ProcessData::env` are - gone. The kernel still keeps two userland texts of each process: the - path its parent read the program from (`SpawnArgs::path_ptr`), which - the spawn's record and a crash's frames name the file by and - `symbolize` opens, its last component the process's name; and its + gone. The kernel still keeps three userland texts of each process: + its name, 27 bytes its parent chooses (`SpawnArgs::name_ptr`), which + is what a bounded process name needs; the path its parent read the + program from (`SpawnArgs::path_ptr`), which the spawn's record and a + crash's frames name the file by and `symbolize` opens; and its endowment labels, kept for its life and answered through `SYS_ENDOWMENTS` as the environment was, the next candidate for the block. `/tmp` is the diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 00436378904..15f6d04aa86 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -122,14 +122,14 @@ fn lay(space: &mut crate::mm::space::AddressSpace, maps: &[Map]) -> Result<(), S /// the child's handle table around its handle to itself once nothing is left /// to refuse. /// -/// `path` labels the program, and its last component is the name the child -/// goes by; the kernel resolves no path. +/// `path` labels the program and `name` the child; the kernel resolves no path. /// /// `Refusal`, not `-> !`, is the error type: every failure below owns a /// partly built process (address space, stack, kernel stack), and nothing /// unwinds, so the error must travel out as a value rather than strand it. pub fn spawn( path: &str, + name: [u8; crate::process::THREAD_NAME_LEN], commit: C, program: Program, parent: Parent, @@ -212,7 +212,7 @@ pub fn spawn( let ((), retire) = admission.land(guard.as_mut().unwrap(), |table, node| { table.insert(ProcessEntry::new( Arc::clone(&object), - start::make_name(path), + name, proc_data, // Two holders: a crash report on this thread reads it without the process table. Some(Arc::clone(&image)), @@ -253,7 +253,7 @@ pub fn spawn( Ok((pid, held)) } -/// The name the supervisor runs under: no file, since it runs from its boot image. +/// The name the supervisor runs under and its label: no file, since it runs from its boot image. const SUPERVISOR: &str = "supervisor"; /// Start the supervisor from its boot image as the build laid it out, holding the machine's one @@ -333,7 +333,8 @@ pub fn spawn_supervisor(images: crate::bootimage::Objects, laid: &toyos_kimage:: }; // 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, table, program, Parent::Root) { + let name = toyos_abi::syscall::process_name(SUPERVISOR.as_bytes()).expect("spawn_supervisor: its own name"); + match spawn(SUPERVISOR, name, table, 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/loader/start.rs b/kernel/src/loader/start.rs index 2a30528bf07..b17ab52e5af 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -96,15 +96,6 @@ pub(crate) fn alloc_kernel_stack(job: &Arc, start: Start) -> Option<(Kernel Some((stack, frame)) } -/// The last path component, truncated to what a process entry can hold. -pub(crate) fn make_name(path: &str) -> [u8; crate::process::THREAD_NAME_LEN] { - let filename = path.rsplit('/').next().unwrap_or(path); - let mut name = [0u8; crate::process::THREAD_NAME_LEN]; - let len = filename.len().min(crate::process::THREAD_NAME_LEN - 1); - name[..len].copy_from_slice(&filename.as_bytes()[..len]); - name -} - /// A caller's request for a child's table: `endow` has not left the caller's table yet. // The move must be last: an earlier move leaves a failed spawn's caller holding handles that name nothing. pub struct PendingHandles { diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index bddfde463b0..291542433ad 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -125,6 +125,14 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Err(refused) => return refused, }; let path = match ctx.user_str(UserAddr::new(args.path_ptr), args.path_len) { Ok(s) => s, Err(e) => return e.to_u64() }; + // Bounded before it is copied, and one token of a record's line. + if args.name_len > toyos_abi::syscall::MAX_PROCESS_NAME as u64 { + return SyscallError::InvalidArgument.to_u64(); + } + let name = match ctx.user_vec(UserAddr::new(args.name_ptr), args.name_len) { Ok(bytes) => bytes, Err(e) => return e.to_u64() }; + let Some(name) = toyos_abi::syscall::process_name(&name) else { + return SyscallError::InvalidArgument.to_u64(); + }; if args.endow_count as usize > toyos_abi::syscall::MAX_SPAWN_ENDOWMENTS || args.labels_len > toyos_abi::syscall::MAX_SPAWN_LABELS_LEN as u64 { @@ -146,7 +154,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Ok(built) => built, Err(e) => return e.refuse(), }; - sys_spawn(&path, pending, program, parent) + sys_spawn(&path, name, pending, program, parent) } SYS_PROCESS_WAIT => sys_process_wait(RawHandle(a1 as u32), a2), SYS_PROCESS_KILL => { diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 4f9104ca938..e54444d1aaf 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -105,12 +105,13 @@ pub(super) fn spawn_program(ctx: &crate::user_ptr::SyscallContext<'_>, args: &Sp /// 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, + name: [u8; toyos_abi::inventory::NAME_BYTES], pending: crate::loader::PendingHandles, 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, pending, program, parent) { + match process::spawn(path, name, pending, program, parent) { Ok((_, handle)) => u64::from(handle.0), Err(e) => e.refuse(), } diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 71a33aa0f8e..e207bf65c04 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -393,10 +393,14 @@ crate::user_safe! { #[derive(Clone, Copy)] pub struct SpawnArgs { /// The path the caller read the program from, which names it in the - /// spawn's record and a crash's, and whose last component is the name - /// the child goes by. Nothing opens it. + /// spawn's record and a crash's. Nothing opens it. pub path_ptr: u64, pub path_len: u64, + /// The name the child goes by in the roster, its exit record and a + /// crash's: a label of the caller's choosing, which names no + /// authority. `InvalidArgument` unless [`process_name`] takes it. + pub name_ptr: u64, + pub name_len: u64, /// `[EndowEntry]`, moved out of the parent's table. pub endow_ptr: u64, pub endow_count: u64, @@ -441,6 +445,22 @@ crate::user_safe! { } } +/// The most bytes of a process's name: the roster's field less its NUL. +pub const MAX_PROCESS_NAME: usize = crate::inventory::NAME_BYTES - 1; + +/// `name` as the roster spells a process's, NUL-padded: `None` unless it is 1 +/// to [`MAX_PROCESS_NAME`] bytes of UTF-8 holding no control character and no +/// white space, so a name is one token of one line wherever a record prints it. +pub fn process_name(name: &[u8]) -> Option<[u8; crate::inventory::NAME_BYTES]> { + let text = core::str::from_utf8(name).ok()?; + if text.is_empty() || text.len() > MAX_PROCESS_NAME || text.chars().any(|c| c.is_control() || c.is_whitespace()) { + return None; + } + let mut padded = [0; crate::inventory::NAME_BYTES]; + padded[..name.len()].copy_from_slice(name); + Some(padded) +} + 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 @@ -2814,6 +2834,20 @@ pub fn process_stats(proc: RawHandle, stats: &mut ProcessStats) -> Result<(), Sy mod tests { use super::*; + /// A name is one token of bounded UTF-8: the bound is a limit, and what + /// would break a record's line or its field is refused. + #[test] + fn a_process_name_is_one_bounded_token() { + let longest = [b'n'; MAX_PROCESS_NAME]; + let named = process_name(&longest).expect("the longest name"); + assert_eq!((&named[..MAX_PROCESS_NAME], named[MAX_PROCESS_NAME]), (&longest[..], 0)); + assert_eq!(process_name(b"cat").map(|n| n[..4].to_vec()), Some(b"cat\0".to_vec())); + assert!(process_name("h\u{e9}llo".as_bytes()).is_some()); + for refused in [&b""[..], &[b'n'; MAX_PROCESS_NAME + 1], b"a\nb", b"a b", b"a\0b", b"a\x1b[0m", b"\xff"] { + assert_eq!(process_name(refused), None, "{refused:?}"); + } + } + /// 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/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index 06e3b030fe8..85c4c01b0d3 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -19,14 +19,15 @@ //! it too. The one endowed under [`JOB_LABEL`], or else this process's own. use toyos_abi::syscall::{ - self, EndowEntry, SpawnArgs, SyscallError, FD_PREFIX, JOB_LABEL, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, - SVC_LABEL, + self, EndowEntry, SpawnArgs, SyscallError, FD_PREFIX, JOB_LABEL, MAX_PROCESS_NAME, MAX_SPAWN_ENDOWMENTS, + MAX_SPAWN_LABELS_LEN, SVC_LABEL, }; use crate::fd::Kind as FdKind; use crate::ipc::IpcError; use crate::launch::{self, Launch, LaunchError, Outcome, MAX_LAUNCH_EXTRAS, MAX_LAUNCH_FDS}; use crate::process::{Image, Process}; +use alloc::string::String; use alloc::vec::Vec; use crate::{AsHandle, BorrowedHandle, OwnedHandle, RawHandle}; @@ -205,6 +206,7 @@ impl Helper<'_> { let s = self.0; let image = image.ok_or(Refused::Invalid("a spawn names the program it runs"))?; let path = image_and_name(s.program, s.image).0; + let name = name(s.argv.split(|&b| b == 0).find(|arg| !arg.is_empty()).unwrap_or(s.program.as_bytes())); 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"))?; // Held until the spawn answers: the child copies its pages from it. @@ -273,6 +275,8 @@ impl Helper<'_> { let args = SpawnArgs { path_ptr: path.as_ptr() as u64, path_len: path.len() as u64, + name_ptr: name.as_ptr() as u64, + name_len: name.len() as u64, endow_ptr: entries.as_ptr() as u64, endow_count: count as u64, labels_ptr: labels.as_ptr() as u64, @@ -391,6 +395,26 @@ fn decimal(mut n: u32, buf: &mut [u8; 10]) -> &[u8] { } } +/// The name a child run as `program`, its `argv[0]`, goes by in the kernel's +/// records (`SpawnArgs::name_ptr`): its last component, cut at a character to +/// [`MAX_PROCESS_NAME`] bytes, with `_` for a character a name does not hold, +/// and `?` where that leaves nothing. +pub fn name(program: &[u8]) -> String { + let text = String::from_utf8_lossy(program); + let mut name = String::new(); + for c in text.rsplit('/').next().unwrap_or_default().chars() { + let c = if c.is_control() || c.is_whitespace() { '_' } else { c }; + if name.len() + c.len_utf8() > MAX_PROCESS_NAME { + break; + } + name.push(c); + } + if name.is_empty() { + name.push('?'); + } + name +} + /// What a helper runs, and the name it runs it under: the image named, else /// the program; `argv[0]` is the program's either way. pub fn image_and_name<'a>(program: &'a str, image: Option<&'a str>) -> (&'a str, &'a str) { @@ -441,6 +465,24 @@ mod tests { } } + /// Whatever a program is called, its name is one the kernel takes, and an + /// applet's is the applet's. + #[test] + fn a_name_is_the_program_s_last_component_as_the_kernel_takes_one() { + use toyos_abi::syscall::process_name; + assert_eq!(name(b"/system/bin/cat"), "cat"); + assert_eq!(name(b"ls"), "ls"); + assert_eq!(name(b"/apps/My App/run\tme"), "run_me"); + assert_eq!(name(b"/system/bin/test_rs_spawn_child_ends_first"), "test_rs_spawn_child_ends_fi"); + assert_eq!(name("/x/".as_bytes()), "?"); + assert_eq!(name(b""), "?"); + let wide = "\u{e9}".repeat(20); + assert_eq!(name(wide.as_bytes()), "\u{e9}".repeat(13)); + for program in [&b"/system/bin/cat"[..], b"", b"a b\n", b"\xff\xfe", wide.as_bytes(), &[b'n'; 100]] { + assert!(process_name(name(program).as_bytes()).is_some(), "{program:?}"); + } + } + /// A spawn that names its image runs that image under its program's name: /// `argv[0]` is the program, never the image's path. #[test] 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 94ea5d4ce31..94cfd76970c 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -38,6 +38,8 @@ use toyos_abi::RawHandle; use toyos_abi::end::End; const SELF: &str = "/system/bin/test_rs_abuse_handle_table"; +/// The name its children go by. +const NAME: &str = "test_rs_abuse_handle_table"; /// The role a spawned copy of this binary takes: it exits at once. const CHILD: &str = "endowed"; @@ -87,6 +89,8 @@ fn main() { let args = SpawnArgs { path_ptr: path as u64, path_len: PATH.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: (region as u64) + (REGION / 4) as u64, endow_count, labels_ptr: (region as u64) + (REGION / 8) as u64, @@ -204,6 +208,8 @@ fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result(); const CANARY: u8 = 0xA5; @@ -93,6 +95,8 @@ fn main() { let args = SpawnArgs { path_ptr: base, path_len: PATH.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs new file mode 100644 index 00000000000..6e355fdb7b1 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs @@ -0,0 +1,74 @@ +//! The kernel must survive a process that hands `SYS_SPAWN` an absurd path or +//! name, and refuse each at its boundary. +//! +//! `path_len` and `name_len` are raw `u64` fields of a user-supplied +//! `SpawnArgs`. The path is copied, so its length sizes a kernel allocation; +//! the name is printed in every record of the child, so it is one bounded +//! token (`toyos_abi::syscall::process_name`). The same arguments with an +//! honest path and name start a child, so each refusal is its own field's. + +#[path = "../image.rs"] +mod image; + +use toyos::process::Process; +use toyos_abi::syscall::{self, SpawnArgs, SyscallError, VmAt, VmProt, MAX_PROCESS_NAME}; + +const PROGRAM: &str = "/system/bin/echo"; + +/// One reservation, so that every byte of an absurd length is readable and +/// its refusal is not `BadAddress`. +const BLOB: usize = 2 * 1024 * 1024; + +fn main() { + // SAFETY: a fresh reservation nothing else names. + let region = unsafe { syscall::vm_reserve(BLOB, VmProt::ReadWrite, VmAt::ANYWHERE) }.expect("a reservation"); + // SAFETY: `BLOB` writable bytes of the reservation. + unsafe { region.write_bytes(b'a', BLOB) }; + + let program = image::image_of(PROGRAM); + let laid = image::laid(&program, PROGRAM); + let honest = SpawnArgs { + path_ptr: region as u64, + path_len: 1, + name_ptr: region as u64, + name_len: MAX_PROCESS_NAME as u64, + 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), + }; + let named = |name: &[u8]| SpawnArgs { name_ptr: name.as_ptr() as u64, name_len: name.len() as u64, ..honest }; + let refused: [(&str, SpawnArgs); 7] = [ + ("a 2 MiB path", SpawnArgs { path_len: BLOB as u64, ..honest }), + ("a 2 MiB name", SpawnArgs { name_len: BLOB as u64, ..honest }), + ("a name one byte past the bound", SpawnArgs { name_len: MAX_PROCESS_NAME as u64 + 1, ..honest }), + ("no name", SpawnArgs { name_len: 0, ..honest }), + ("a name of two lines", named(b"a\nb")), + ("a name of two words", named(b"a b")), + ("a name that is not UTF-8", named(b"\xff")), + ]; + for (what, args) in refused { + // SAFETY: every pointer names a live buffer for the whole call. + let answer = unsafe { syscall::spawn(&args) }; + assert_eq!(answer, Err(SyscallError::InvalidArgument), "{what}"); + println!("abuse_spawn_labels: {what}: refused"); + } + + // The bounds are limits: the longest name there is starts a child. + // SAFETY: as above. + let child = unsafe { syscall::spawn(&honest) }.expect("an honest path and the longest name start a child"); + // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. + unsafe { Process::from_raw(child) }.wait().expect("wait for the honest child"); + + // SAFETY: the reservation made above, which nothing names any more. + unsafe { syscall::vm_release(region, BLOB) }.expect("release"); + println!("abuse_spawn_labels: PASS"); +} 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 41e8356dfeb..d4420ae7628 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs @@ -14,6 +14,8 @@ use toyos_abi::syscall::{self, SpawnArgs, SpawnMap, SyscallError, VmProt, MAX_SP use toyos_abi::RawHandle; const SELF: &str = "/system/bin/test_rs_abuse_spawn_maps"; +/// The name its children go by. +const NAME: &str = "test_rs_abuse_spawn_maps"; const CHILD: &str = "spawned-from-checked-ranges"; const NONE: u64 = toyos_abi::HANDLE_INVALID.0 as u64; @@ -24,6 +26,8 @@ fn spawn(maps: &[SpawnMap], entry: u64, image_base: u64) -> Result Result { let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, @@ -368,8 +372,7 @@ fn a_detached_program_outlives_its_shell() { let input = shell.stdin.take().expect("the input the shell and cat read"); let status = shell.wait().expect("wait the shell"); assert!(status.success(), "the shell's detach failed: {status:?}"); - // A launched process goes by its row's file, which `cat` is a link to. - let live = live_named(&["toybox"]); + let live = live_named(&["cat"]); assert_eq!(live.len(), 1, "the cat the shell detached ended with the shell: {live:?}"); drop(input); println!(" a detached cat outlived its shell"); 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 4b09629d7cd..bfa2a5947ad 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 @@ -22,6 +22,8 @@ use toyos_abi::RawHandle; use toyos_abi::end::End; const SELF_PATH: &str = "/system/bin/test_rs_spawn_child_ends_first"; +/// The name its children go by. +const NAME: &str = "test_rs_spawn_child_ends_fi"; /// What the child exits with. const CODE: i32 = 7; @@ -103,6 +105,8 @@ fn spawn(laid: &image::Laid, stdout: Option) -> Result Result { syscall::spawn(&SpawnArgs { path_ptr: SELF.as_ptr() as u64, path_len: SELF.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, 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 b6412ae66c4..9f757b4ea88 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -25,6 +25,8 @@ use std::os::toyos::process::ExitStatusExt; use toyos_abi::end::End; const SELF_PATH: &str = "/system/bin/test_rs_spawn_lands_claimed"; +/// The name its children go by. +const NAME: &str = "test_rs_spawn_lands_claimed"; /// The name the place's namespace carries the port back to the test under. const BACK: &str = "back"; @@ -85,6 +87,8 @@ fn spawn_under(place: RawHandle) -> Result { let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 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 index 07ca3bd7c4b..867dcf64748 100644 --- a/tests/guest/rust-tests/src/bin/start_refuses_itself.rs +++ b/tests/guest/rust-tests/src/bin/start_refuses_itself.rs @@ -16,6 +16,8 @@ use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; const DIR: &str = "/tmp/start_refuses_itself"; +/// The name its children go by. +const NAME: &str = "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"; @@ -60,6 +62,8 @@ fn spawn_endowed_nothing(path: &str, argv: &str) -> Result End { let args = SpawnArgs { path_ptr: SELF.as_ptr() as u64, path_len: SELF.len() as u64, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, From 5d5fb7fd3557d919072e5b41cac6100d9c243e01 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 03:18:34 +0200 Subject: [PATCH 06/11] Review r2 of #1200: the name's record closes, and the path's opens issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md is closed. Its forged name, `test_rs_job pid=1 code=0`, is refused at spawn: a name is 1 to 27 bytes of UTF-8 with no control character and no white space (toyos_abi::syscall::process_name), so it cannot carry a record's separators. The claim is held by sdk/abi's `a_process_name_is_one_bounded_token` on the host and by abuse_spawn_path_and_name's two-word and two-line names on the T14's boot. `make_name`, which the record cited, went with the name field. The same forgery through the path is not closed and has its own record, issues/a-spawns-record-quotes-the-path-its-caller-states.md: `spawn: {path} pid=` writes a label its caller states, unchecked and unescaped. Read, not run. build/bootlog.rs and tests/common/metal.rs say where a recorded name comes from now, the parent's SDK, and what `recorded_name` must agree with. abuse_spawn_labels is abuse_spawn_path_and_name, since "labels" is the endowment blob in this ABI. The SDK's `name` is private. The kernel spells a name's array one way, THREAD_NAME_LEN, which is the roster's NAME_BYTES, and `name_str` no longer answers `?` for a name that cannot be. The stall's sighting says the run's serial artifact ends at the console's hand-over and holds no exit record of the nested shell. Co-Authored-By: Claude Fable 5.2 --- build/bootlog.rs | 12 ++++-- ...nto-the-kernels-own-records-unsanitised.md | 31 -------------- ...ecord-quotes-the-path-its-caller-states.md | 41 +++++++++++++++++++ kernel/src/process.rs | 4 +- kernel/src/syscall/proc.rs | 2 +- sdk/toyos/src/spawn.rs | 2 +- tests/common/metal.rs | 8 ++-- ...labels.rs => abuse_spawn_path_and_name.rs} | 4 +- 8 files changed, 59 insertions(+), 45 deletions(-) delete mode 100644 issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md create mode 100644 issues/a-spawns-record-quotes-the-path-its-caller-states.md rename tests/guest/rust-tests/src/bin/{abuse_spawn_labels.rs => abuse_spawn_path_and_name.rs} (96%) diff --git a/build/bootlog.rs b/build/bootlog.rs index e86d0366694..ae9a888a2e8 100644 --- a/build/bootlog.rs +++ b/build/bootlog.rs @@ -82,12 +82,16 @@ pub fn kernel_records(log: &str) -> String { /// trailing ` online` as a separate word: the same head carries the failure. pub const AP_BRINGUP: &str = "SMP: AP cpu"; -/// `kernel/src/process.rs`'s `THREAD_NAME_LEN`, one byte of which is the -/// terminator `make_name` leaves. +/// `toyos_abi::inventory::NAME_BYTES`, one byte of which is the name's +/// terminator. const NAME_LEN: usize = 28; -/// A process's name as the kernel records it: the path's last component, -/// truncated to what [`NAME_LEN`] holds. +/// The name the kernel records a process run as `binary` under: what its +/// parent's SDK states at the spawn (`toyos::spawn::name` in +/// `sdk/toyos/src/spawn.rs`), which this agrees with for every name of +/// ASCII letters, digits and punctuation: the last component, cut to what +/// [`NAME_LEN`] holds. That function cuts at a character and writes `_` for +/// white space and a control character, which this does not. /// /// **A predicate looking for the whole name finds nothing on a good boot**: /// `test_rs_lent_device_memory_refused` is `test_rs_lent_device_memory_` on the wire. diff --git a/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md b/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md deleted file mode 100644 index 9a53ad2ef93..00000000000 --- a/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-23 ---- - -# A process name is written into the kernel's own records unsanitised - -`make_name` (`kernel/src/loader/start.rs`) keeps a spawned file's name as the -process-table name, and the kernel writes that name into records a judge parses -by field: `exit: {name} pid=N code=N cpu=Nms` (`kernel/src/process.rs`) is the -verdict channel `bootlog::EXIT` and the metal judge's `Boot::exit_code` -(`tests/common/metal.rs`) read. A program -that can write and spawn a file whose name holds a space and the record's own -words — `test_rs_job pid=1 code=0`, inside `THREAD_NAME_LEN` — gets a kernel -record that opens as the job's own: `exit: test_rs_job pid=1 code=0 pid=7 -code=3 cpu=0ms`. `exit_code("test_rs_job")` matches its head, takes the -first `pid=` and `code=` after it, and keeps the record with the lowest pid, -which is the one the forger named. Not run: the reading of the two functions. - -A program's own lines are not this: they reach `/log` under the head `logkeeper` -writes for them, whose name is the supervisor's and refused outside -`[A-Za-z0-9._+-]` (`toyos_logstream::Tag`). This is the kernel's record quoting -a name it does not clean. - -## Exit condition - -A name the kernel writes into a record it or a judge parses by field cannot -carry that record's separators — sanitised where it is written, or refused at -spawn — and a test that spawns a file named after a record's fields reads the -real exit code out of `/log`. diff --git a/issues/a-spawns-record-quotes-the-path-its-caller-states.md b/issues/a-spawns-record-quotes-the-path-its-caller-states.md new file mode 100644 index 00000000000..a6038baac0c --- /dev/null +++ b/issues/a-spawns-record-quotes-the-path-its-caller-states.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-11-11 +--- + +# A spawn's record quotes the path its caller states + +`SYS_SPAWN` takes the program's path as a label, any UTF-8 up to 64 KiB, which +nothing opens and nothing checks (`SpawnArgs::path_ptr`, read by `user_str` in +`kernel/src/syscall/dispatch.rs`), and the kernel writes it into its own +record as it came: `spawn: {path} pid=N (total=Nms)` and each refusal's +`spawn: {path}: …` (`kernel/src/loader/mod.rs`). A crash frame escapes the +same text (`toyos_symbols::frame`); the spawn record does not. + +So a program that may spawn can have the kernel write a record that reads as +another program's start, two ways: + +- by stating another program's path. A child labelled + `/system/bin/logkeeper` is recorded `spawn: /system/bin/logkeeper pid=N`, + the head `bootlog::LOGKEEPER_SPAWN` and the audio judges' `spawn: + /system/bin/{job} ` (`tests/common/audio.rs`) look for; +- by stating a path that holds the record's own words: a space, `pid=7`, or a + second `spawn: `, which a judge that searches a line for a head finds + (`tests/toyos.rs`, `tests/common/claims.rs`, `tests/common/qemu.rs`). + +By reading, not run. The name beside the path is refused at spawn unless it is +one bounded token (`toyos_abi::syscall::process_name`); the path has no such +rule. + +Owner: the kernel track +(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), whose +record names the path as userland text the kernel still keeps. + +**Exit**: the kernel's spawn record cannot be read as another process's: +either the record names the child by what the kernel itself knows of it (its +pid and its checked name) and the path leaves the kernel, or the path is +escaped where it is written as a crash frame's is. A guest test that spawns a +child labelled `/system/bin/logkeeper pid=1` reads one record in `/log`, and +no judge takes it for logkeeper's; red on the kernel that writes the label as +it came. diff --git a/kernel/src/process.rs b/kernel/src/process.rs index a8152622d54..19f617182d6 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -108,7 +108,7 @@ impl KernelMapping { pub const KERNEL_STACK_SIZE: usize = 128 * 1024; /// How much of a process or thread name the table keeps, fixed by the `SYS_SYSINFO` record's name field. -pub const THREAD_NAME_LEN: usize = 28; +pub const THREAD_NAME_LEN: usize = toyos_abi::inventory::NAME_BYTES; /// Per-thread metadata, keyed by its tid in `ProcessEntry::threads`. pub struct ThreadEntry { @@ -192,7 +192,7 @@ impl ProcessEntry { pub fn pid(&self) -> Pid { self.pid } pub fn name(&self) -> &[u8; THREAD_NAME_LEN] { &self.name } pub fn name_str(&self) -> &str { - core::str::from_utf8(&self.name).unwrap_or("?").trim_end_matches('\0') + core::str::from_utf8(&self.name).expect("a process's name is UTF-8, as `process_name` took it").trim_end_matches('\0') } pub fn process_data(&self) -> &Arc> { &self.process_data } pub fn main_tid(&self) -> Tid { self.main_tid } diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index e54444d1aaf..ede3437188e 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -105,7 +105,7 @@ pub(super) fn spawn_program(ctx: &crate::user_ptr::SyscallContext<'_>, args: &Sp /// 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, - name: [u8; toyos_abi::inventory::NAME_BYTES], + name: [u8; process::THREAD_NAME_LEN], pending: crate::loader::PendingHandles, program: crate::loader::Program, parent: process::Parent, diff --git a/sdk/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index 85c4c01b0d3..f478c61fb98 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -399,7 +399,7 @@ fn decimal(mut n: u32, buf: &mut [u8; 10]) -> &[u8] { /// records (`SpawnArgs::name_ptr`): its last component, cut at a character to /// [`MAX_PROCESS_NAME`] bytes, with `_` for a character a name does not hold, /// and `?` where that leaves nothing. -pub fn name(program: &[u8]) -> String { +fn name(program: &[u8]) -> String { let text = String::from_utf8_lossy(program); let mut name = String::new(); for c in text.rsplit('/').next().unwrap_or_default().chars() { diff --git a/tests/common/metal.rs b/tests/common/metal.rs index e792f31fdda..c9d797dfceb 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -301,10 +301,10 @@ impl Readback { /// What the kernel recorded about the process the *runner* spawned as /// `binary`, or why there is no such record. /// - /// **The name is the file's, truncated the way the kernel truncates it.** - /// `ProcessEntry`'s name is `THREAD_NAME_LEN` bytes and the loader fills it - /// from the path's last component, so a binary whose name is longer than - /// that is recorded under a prefix: the staged `lent_device_memory_refused` + /// **The name is the file's, cut the way its parent's SDK cuts it** + /// (`bootlog::recorded_name`, which agrees with `toyos::spawn::name`): a + /// process's name is at most 27 bytes, so a binary whose name is longer + /// is recorded under a prefix: the staged `lent_device_memory_refused` /// ends `…_device_memory_` on the wire, and a predicate looking for the whole /// name would find nothing on a perfectly good boot. /// diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_path_and_name.rs similarity index 96% rename from tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs rename to tests/guest/rust-tests/src/bin/abuse_spawn_path_and_name.rs index 6e355fdb7b1..d6ac930b1e4 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_path_and_name.rs @@ -59,7 +59,7 @@ fn main() { // SAFETY: every pointer names a live buffer for the whole call. let answer = unsafe { syscall::spawn(&args) }; assert_eq!(answer, Err(SyscallError::InvalidArgument), "{what}"); - println!("abuse_spawn_labels: {what}: refused"); + println!("abuse_spawn_path_and_name: {what}: refused"); } // The bounds are limits: the longest name there is starts a child. @@ -70,5 +70,5 @@ fn main() { // SAFETY: the reservation made above, which nothing names any more. unsafe { syscall::vm_release(region, BLOB) }.expect("release"); - println!("abuse_spawn_labels: PASS"); + println!("abuse_spawn_path_and_name: PASS"); } From 9b6f8fb2c9cb245aedfa2d55c5bed645054eab79 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 03:59:56 +0200 Subject: [PATCH 07/11] process_tree's judge stops counting a kernel line no spawn prints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T14's judge counted two kernel records, `spawn: /tmp/process_tree_invalid_elf: …`, one per B. Since #1189 the spawner lays a program out, so 64 zero bytes are refused in the caller and the kernel hears of no spawn: nothing prints that line, and the judge was red on every boot whose job passed. The job itself asserts the refusal in each B, which A's checks cannot pass without, so the count goes. The depth refusal, which only the kernel says, stays. Co-Authored-By: Claude Fable 5.2 --- tests/guest/rust-tests/src/bin/process_tree.rs | 2 +- tests/toyos.rs | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 41862024e23..4e594e7a57f 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -408,7 +408,7 @@ fn park() -> ! { fn b() -> ! { let conn = endow::service(BACK).expect("A endowed a port back"); - // The supplied image reaches ELF validation after admission under B. + // No program: the spawn lays it out here and is refused before the kernel hears of it. assert!(Command::new(INVALID_PATH).spawn().is_err(), "an invalid executable started"); let c = Command::new(SELF_PATH).arg("c").stdin(Stdio::null()).spawn().expect("B spawns C"); let d = Command::new(HELD).independent().spawn().expect("B launches D"); diff --git a/tests/toyos.rs b/tests/toyos.rs index 23851775012..3ca403aea66 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -6484,19 +6484,12 @@ fn pci_cap_selftest(log: &str) -> Result<(), String> { Ok(()) } -/// `process_tree` passed, and the kernel said the two things the guest cannot -/// see: each B's first spawn was refused by the loader, past the admission that -/// holds B, and the chain's one depth refusal names `MAX_DEPTH` + 1. +/// `process_tree` passed, and the kernel said what the guest cannot see: the +/// chain's one depth refusal names `MAX_DEPTH` + 1. fn process_tree(back: &metal::Readback) -> Result<(), String> { back.job_passed("test_rs_process_tree")?; let kernel = back.kernel(); let log = kernel.text(); - let unloaded = log.lines().filter(|l| l.contains("spawn: /tmp/process_tree_invalid_elf: ")).count(); - if unloaded != 2 { - return Err(format!( - "the loader refused the invalid executable {unloaded} times, not once per B" - )); - } let refused: Vec<&str> = log.lines().filter(|l| l.contains("spawn: refused under pid ")).collect(); if refused.len() != 1 || !refused[0].contains("at depth 65, more than 64 below the supervisor") { return Err(format!("the kernel's depth refusals were {refused:?}, not one naming depth 65")); From b16d3f4a33c2436b832f89e60c92c67bbfd097eb Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 04:05:42 +0200 Subject: [PATCH 08/11] Review r3 of #1200: the host's name length is the ABI's, and the path's record says its worst case build/bootlog.rs held kernel/src/process.rs to the text `THREAD_NAME_LEN: usize = 28`, which 98853704a changed, so `the_kernel_writes_the_records_the_host_reads` was red on every host from that commit. NAME_LEN is toyos_abi::inventory::NAME_BYTES, the constant the kernel's is defined as, and the needle goes; the test's doc no longer says the kernel truncates a name or that nothing links the crates. issues/a-spawns-record-quotes-the-path-its-caller-states.md states the line break: a path carrying a newline and a whole kernel record puts a line in /log that a judge reads as a verdict, from a spawn that need not succeed. Its exit is the orchestrator's decision in two parts, one for the forged line and one for the stated path. The stall's sighting gives the three UART logs' ends as they are. Co-Authored-By: Claude Fable 5.2 --- build/bootlog.rs | 11 ++- ...ecord-quotes-the-path-its-caller-states.md | 68 ++++++++++++------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/build/bootlog.rs b/build/bootlog.rs index ae9a888a2e8..3fa68520fbd 100644 --- a/build/bootlog.rs +++ b/build/bootlog.rs @@ -82,9 +82,8 @@ pub fn kernel_records(log: &str) -> String { /// trailing ` online` as a separate word: the same head carries the failure. pub const AP_BRINGUP: &str = "SMP: AP cpu"; -/// `toyos_abi::inventory::NAME_BYTES`, one byte of which is the name's -/// terminator. -const NAME_LEN: usize = 28; +/// The roster's name field, one byte of which is the name's terminator. +const NAME_LEN: usize = toyos_abi::inventory::NAME_BYTES; /// The name the kernel records a process run as `binary` under: what its /// parent's SDK states at the spawn (`toyos::spawn::name` in @@ -503,16 +502,14 @@ mod tests { assert_eq!(stop_marked(""), Err(Unfit::NoStopMark)); } - /// The two kernel spellings a metal readback is judged on, and the length - /// it truncates a name to — held to the kernel's own source, because - /// nothing links this crate to it either. + /// The kernel spellings a metal readback is judged on, held to the + /// kernel's own source, because nothing links this crate to it. #[test] fn the_kernel_writes_the_records_the_host_reads() { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for (file, needle) in [ ("kernel/src/process.rs", format!("log!(\"{EXIT}{{name}} pid=")), ("kernel/src/arch/x86_64/smp.rs", format!("log!(\"{AP_BRINGUP}")), - ("kernel/src/process.rs", format!("THREAD_NAME_LEN: usize = {NAME_LEN}")), ("kernel/src/deadline.rs", format!("\"{DEADLINE_ARMED}{{ms}} ms")), ] { let at = root.join(file); diff --git a/issues/a-spawns-record-quotes-the-path-its-caller-states.md b/issues/a-spawns-record-quotes-the-path-its-caller-states.md index a6038baac0c..a79bb7e0ccc 100644 --- a/issues/a-spawns-record-quotes-the-path-its-caller-states.md +++ b/issues/a-spawns-record-quotes-the-path-its-caller-states.md @@ -7,35 +7,53 @@ opened: 2026-11-11 # A spawn's record quotes the path its caller states `SYS_SPAWN` takes the program's path as a label, any UTF-8 up to 64 KiB, which -nothing opens and nothing checks (`SpawnArgs::path_ptr`, read by `user_str` in -`kernel/src/syscall/dispatch.rs`), and the kernel writes it into its own -record as it came: `spawn: {path} pid=N (total=Nms)` and each refusal's -`spawn: {path}: …` (`kernel/src/loader/mod.rs`). A crash frame escapes the -same text (`toyos_symbols::frame`); the spawn record does not. - -So a program that may spawn can have the kernel write a record that reads as -another program's start, two ways: - -- by stating another program's path. A child labelled - `/system/bin/logkeeper` is recorded `spawn: /system/bin/logkeeper pid=N`, - the head `bootlog::LOGKEEPER_SPAWN` and the audio judges' `spawn: - /system/bin/{job} ` (`tests/common/audio.rs`) look for; -- by stating a path that holds the record's own words: a space, `pid=7`, or a - second `spawn: `, which a judge that searches a line for a head finds - (`tests/toyos.rs`, `tests/common/claims.rs`, `tests/common/qemu.rs`). +nothing opens and nothing checks but for UTF-8 (`SpawnArgs::path_ptr`, read by +`user_str` in `kernel/src/syscall/dispatch.rs`). The kernel writes it into its +own records as it came: `spawn: {path} pid=N (total=Nms)` for a spawn that +lands and `spawn: {path}: …` for each one it refuses +(`kernel/src/loader/mod.rs`), so the spawn need not succeed. A crash frame +escapes the same text (`toyos_symbols::frame`); these records do not. + +A program that may call `SYS_SPAWN` can therefore have the kernel write, three +ways: + +1. **A whole forged line.** Nothing between the call and `/log` touches a + control character: `Message::write_str` (`kernel/src/log/mod.rs`) copies + the text, `LogRecord`'s `Display` (`sdk/abi/src/log.rs`) writes the message + raw, and `logkeeper` writes that (`system/logkeeper/src/main.rs`). A path of + `x`, a newline and `[2026-11-11 10:00:00 1.000 cpu0 kernel] exit: + test_rs_job pid=1 code=0 cpu=0ms` puts a line in `/log` that + `toyos_logstream::parse` reads as a kernel record. `Readback::exit_code` + (`tests/common/metal.rs`) takes the first `pid=` and `code=` after the + job's name and keeps the record with the lowest pid: the forged line is a + test's verdict. The same line can wear any program's head. +2. **A record that reads as another program's start**, by stating that + program's path. A child labelled `/system/bin/logkeeper` is recorded + `spawn: /system/bin/logkeeper pid=N`, the head `bootlog::LOGKEEPER_SPAWN` + and the audio judges' `spawn: /system/bin/{job} ` (`tests/common/audio.rs`) + look for. The label holds nothing to escape. +3. **A record carrying another record's words on its own line**: a space, + `pid=7`, or a second `spawn: `, which a judge that searches a line for a + head finds (`tests/toyos.rs`, `tests/common/claims.rs`, + `tests/common/qemu.rs`). By reading, not run. The name beside the path is refused at spawn unless it is -one bounded token (`toyos_abi::syscall::process_name`); the path has no such -rule. +one bounded token (`toyos_abi::syscall::process_name`), which a record's head +never fitted in; the path has no such rule. Every reader of these records is a +judge in `tests/` or `build/`, or a person: nothing in `system/`, `apps/`, +`sdk/` or `lib/` decides by one. Owner: the kernel track (`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), whose record names the path as userland text the kernel still keeps. -**Exit**: the kernel's spawn record cannot be read as another process's: -either the record names the child by what the kernel itself knows of it (its -pid and its checked name) and the path leaves the kernel, or the path is -escaped where it is written as a crash frame's is. A guest test that spawns a -child labelled `/system/bin/logkeeper pid=1` reads one record in `/log`, and -no judge takes it for logkeeper's; red on the kernel that writes the label as -it came. +**Exit**, the orchestrator's decision (2026-11-11, on #1200), in two parts: + +- (a) The spawn boundary refuses a path label carrying a control character, + the rule the name has, and `abuse_spawn_path_and_name` holds it: a path + with a newline is `InvalidArgument`, red on the kernel that takes it. That + closes way 1. +- (b) No judge in `tests/` or `build/` decides by a `spawn:` record's path, + since it is its caller's label: each reads what the kernel itself knows of + the child, or the program's own lines under the head the supervisor + registered. That closes ways 2 and 3. From bd6513ce1e592fe59179c03328061e21d3d8618f Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 05:22:45 +0200 Subject: [PATCH 09/11] issues: process_tree's judge record closes, and the inventory loses what #1200 took issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md is closed by its exit's second way: the judge's count is gone (`process_tree` in tests/toyos.rs), and the T14's `proctreecase` boot of 98853704a is judged green by it, `process_tree` exiting 0. What the count meant to hold, a spawn refused after its admission giving back what admission took, is held on the host by kernel/proclife's tree tests, and on the T14 by abuse_spawn_maps, whose refusals come after admission and whose honest spawn runs after every one. The job keeps its own assertion that 64 zero bytes start nothing. The sighting of tty_interrupt's stall this branch added is dropped: main closed that record (ecb718f5f). The kernel's inventory, which main wrote while this was in review, no longer lists a child's arguments and environment as still held, and its ranking and sums lose that row. kernel/src is 215 files and 55,239 lines on this tree, from main's 55,379. Co-Authored-By: Claude Fable 5.2 --- ...waits-for-a-kernel-line-no-spawn-prints.md | 33 ------------------- ...-small-interrupts-post-and-threads-wait.md | 32 +++++++++++------- 2 files changed, 21 insertions(+), 44 deletions(-) delete mode 100644 issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md diff --git a/issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md b/issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md deleted file mode 100644 index 7b65fbfe35e..00000000000 --- a/issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-11-11 ---- - -# process_tree's metal judge waits for a kernel line no spawn prints - -The metal `process_tree` row's judge (`process_tree` in `tests/toyos.rs`) -requires the kernel's log to say `spawn: /tmp/process_tree_invalid_elf: ` -twice, once per B: the kernel loader refusing the 64 zero bytes the job -writes there. Since the spawner lays a program out itself -(`toyos::elf::program::lay_out` in `Helper::spawn`, `sdk/toyos/src/spawn.rs`), -an image that is no ELF is refused in the caller before any syscall, so the -kernel never sees it and never prints the line. The job still passes: B -asserts only that the spawn failed. - -Evidence: the T14's `proctreecase` boot of #1203 at e78085e47. The job ended -`===TEST_END test_rs_process_tree exit=0===`, the kernel's log holds no line -naming `/tmp/process_tree_invalid_elf`, and the judge answered `the loader -refused the invalid executable 0 times, not once per B`; the row's other -three tests passed. #1203 changes no kernel, spawn or `process_tree` code. -https://github.com/ToyOSOrg/evidence/tree/eff6d9e767dc90c4472908660613872f68f76dbb/r3. - -So the row is red on every bench, and what it meant to hold, a spawn refused -after admission under B releasing what admission took, is no longer reached -by this job. - -Exit: the job reaches a kernel refusal after admission again, with an image -that lays out and the kernel refuses, and the judge counts that line; or the -arm and the judge's count go, if a cheaper test holds that release. - -Owner: the guest-suite track. 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 f89849d29ad..a56921d5224 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 @@ -341,8 +341,10 @@ Beside that order, each waiting on something outside it: ## What the kernel still holds -An inventory at `9349705ce` (main, 2026-11-10): `git ls-files -'kernel/src/*.rs'` is 215 files and 55,378 lines by `wc -l`. Each file is +An inventory at `9349705ce` (main, 2026-11-10), when `git ls-files +'kernel/src/*.rs'` was 215 files and 55,378 lines by `wc -l`; #1200 has since +taken a child's arguments and environment out, about 140 lines of the +process area below. Each file is counted once, in the area of its main responsibility. "Kept" is the design read against the code; "inferred" marks a reading the design does not state. Nothing here is ruled, and the ranking is a proposal. @@ -386,11 +388,15 @@ the first. | What | Size | Dependents | Owner in the design, and what blocks it | |---|---|---|---| -| A child's arguments and environment | about 140, by #1200's count | std, libc, `toyos::startup` | the parent's startup block: #1200 | | Endowment labels: a blob per process for its life, answered by `SYS_ENDOWMENTS`, of which the kernel compares three (`JOB_LABEL`, `SELF_LABEL`, `QUIT_LABEL`) and writes three more for the supervisor | ~130 | `toyos::endow`, the supervisor's table | the startup block (inferred); needs the child's handle numbers known to its parent and the three compared labels as typed spawn fields; after #1200 | | The TTY mark on a pipe end (`SYS_MARK_TTY`, `FileType::Tty`) | ~40 | std's spawn, libc's `isatty` and stdio, std's stdio | a fact about a stream that only libc and std read; a label or the block could carry it (inferred); after the labels | +<<<<<<< HEAD | The main thread's stack: 8 MiB and a guard the kernel chooses and reserves | ~30 | `SYS_STACK_INFO`, which only a guest test calls | its creator's, since the design gives a thread's stack a caller-chosen size; after #1200, when a child's stack starts empty | | **Done** (#1210): the loader writes one record, the kernel hands it to the supervisor as a read-only boot image, and the supervisor parses it. What the loader named: partition GUIDs by role, in the inventory, and the boot sources `SYS_BOOT_SOURCES` answers, whose device paths `loaded.rs` parses (`toyos-devicepath`) only to refuse the loader's own bytes | ~95, and the kernel's link to a 324-line parser | the supervisor, `toyos::syscap`, the manifest's inventory reader | the design gives the kernel a typed description of what was loaded and keeps a partition GUID out of it; passing the description on unread, as a read-only boot image as ROOT is, fits both (inferred) and waits on no other change | +======= +| The main thread's stack: 8 MiB and a guard the kernel chooses and reserves | ~30 | `SYS_STACK_INFO`, which only a guest test calls | its creator's, since the design gives a thread's stack a caller-chosen size; a child's stack starts empty since #1200 | +| What the loader named: partition GUIDs by role, in the inventory, and the boot sources `SYS_BOOT_SOURCES` answers, whose device paths `loaded.rs` parses (`toyos-devicepath`) only to refuse the loader's own bytes | ~95, and the kernel's link to a 324-line parser | the supervisor, `toyos::syscap`, the manifest's inventory reader | the design gives the kernel a typed description of what was loaded and keeps a partition GUID out of it; passing the description on unread, as a read-only boot image as ROOT is, fits both (inferred) and waits on no other change | +>>>>>>> 8918af1e3 (issues: process_tree's judge record closes, and the inventory loses what #1200 took) | The IGD OpRegion's lookup: a firmware window is a granted privilege the design keeps, but `SYS_DEVICE_FIRMWARE_WINDOW` finds it by reading Intel graphics' `ASLS` | ~90 and ~30 of `pcidev` | tgl-display, the supervisor, the manifest | the lookup is device knowledge; a window the ACPI service names for a claim would be generic (inferred); needs a design | | The supervisor's last words, userland text written into the kernel's ring (`SYS_LOG_LAST_WORDS`) | ~20 | the supervisor | the design keeps userland output out of the kernel's ring and names no exception; this is one | @@ -415,22 +421,26 @@ lines), `issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands- By mechanisms eliminated: -1. #1200: `SYS_GET_ENV`, the environment kept per process, the kernel's - write onto a child's stack. -2. Endowment labels, after #1200: `SYS_ENDOWMENTS`, the per-process blob +1. Endowment labels: `SYS_ENDOWMENTS`, the per-process blob and its charge, and label text the kernel compares. +<<<<<<< HEAD 3. What the loader named, independent of the others: `SYS_BOOT_SOURCES`, the inventory's partition records, the device-path parser. **Done** (#1210). 4. The TTY mark, with or after 2: `SYS_MARK_TTY`. It meets +======= +2. What the loader named, independent of the others: `SYS_BOOT_SOURCES`, + the inventory's partition records, the device-path parser. +3. The TTY mark, with or after 1: `SYS_MARK_TTY`. It meets +>>>>>>> 8918af1e3 (issues: process_tree's judge record closes, and the inventory loses what #1200 took) `issues/a-received-handle-has-no-knowable-type.md`, which wants `SYS_FSTAT` to answer more, so the two are designed together. -5. The main thread's stack, after #1200: `SYS_STACK_INFO` and the kernel's +4. The main thread's stack: `SYS_STACK_INFO` and the kernel's two stack constants. -By lines: none is large. The five ranked are about 435 of 55,378; with the +By lines: none is large. The four ranked are about 295 of 55,239; with the OpRegion's lookup (~120, which needs a design) and the last words (~20), -the table's rows are about 575. What the design gives userland is nearly +the table's rows are about 435. What the design gives userland is nearly out, and a larger number needs one of the design changes above. ### Dead or nearly so, recorded and not deleted @@ -443,8 +453,8 @@ out, and a larger number needs one of the design changes above. line, went with #1210.) - `KernelStack::heap` (`kernel/src/loader/start.rs`) charges every thread two strings at 64 KiB and a page, "as a rename's paths". No rename is - left: a spawn's path and arguments are the two strings today. After #1200 - no call copies two, and one string of that bound remains for as long as + left, and since #1200 took a spawn's arguments out no call copies two: + one string of that bound remains for as long as a spawn's path or `SYS_NAMESPACE_OPEN`'s name, which nothing bounds below 64 KiB, is copied; the page is the namespace names'. - `FileType::File`, `Directory` and `Symlink` are answered by no kernel From 6516df42f68bf6e44ff4ce1233a519cee5da734e Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 06:38:28 +0200 Subject: [PATCH 10/11] Review r4 of #1200: process_tree's header says where B's first spawn is refused In the caller, before the kernel hears of it, not by the loader after admission. Three lines for three, so no panic location moves. Co-Authored-By: Claude Fable 5.2 --- tests/guest/rust-tests/src/bin/process_tree.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 4e594e7a57f..d4d072795c5 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -9,9 +9,9 @@ //! - E, which the supervisor started under its session's owner, runs on: detached is the one way to outlive a starter; //! - after the kill, a spawn and a launch under B's `self` answer `Gone`. //! -//! B's first act is a spawn the loader refuses once it is admitted under B: its -//! hold on B goes with it, or B is never published and the arm's wait never -//! answers. +//! B's first act is a spawn of 64 zero bytes, which lay out as no program: it +//! is refused in B, before the kernel hears of it, and B asserts that nothing +//! started. //! //! **The other arms.** A `MANAGE`-only handle is no place, and the supervisor refuses a //! launch whose place is a pipe and answers the launches after it. A chain — From 8670c0df521a23f44c8360981658fe9b444b14df Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 09:23:07 +0200 Subject: [PATCH 11/11] issues: the track after the rebase over #1210 The rebase's resolution of the inventory left conflict markers in the commit before this one. The section is main's again, with what #1200 did marked done as #1210 marked its own, and the track's measurement says the kernel this branch leaves. Co-Authored-By: Claude Fable 5.2 --- ...-small-interrupts-post-and-threads-wait.md | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) 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 a56921d5224..5c0a9ad6ac9 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 @@ -23,7 +23,8 @@ kernel's part of a restart, and named module tests hold each program's What the loader named has left since (#1210): 214 files and 55,303 lines, 75 fewer than the inventory below counts, and no link to the device-path parser. - A child's arguments and environment have left since (#1200). + A child's arguments and environment have left since (#1200): 214 files and + 55,163 lines. 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 @@ -341,10 +342,8 @@ Beside that order, each waiting on something outside it: ## What the kernel still holds -An inventory at `9349705ce` (main, 2026-11-10), when `git ls-files -'kernel/src/*.rs'` was 215 files and 55,378 lines by `wc -l`; #1200 has since -taken a child's arguments and environment out, about 140 lines of the -process area below. Each file is +An inventory at `9349705ce` (main, 2026-11-10): `git ls-files +'kernel/src/*.rs'` is 215 files and 55,378 lines by `wc -l`. Each file is counted once, in the area of its main responsibility. "Kept" is the design read against the code; "inferred" marks a reading the design does not state. Nothing here is ruled, and the ranking is a proposal. @@ -388,15 +387,11 @@ the first. | What | Size | Dependents | Owner in the design, and what blocks it | |---|---|---|---| +| **Done** (#1200): the parent lays them out in a startup block. A child's arguments and environment | about 140 | std, libc, `toyos::startup` | the parent's startup block | | Endowment labels: a blob per process for its life, answered by `SYS_ENDOWMENTS`, of which the kernel compares three (`JOB_LABEL`, `SELF_LABEL`, `QUIT_LABEL`) and writes three more for the supervisor | ~130 | `toyos::endow`, the supervisor's table | the startup block (inferred); needs the child's handle numbers known to its parent and the three compared labels as typed spawn fields; after #1200 | | The TTY mark on a pipe end (`SYS_MARK_TTY`, `FileType::Tty`) | ~40 | std's spawn, libc's `isatty` and stdio, std's stdio | a fact about a stream that only libc and std read; a label or the block could carry it (inferred); after the labels | -<<<<<<< HEAD -| The main thread's stack: 8 MiB and a guard the kernel chooses and reserves | ~30 | `SYS_STACK_INFO`, which only a guest test calls | its creator's, since the design gives a thread's stack a caller-chosen size; after #1200, when a child's stack starts empty | -| **Done** (#1210): the loader writes one record, the kernel hands it to the supervisor as a read-only boot image, and the supervisor parses it. What the loader named: partition GUIDs by role, in the inventory, and the boot sources `SYS_BOOT_SOURCES` answers, whose device paths `loaded.rs` parses (`toyos-devicepath`) only to refuse the loader's own bytes | ~95, and the kernel's link to a 324-line parser | the supervisor, `toyos::syscap`, the manifest's inventory reader | the design gives the kernel a typed description of what was loaded and keeps a partition GUID out of it; passing the description on unread, as a read-only boot image as ROOT is, fits both (inferred) and waits on no other change | -======= | The main thread's stack: 8 MiB and a guard the kernel chooses and reserves | ~30 | `SYS_STACK_INFO`, which only a guest test calls | its creator's, since the design gives a thread's stack a caller-chosen size; a child's stack starts empty since #1200 | -| What the loader named: partition GUIDs by role, in the inventory, and the boot sources `SYS_BOOT_SOURCES` answers, whose device paths `loaded.rs` parses (`toyos-devicepath`) only to refuse the loader's own bytes | ~95, and the kernel's link to a 324-line parser | the supervisor, `toyos::syscap`, the manifest's inventory reader | the design gives the kernel a typed description of what was loaded and keeps a partition GUID out of it; passing the description on unread, as a read-only boot image as ROOT is, fits both (inferred) and waits on no other change | ->>>>>>> 8918af1e3 (issues: process_tree's judge record closes, and the inventory loses what #1200 took) +| **Done** (#1210): the loader writes one record, the kernel hands it to the supervisor as a read-only boot image, and the supervisor parses it. What the loader named: partition GUIDs by role, in the inventory, and the boot sources `SYS_BOOT_SOURCES` answers, whose device paths `loaded.rs` parses (`toyos-devicepath`) only to refuse the loader's own bytes | ~95, and the kernel's link to a 324-line parser | the supervisor, `toyos::syscap`, the manifest's inventory reader | the design gives the kernel a typed description of what was loaded and keeps a partition GUID out of it; passing the description on unread, as a read-only boot image as ROOT is, fits both (inferred) and waits on no other change | | The IGD OpRegion's lookup: a firmware window is a granted privilege the design keeps, but `SYS_DEVICE_FIRMWARE_WINDOW` finds it by reading Intel graphics' `ASLS` | ~90 and ~30 of `pcidev` | tgl-display, the supervisor, the manifest | the lookup is device knowledge; a window the ACPI service names for a claim would be generic (inferred); needs a design | | The supervisor's last words, userland text written into the kernel's ring (`SYS_LOG_LAST_WORDS`) | ~20 | the supervisor | the design keeps userland output out of the kernel's ring and names no exception; this is one | @@ -421,26 +416,22 @@ lines), `issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands- By mechanisms eliminated: -1. Endowment labels: `SYS_ENDOWMENTS`, the per-process blob +1. #1200: `SYS_GET_ENV`, the environment kept per process, the kernel's + write onto a child's stack. **Done** (#1200). +2. Endowment labels, after #1200: `SYS_ENDOWMENTS`, the per-process blob and its charge, and label text the kernel compares. -<<<<<<< HEAD 3. What the loader named, independent of the others: `SYS_BOOT_SOURCES`, the inventory's partition records, the device-path parser. **Done** (#1210). 4. The TTY mark, with or after 2: `SYS_MARK_TTY`. It meets -======= -2. What the loader named, independent of the others: `SYS_BOOT_SOURCES`, - the inventory's partition records, the device-path parser. -3. The TTY mark, with or after 1: `SYS_MARK_TTY`. It meets ->>>>>>> 8918af1e3 (issues: process_tree's judge record closes, and the inventory loses what #1200 took) `issues/a-received-handle-has-no-knowable-type.md`, which wants `SYS_FSTAT` to answer more, so the two are designed together. -4. The main thread's stack: `SYS_STACK_INFO` and the kernel's +5. The main thread's stack, after #1200: `SYS_STACK_INFO` and the kernel's two stack constants. -By lines: none is large. The four ranked are about 295 of 55,239; with the +By lines: none is large. The five ranked are about 435 of 55,378; with the OpRegion's lookup (~120, which needs a design) and the last words (~20), -the table's rows are about 435. What the design gives userland is nearly +the table's rows are about 575. What the design gives userland is nearly out, and a larger number needs one of the design changes above. ### Dead or nearly so, recorded and not deleted @@ -453,8 +444,8 @@ out, and a larger number needs one of the design changes above. line, went with #1210.) - `KernelStack::heap` (`kernel/src/loader/start.rs`) charges every thread two strings at 64 KiB and a page, "as a rename's paths". No rename is - left, and since #1200 took a spawn's arguments out no call copies two: - one string of that bound remains for as long as + left: a spawn's path and arguments were the two strings. Since #1200 + no call copies two, and one string of that bound remains for as long as a spawn's path or `SYS_NAMESPACE_OPEN`'s name, which nothing bounds below 64 KiB, is copied; the page is the namespace names'. - `FileType::File`, `Directory` and `Symlink` are answered by no kernel