diff --git a/build/bootlog.rs b/build/bootlog.rs index e86d0366694..3fa68520fbd 100644 --- a/build/bootlog.rs +++ b/build/bootlog.rs @@ -82,12 +82,15 @@ 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. -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 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 +/// `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. @@ -499,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/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..39302c18aac 100644 --- a/docs/src/architecture/kernel.md +++ b/docs/src/architecture/kernel.md @@ -15,9 +15,12 @@ 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 -userland ACPI service, which tells the kernel the little it needs, such as how -to power off. +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. 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/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-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-spawn-drops-an-empty-argument.md b/issues/a-spawn-drops-an-empty-argument.md index d9c150ecff9..05ea24c1e7c 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::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 +(`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/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..a79bb7e0ccc --- /dev/null +++ b/issues/a-spawns-record-quotes-the-path-its-caller-states.md @@ -0,0 +1,59 @@ +--- +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 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`), 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 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. 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/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 012de414ccd..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,6 +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): 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 @@ -170,10 +172,18 @@ 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 (#1200): `SYS_GET_ENV` (73) and `ProcessData::env` are + 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 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. @@ -377,10 +387,10 @@ 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 | +| **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 | -| 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 | +| 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 | | **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 | @@ -407,7 +417,7 @@ 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. + 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. 3. What the loader named, independent of the others: `SYS_BOOT_SOURCES`, @@ -434,7 +444,7 @@ 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 + 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'. 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..15f6d04aa86 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 `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, - argv: &[&str], + name: [u8; crate::process::THREAD_NAME_LEN], 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), + name, proc_data, // Two holders: a crash report on this thread reads it without the process table. Some(Arc::clone(&image)), @@ -290,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 @@ -370,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, &[SUPERVISOR], table, Vec::new(), 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 8f64cb98a38..b17ab52e5af 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; @@ -97,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/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..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 } @@ -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..291542433ad 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -124,21 +124,15 @@ 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() }; + // 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 { @@ -160,21 +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(), }; - // 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, name, pending, program, parent) } SYS_PROCESS_WAIT => sys_process_wait(RawHandle(a1 as u32), a2), SYS_PROCESS_KILL => { @@ -280,16 +260,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..ede3437188e 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -105,14 +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, - args: &[&str], + name: [u8; process::THREAD_NAME_LEN], 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, 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 a27d64aafb3..e207bf65c04 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,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. **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. 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, + /// 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, @@ -411,7 +410,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`] @@ -444,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 @@ -1082,12 +1099,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 @@ -2823,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/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..260af060069 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -7,8 +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}; /// The kernel's word as the `ErrorKind` a caller can act on. @@ -165,14 +163,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 +172,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 +187,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 +207,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 +220,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..80a5a428f9a 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,29 @@ 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 -/// 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 { +/// 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) -> 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 +73,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..f478c61fb98 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}; @@ -196,20 +197,29 @@ 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 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. + let job = s.endow.iter().find(|(label, _)| *label == JOB_LABEL).map(|(_, job)| job.as_handle()); + 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); + 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 +275,8 @@ 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, + 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, @@ -387,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. +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) { @@ -437,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/sdk/toyos/src/startup.rs b/sdk/toyos/src/startup.rs new file mode 100644 index 00000000000..f46d76145e1 --- /dev/null +++ b/sdk/toyos/src/startup.rs @@ -0,0 +1,181 @@ +//! 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. +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 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(); + 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) +} + +/// 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. +pub struct Block { + memory: SharedMemory, + len: usize, +} + +impl Block { + /// 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()), + } + .map_err(Refused::Memory)?; + 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: 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()) } +} + +/// 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 = 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!( + 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!(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!(bytes(&fits, b"").map(|block| block.len()), Some(MAX_BLOCK)); + let mut past = fits; + past.push(b'a'); + assert_eq!(bytes(&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/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_handle_table.rs b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs index a2c533d2ba7..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"; @@ -53,11 +55,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() { @@ -70,20 +79,18 @@ 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, + 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, @@ -195,17 +202,14 @@ 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()); + let laid = own(); // 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, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, 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..8e7e44898da 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 @@ -23,6 +23,8 @@ use toyos_abi::syscall::{ const PAGE: u64 = 4096; +/// The name its children go by. +const NAME: &str = "echo"; const STAT_LEN: usize = core::mem::size_of::(); const CANARY: u8 = 0xA5; @@ -84,19 +86,17 @@ 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, + 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_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..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,21 +14,20 @@ 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; /// 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, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, @@ -46,7 +45,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, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, endow_ptr: 0, endow_count: 0, labels_ptr: 0, @@ -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/guest/rust-tests/src/bin/spawn_child_ends_first.rs b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs index 54df65317fa..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 @@ -20,9 +20,10 @@ 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"; +/// The name its children go by. +const NAME: &str = "test_rs_spawn_child_ends_fi"; /// What the child exits with. const CODE: i32 = 7; @@ -40,10 +41,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 +54,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 +73,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,21 +96,17 @@ 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); 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, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.len() as u64, 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..1565badbbf8 100644 --- a/tests/guest/rust-tests/src/bin/spawn_image_object.rs +++ b/tests/guest/rust-tests/src/bin/spawn_image_object.rs @@ -9,15 +9,18 @@ //! 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::Block; use toyos::AsHandle; use toyos_abi::handle::{RawHandle, Rights}; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; use toyos_abi::end::End; const SELF: &str = "/system/bin/test_rs_spawn_image_object"; +/// The name its children go by. +const NAME: &str = "test_rs_spawn_image_object"; const CHILD: &str = "spawned-from-an-object"; /// `path` read whole into a memory object of this process's own, and its bytes. @@ -30,17 +33,15 @@ 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 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 { 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, + 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 436ca826d59..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"; @@ -81,14 +83,12 @@ 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, + 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 64e15b99409..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"; @@ -54,16 +56,14 @@ 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, + 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/startup_block.rs b/tests/guest/rust-tests/src/bin/startup_block.rs new file mode 100644 index 00000000000..5cc5e16d5ff --- /dev/null +++ b/tests/guest/rust-tests/src/bin/startup_block.rs @@ -0,0 +1,95 @@ +//! 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"; +/// The name its children go by. +const NAME: &str = "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; + +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"); + std::process::exit(UNSAID_HELD); +} + +/// 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")); + std::process::exit(SAID_HELD); +} + +/// 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, + name_ptr: NAME.as_ptr() as u64, + name_len: NAME.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() => unsaid(), + Some(SAID) => 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_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 47e449913f8..e489d74225d 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::Block; /// `path`, read whole into a memory object of this process's own, which a /// spawn's ranges name ([`laid`]). @@ -26,9 +27,25 @@ 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 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 } } 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"));