diff --git a/issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md b/issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md new file mode 100644 index 00000000000..dd9cf30e3a0 --- /dev/null +++ b/issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md @@ -0,0 +1,25 @@ +--- +status: open +kind: tooling +opened: 2026-10-02 +--- + +# A metal judge takes a name's lowest pid for the job + +`Readback::exit_code` (`tests/common/metal.rs`) reads a job's exit off the +record with the lowest pid among those of its name: a binary that re-executes +itself leaves one record per child under that name, and the runner spawned the +job before the job spawned anything. + +The kernel does not issue pids in landing order. A spawn refused after its +admission gives its pid back, and the next admission takes it +(`toyos_proclife::pids`). A spawn admitted before a job and refused after the +job landed hands a pid below the job's to the next spawn, and if that is the +job's child of the same name the judge reads the child's exit as the job's. + +Evidence: by reading. No boot has shown it: the runner starts one job at a +time, so it takes another process inside `SYS_SPAWN` across a job's start. + +Exit: the judge finds the job's record by something only the job has (the pid +the runner records for it), and a host test over a log in which a child holds +the lower pid reds the lowest-pid rule. Owner: the harness. diff --git a/issues/build/pkg-install-gbaes-launch-client-exits-over-a-child-that-ends-with-it.md b/issues/build/pkg-install-gbaes-launch-client-exits-over-a-child-that-ends-with-it.md new file mode 100644 index 00000000000..e9b5b283778 --- /dev/null +++ b/issues/build/pkg-install-gbaes-launch-client-exits-over-a-child-that-ends-with-it.md @@ -0,0 +1,21 @@ +--- +status: open +kind: tooling +opened: 2026-10-02 +--- + +# pkg_install_gbae's launch client exits over a child that ends with it + +`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` brings +`pkg_install_gbae` back as a metal row, from `main` before the cut. Its client +there, `pkg_launch_gbae`, launches gbae and exits at once. A launched program +ends with its launcher, so gbae ends as killed before the compositor counts its +window. + +Evidence: under QEMU before the cut, `cargo test --test toyos-build -- +pkg_install_gbae` exited 1 with the kernel's `exit: gbae pid=21 code=137` +beside the client's own exit, and 0 with the client's launch made +`Command::new(PROGRAM).under_init()` (commit ad21ebf3a). + +Exit: the row's client starts gbae under init, and the row is green on the T14. +Owner: the guest-suite track's stage D. diff --git a/issues/diagnostics/the-shell-says-not-found-for-every-spawn-error.md b/issues/diagnostics/the-shell-says-not-found-for-every-spawn-error.md new file mode 100644 index 00000000000..c31367004c5 --- /dev/null +++ b/issues/diagnostics/the-shell-says-not-found-for-every-spawn-error.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-02 +--- + +# The shell says "not found" for every spawn error + +`execute_simple` and `execute_pipeline` (`userland/shell/src/main.rs`) match +the error of `Command::spawn` and `Command::status` as `Err(_)` and print +`: not found`. A spawn refused for its depth, one under a place being +torn down and one refused `PermissionDenied` all read to the user as a program +that is not there. + +Evidence: the T14's `proctreecase` boot at 1e9cb8c77. The kernel recorded +`spawn: refused under pid 79 at depth 65, more than 64 below init`, and the +shell at that depth printed `/system/bin/test_rs_process_tree: not found`. +`process_tree`'s chain (`tests/toyos-rust-tests/src/bin/process_tree.rs`) +accepts that line as its stop, and the row's judge reads the kernel's record +for the depth. + +Exit: the shell prints the error the spawn answered, as its `detach` does, and +says `not found` for `ErrorKind::NotFound` alone; `process_tree`'s chain no +longer accepts `: not found` as its stop. Owner: the shell. diff --git a/issues/isolation/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md b/issues/isolation/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md new file mode 100644 index 00000000000..69e65c618a0 --- /dev/null +++ b/issues/isolation/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-10-02 +--- + +# A launch too large for one frame becomes a direct spawn + +`toyos::launch::launch` (`toyos/src/launch.rs`) answers `LaunchError::NotSent` +when `Launch::encode` refuses the request, and std's `Command::launch` +(`rust/library/std/src/sys/process/toyos.rs`) answers `NotSent` with the direct +spawn. `encode` refuses a request whose header, program, argv, environment, +working directory and connector names do not fit `MAX_FRAME_LEN` +(`toyos/src/ipc.rs`). Its other refusal, too many connectors or slots, std +never reaches: it refuses the connectors itself, and a launch carries stdio +alone. + +So a declared program started with an argv and environment past one frame is +not launched. Its caller spawns it: it holds its caller's namespace and not its +manifest row, a connector the caller `provide`d does not reach it, and it +carries no `HOME` unless its caller named one. Neither side is told. A child +asked of init (`under_init`) is refused `PermissionDenied` instead. + +Evidence: by reading. No test sends a launch past one frame. + +Exit: a launch `encode` refuses answers its caller an error and starts nothing, +or the wire carries every argv and environment `SYS_SPAWN` takes; a test +launching a declared program with an environment past one frame reads that +error, or finds the child holding its row. Owner: the std lane, with +`toyos/src/launch.rs`. diff --git a/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md b/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md index f22a4a92526..8e394c18b70 100644 --- a/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md +++ b/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md @@ -117,57 +117,6 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md the pipe answering (passed to the kernel, which ends the caller). Negative control: today's libc, against which the corpus does not link. Oracle: POSIX's `posix_spawn`, `waitpid`, `poll` and `SA_RESTART`, and signal(7). -4. **A parent takes its children down**. A spawn's parent is its - spawner. A launch names its parent in its request, by one of two words: the - caller, whose place is a copy of the handle to itself every process starts - holding under the label `self` (`WRITE`, `DUP`, `TRANSFER`), carried as one - of the batch's five extras (`toyos/src/launch.rs`); or init, the one way to - outlive a starter, which std's ToyOS `CommandExt` asks for and the shell - gains a way to use; std answers a request for init it cannot launch - `PermissionDenied`, as it answers a `provide` with no launcher, and spawns - nothing. init spawns under the place, which `SpawnArgs` gains (112 bytes to - 120). The place and `SYS_NAMESPACE_BUILD`'s connector are each a handle a - peer sent, and one lookup resolves both, answering a wrong type - `InvalidArgument` rather than ending the caller: `HandleError::WrongType` - declares that exception once, for that lookup, and the connector's own match - in `kernel/src/syscall/ipc.rs` goes. The kernel keeps each process's parent, - children and depth on its table entry, answered by no syscall. A spawn that - would put a process more than `toyos_proclife::MAX_DEPTH` (64) below init, - counted from the place, is refused `ResourceExhausted`, and the kernel logs - a depth refusal naming that depth. Every end — exit, kill, CPU fault, - handle fault — closes admission at its top and claims its subtree on the - ending or killing thread, one process per table-lock hold, calling - `scheduler::yield_now` between two claims, with nothing held, whenever a - reschedule is owed. An end is published once its teardown is done and every - child's end is, by a count admission raises and a child's publication or - refused insert lowers, so the climb from the last teardown, run with - preemption off, is at most `MAX_DEPTH` publications. Decided in - `toyos-proclife`; init's part goes where launch resolution is by then - (`issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md` stage - 2). *Exit*: host — `toyos-proclife`'s interleavings of a spawn racing its - place's kill land nothing under it, end all under it and publish it in every - one; a spawn under an unrelated process lands between two claims of one - walk; a chain is refused at `MAX_DEPTH` + 1 below init. Guest — A starts B, - B starts C and launches D: killing B ends all three, read as killed, and A's - watch on B completes after C's and D's; so do B's exit, a CPU fault and a - handle fault in B; a spawn or launch under B after its kill answers `Gone`; - a chain alternating spawn and launch, each process starting the next, stops - where the kernel's depth refusal names `MAX_DEPTH` + 1, and dies whole with - its first; a launch that neither carries a place nor asks for init is - refused and nothing starts, and so is a request for init to start an - undeclared program or carrying an extra slot; a launch whose place is a - pipe is refused and init answers the next; a `MANAGE`-only place answers - `PermissionDenied`; sshd launched from a shell dies with it, and with init - as its parent outlives it. Negative control: the stage reverted whole, where - C and D outlive B. Mutations: a walk that skips a launched child (D); a - publication before the children's (A's watch); a walk from `sys_exit` alone - (the faults); the walk in one lock hold (the interleaved spawn); the count - kept on a refused insert (B unpublished); the place looked up as any handle - (init ends); no depth check, and a depth counted from the spawner rather - than the place (the chain, which no depth refusal stops); a launch with no - place spawned under init (it starts); std's `NotDeclared` fallback, or its - early return for an endowment or extra slot, kept for init (it starts). - Oracle: cgroup v2's `cgroup.kill` and `cgroup.max.depth`. 5. **A login is a session under init**. Per login init starts a session program that logout ends and that only parents what its user starts — the desktop's at boot, one per SSH connection at sshd's request. As it starts @@ -182,7 +131,7 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md 6. **A program is asked to quit**. `SYS_PROCESS_QUIT` (124, never assigned), `(process, reason)`, needs `MANAGE` as the kill does, and the reason is interrupt, hang-up or terminate. A quit reaches the process's - subtree by stage 4's walk. Each process is started holding its notice, a new + subtree. Each process is started holding its notice, a new object kind under the label `quit`, `READABLE` while a reason is asked and not yet read, whose read takes it. A quit kills a process that has never watched its notice or whose notice still holds a reason, whoever asks, in diff --git a/issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md b/issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md new file mode 100644 index 00000000000..121776c2d33 --- /dev/null +++ b/issues/kernel/a-process-that-starts-four-billion-threads-panics-the-kernel.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# A process that starts four billion threads panics the kernel + +`process::spawn_thread` takes a thread's id from its process's +`IdMap` (`kernel/src/id_map.rs`), whose `insert` steps its +counter with `Tid + Tid` (`toyos-abi/src/lib.rs`). The kernel builds with +`overflow-checks = true` (`kernel/Cargo.toml`), so the 2^32nd thread a single +process starts panics the kernel, and it does so under `PROCESS_TABLE`, which +hangs the machine instead of reporting. The thread before it is issued +`Tid(u32::MAX)`, which is the per-CPU word for no thread +(`arch::x86_64::percpu::current_tid`), so `handle_page_fault` refuses every +fault that thread takes. + +Userland reaches it by starting and joining threads in a loop; only a +successful start spends an id, so it costs 2^32 thread starts, never measured. +A pid had the same shape until `toyos_proclife::pids` (stops below `Pid::MAX` +and refuses the next spawn by name); a thread id needs the same: never +`Tid::MAX`, and a refused start once the ids are spent. + +*Exit*: a host test of the thread-id allocator answering a refusal after +`Tid(u32::MAX - 1)`, and `SYS_THREAD_SPAWN` answering it as a refusal. diff --git a/issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md b/issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md new file mode 100644 index 00000000000..f05cf31a534 --- /dev/null +++ b/issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# A spawn refused for its caller's full table has already moved its endowments + +`sys_spawn` (`kernel/src/syscall/proc.rs`) installs the new child's `Process` +handle in its caller's table after `loader::spawn` returns, which is after +`PendingHandles::commit` (`kernel/src/loader/start.rs`) moved the endowed +handles out of that table and the child landed. When the install finds the +table full, the spawn kills the child and answers `ResourceExhausted`: a +refusal after the caller's handles left, and a child that may have run user +code before its retire landed. Every other refusal of a spawn leaves the +caller's table as it was. + +The moves free one slot per endowment, so with endowments it takes another +thread of the caller filling the table between the commit and the install; +with none, a caller whose table is full when it spawns. Neither is measured. + +*Exit*: the child's handle goes into the caller's table in the commit's hold, +with the room checked before anything moves; a guest arm spawning from a +full table reads `ResourceExhausted` and no child started. diff --git a/kernel/src/id_map.rs b/kernel/src/id_map.rs index 8ca5254e9e8..90e03c97230 100644 --- a/kernel/src/id_map.rs +++ b/kernel/src/id_map.rs @@ -14,10 +14,6 @@ pub trait IdKey: Copy + Eq + Hash + Ord + Add { } // No impl for u32/u64/usize: that would let a bare integer key an IdMap. -impl IdKey for toyos_abi::Pid { - const ZERO: Self = Self(0); - const ONE: Self = Self(1); -} impl IdKey for toyos_abi::Tid { const ZERO: Self = Self(0); const ONE: Self = Self(1); diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 7799a8ffc72..722a0d2015b 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -28,7 +28,7 @@ use crate::object::{ops, HandleTable, KObjectRef}; use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::process::{ - ElfInfo, Endowments, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Pid, + Admission, ElfInfo, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Parent, Pid, ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserStack, PROCESS_TABLE, }; @@ -335,11 +335,8 @@ fn rela_dyn_from_sections( } } -/// Load a program and place its main thread, answering the object a handle to -/// the new process names. -/// -/// No parent argument: a process has no parent, and the only thing the caller -/// contributes beyond its endowment is the working directory it passes in. +/// Load a program and place its main thread under `parent`, answering the object +/// a handle to the new process names. /// /// `image` is the program's bytes when the caller read them itself, and then /// `argv[0]` is only its name: nothing opens it, and its libraries come from @@ -355,11 +352,14 @@ pub fn spawn( cwd: String, env: Vec, image: Option>, + parent: Parent, ) -> Result, crate::object::Refusal> { // An argv of only separators survives sys_spawn's split as an empty slice. let Some(&path) = 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 t0 = crate::clock::nanos_since_boot(); let from_image = image.is_some(); @@ -586,10 +586,11 @@ pub fn spawn( let NeededLibs { libs: loaded_libs, paths: lib_paths } = loaded_libs; + let pid = admission.pid(); + let object = crate::object::process::ProcessObject::new(pid); // The point of no return: every failure above answers the caller with its - // table untouched. `commit`'s own `?` is different — reachable only if the - // caller raced its own spawn, and fatal to it, not a refusal. - let (handles, endowments) = pending.commit()?; + // table untouched. + let (handles, endowments) = pending.commit(KObjectRef::Process(Arc::clone(&object)))?; let proc_data = Arc::new(Lock::new(ProcessData { handles, cwd, @@ -636,30 +637,39 @@ pub fn spawn( // One table, two holders: cloned so a crash report on this thread reads names without the process table. let syms = Arc::new(syms); + #[cfg(feature = "test-actuators")] + crate::process::debug_kill_marked_place(parent); + let mut guard = PROCESS_TABLE.lock(); - let table = guard.as_mut().unwrap(); - let pid = table.insert_with(|pid| ProcessEntry::new( - pid, - start::make_name(path), - proc_data, - Arc::clone(&syms), - ThreadEntry::new(thread_data), - )); - let tid = table.get(pid).unwrap().main_tid(); - let object = Arc::clone(table.get(pid).unwrap().object()); - - // Placed while still holding the table lock: kill_process claims teardown - // under it, so a retire sweep can never see the pid before its thread is scheduled. - let (sched, dst) = scheduler::enqueue_new( - scheduler::TaskId(pid, tid), - ks_alloc, - ks_sp, - child_pt.clone(), - thread_pointer, - syms, - ); - table.get_mut(pid).unwrap().threads_mut().get_mut(tid).unwrap().set_sched(sched); + let ((tid, dst), retire) = admission.land(guard.as_mut().unwrap(), |table, node| { + table.insert(ProcessEntry::new( + Arc::clone(&object), + start::make_name(path), + proc_data, + Arc::clone(&syms), + ThreadEntry::new(thread_data), + node, + )); + let tid = table.get(pid).unwrap().main_tid(); + // Placed while still holding the table lock: kill_process claims teardown + // under it, so a retire sweep can never see the pid before its thread is scheduled. + let (sched, dst) = scheduler::enqueue_new( + scheduler::TaskId(pid, tid), + ks_alloc, + ks_sp, + child_pt.clone(), + thread_pointer, + syms, + ); + table.get_mut(pid).unwrap().threads_mut().get_mut(tid).unwrap().set_sched(sched); + (tid, dst) + }); drop(guard); + // Its parent was claimed while it was built, and its walk has passed: the + // child is ended as that walk would have ended it, and the spawn answers it. + for sched in &retire { + scheduler::post_retire(sched); + } let t3 = crate::clock::nanos_since_boot(); log!("spawn: {} pid={} tid={} dst={} base={:#x} entry={:#x} root={:#x} symbols={}KiB (layout={}ms relocs={}ms deps={}ms tls={}ms total={}ms)", @@ -876,16 +886,17 @@ pub fn spawn_init() -> Pid { .install(crate::object::HandleEntry::new(cap, rights)) .expect("spawn_init: an empty table refused the system capability"); let label = toyos_abi::syscall::SYSCAP_LABEL; - let endowments = Endowments::new( - alloc::vec![toyos_abi::syscall::EndowEntry { + let pending = PendingHandles::Ready { + table: handles, + entries: alloc::vec![toyos_abi::syscall::EndowEntry { label_off: 0, label_len: label.len() as u32, handle: cap_handle, _pad: 0, }], - label.as_bytes().to_vec(), - ); - match spawn(&[INIT_PATH], PendingHandles::Ready(handles, endowments), String::from("/"), Vec::new(), None) { + labels: label.as_bytes().to_vec(), + }; + match spawn(&[INIT_PATH], pending, String::from("/"), Vec::new(), None, Parent::Root) { Ok(object) => object.pid(), Err(crate::object::Refusal::Error(e)) => panic!("spawn_init: failed to spawn: {e:?}"), Err(crate::object::Refusal::Handle(e)) => panic!("spawn_init: {e}"), diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs index 57686d5b93a..33463f16302 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -4,14 +4,16 @@ use alloc::vec::Vec; -use crate::object::{HandleTable, Refusal}; +use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal}; use crate::process::{ process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE, }; use crate::scheduler; use crate::user_ptr::UserBytes; use toyos_abi::handle::{RawHandle, Rights}; -use toyos_abi::syscall::{EndowEntry, SyscallError, MAX_ENDOWMENTS, MAX_LABELS_LEN, MAX_SLOT_MAP}; +use toyos_abi::syscall::{ + EndowEntry, SyscallError, MAX_SLOT_MAP, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, SELF_LABEL, +}; /// One `[child_slot, parent_handle]` pair of `SpawnArgs::slot_map_ptr`, in bytes. pub const SLOT_PAIR_LEN: usize = 8; @@ -44,16 +46,20 @@ pub(crate) fn make_name(path: &str) -> [u8; crate::process::THREAD_NAME_LEN] { // The move must be last: an earlier move leaves a failed spawn's parent holding handles that name nothing. pub enum PendingHandles { /// Built by the kernel and owing nobody anything — the boot's `/system/bin/init`. - Ready(HandleTable, Endowments), + Ready { table: HandleTable, entries: Vec, labels: Vec }, /// A caller's request: `endow` has not left the caller's table yet. Moving { table: HandleTable, endow: Vec, labels: Vec }, } impl PendingHandles { /// Take the endowed handles out of the parent's table, all under one lock hold: a refusal leaves it unchanged. - pub fn commit(self) -> Result<(HandleTable, Endowments), Refusal> { - let (mut table, endow, labels) = match self { - Self::Ready(table, endowments) => return Ok((table, endowments)), + /// `own` is the child itself, which its table holds under [`SELF_LABEL`] beside them. + pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments), Refusal> { + let (mut table, endow, mut labels) = match self { + Self::Ready { mut table, mut entries, mut labels } => { + endow_self(&mut table, &mut entries, &mut labels, own); + return Ok((table, Endowments::new(entries, labels))); + } Self::Moving { table, endow, labels } => (table, endow, labels), }; let data_arc = process_data(); @@ -71,6 +77,10 @@ impl PendingHandles { if end > labels.len() { return Err(SyscallError::InvalidArgument.into()); } + // The kernel's own, which a caller's of that name would shadow in the child's lookup. + if &labels[label_off as usize..end] == SELF_LABEL.as_bytes() { + return Err(SyscallError::InvalidArgument.into()); + } // Checked before any removal, so a missing `TRANSFER` refuses the spawn instead of leaving a hole. let rights = data.handles.rights_of(handle)?; if !rights.contains(Rights::TRANSFER) { @@ -84,7 +94,7 @@ impl PendingHandles { moving.push((EndowEntry { label_off, label_len, handle, _pad: 0 }, handle)); } // Checked before any removal, so a failed install can't strand a handle out of a table that never spawned. - if !table.has_room(moving.len()) { + if !table.has_room(moving.len() + 1) { return Err(SyscallError::ResourceExhausted.into()); } @@ -99,10 +109,27 @@ impl PendingHandles { .expect("a child table with verified room refused an endowment"); entries.push(entry); } + drop(data); + endow_self(&mut table, &mut entries, &mut labels, own); Ok((table, Endowments::new(entries, labels))) } } +/// Install `own` in its own table under [`SELF_LABEL`]: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on. Its caller verified the room. +fn endow_self(table: &mut HandleTable, entries: &mut Vec, labels: &mut Vec, own: KObjectRef) { + let rights = Rights::WRITE.union(Rights::DUP).union(Rights::TRANSFER); + let handle = table + .install(HandleEntry::new(own, rights)) + .expect("a child table with verified room refused its own handle"); + entries.push(EndowEntry { + label_off: labels.len() as u32, + label_len: SELF_LABEL.len() as u32, + handle, + _pad: 0, + }); + labels.extend_from_slice(SELF_LABEL.as_bytes()); +} + /// Reads `SpawnArgs`'s two handle vectors into the child's pending handle state. // Every refusal here precedes any child state, so nothing is enqueued or orphaned. pub fn build_child_handles( @@ -110,7 +137,7 @@ pub fn build_child_handles( endow: &UserBytes, labels: &[u8], ) -> Result { - if endow.len() / ENDOW_ENTRY_LEN > MAX_ENDOWMENTS { + if endow.len() / ENDOW_ENTRY_LEN > MAX_SPAWN_ENDOWMENTS { return Err(SyscallError::InvalidArgument.into()); } // Checked before the loop: `install_at`'s cap misses a repeated slot, which would duplicate @@ -118,7 +145,7 @@ pub fn build_child_handles( if slot_map.len() / SLOT_PAIR_LEN > MAX_SLOT_MAP { return Err(SyscallError::InvalidArgument.into()); } - if labels.len() > MAX_LABELS_LEN { + if labels.len() > MAX_SPAWN_LABELS_LEN { return Err(SyscallError::InvalidArgument.into()); } let data_arc = process_data(); diff --git a/kernel/src/object/handle.rs b/kernel/src/object/handle.rs index f1d4048689b..6cb035aeb0f 100644 --- a/kernel/src/object/handle.rs +++ b/kernel/src/object/handle.rs @@ -27,7 +27,7 @@ pub enum HandleError { BadHandle, /// The slot has moved past this handle: closed, or its generations ran out. Stale, - /// Not the caller's bug at `SYS_NAMESPACE_BUILD`'s connector argument — the sole named exception. + /// Ends the caller like the two above, except through [`HandleTable::get_sent`], the one lookup of a handle a peer sent — `SYS_NAMESPACE_BUILD`'s connector and `SYS_SPAWN`'s place — where it is `InvalidArgument`. WrongType { held: &'static str, wanted: &'static str }, /// The handle is fine and does not carry what the call needs. Rights { held: Rights, needed: Rights }, @@ -274,6 +274,18 @@ impl HandleTable { .ok_or(HandleError::WrongType { held: entry.object.kind(), wanted: T::NAME }) } + /// [`get`](Self::get) for a handle a peer sent, whose type is that peer's claim rather than proof of a bug in this caller: a wrong type refuses `InvalidArgument`, and every other failure is `get`'s. + pub fn get_sent( + &self, + h: RawHandle, + need: Rights, + ) -> Result, Refusal> { + match self.get::(h, need) { + Err(HandleError::WrongType { .. }) => Err(SyscallError::InvalidArgument.into()), + other => other.map_err(Refusal::from), + } + } + /// The borrowing accessor, for a call that completes while still holding the guard. pub fn get_ref(&self, h: RawHandle, need: Rights) -> Result<&KObjectRef, HandleError> { let entry = self.entry_of(h)?; diff --git a/kernel/src/process.rs b/kernel/src/process.rs index a3433726025..d7fb1d44161 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -26,11 +26,11 @@ use crate::loader::{alloc_kernel_stack, thread_start, TlsBlock}; pub use toyos_abi::{Pid, Tid}; pub use crate::scheduler::TaskId; -use toyos_abi::syscall::EndowEntry; +use toyos_abi::syscall::{EndowEntry, SyscallError}; /// The lifecycle's decisions; this file only performs them. pub use toyos_proclife::{ThreadLocation, Watch}; -use toyos_proclife::{join, reap, spawn as proclife_spawn, teardown as proclife, Lifecycle, Processes}; +use toyos_proclife::{join, reap, spawn as proclife_spawn, teardown as proclife, tree, Lifecycle, Node, Processes}; /// One `EndowEntry` on the wire; `loader::start` and [`Endowments::encode`] both index by it. pub const ENDOW_ENTRY_LEN: usize = core::mem::size_of::(); @@ -333,28 +333,35 @@ pub struct ProcessEntry { threads: crate::id_map::IdMap, /// Set once, with the exit's code, by the exit or kill that claims teardown; checked by `spawn_thread` so no thread appears after the retire set. teardown_code: Option, + /// Its parent, children and depth, answered by no syscall; `toyos_proclife::tree` reads and writes it. + node: Node, + /// The exit its teardown made, kept until every end below it is published. + exit: Option, } impl ProcessEntry { - /// Create a new process with its main thread (always `Tid(0)`). + /// Create a new process with its main thread (always `Tid(0)`), at `object`'s pid. pub fn new( - pid: Pid, + object: Arc, name: [u8; THREAD_NAME_LEN], process_data: Arc>, symbols: Arc, main_thread: ThreadEntry, + node: Node, ) -> Self { let mut threads = crate::id_map::IdMap::new(); let main_tid = threads.insert(main_thread); Self { - pid, - object: crate::object::process::ProcessObject::new(pid), + pid: object.pid(), + object, name, process_data, symbols, main_tid, threads, teardown_code: None, + node, + exit: None, } } pub fn pid(&self) -> Pid { self.pid } @@ -395,27 +402,31 @@ impl Lifecycle for ProcessEntry { f(tid, thread.state); } } + fn node(&self) -> &Node { &self.node } + fn node_mut(&mut self) -> &mut Node { &mut self.node } } /// The lifecycle face of the table; `published_exit` comes off the [`ProcessObject`](crate::object::process::ProcessObject) rather than the table, since it outlives the entry. impl Processes for ProcessTable { type Proc = ProcessEntry; - // Via `IdMap::get`, not `self.get(pid)`: the inherent method and a trait method of the same name resolve to the inherent one, so renaming this would turn it into infinite recursion. fn get(&self, pid: Pid) -> Option<&ProcessEntry> { - crate::id_map::IdMap::get(self, pid) + self.entries.get(&pid) } fn get_mut(&mut self, pid: Pid) -> Option<&mut ProcessEntry> { - crate::id_map::IdMap::get_mut(self, pid) + self.entries.get_mut(&pid) } fn published_exit(&self, pid: Pid) -> bool { - crate::id_map::IdMap::get(self, pid).is_some_and(|p| p.object.finished()) + self.entries.get(&pid).is_some_and(|p| p.object.finished()) } fn each_pid(&self, f: &mut dyn FnMut(Pid)) { - for (pid, _) in self.iter() { + for &pid in self.entries.keys() { f(pid); } } + fn pids(&mut self) -> &mut toyos_proclife::Pids { + &mut self.pids + } } impl Drop for ProcessEntry { @@ -660,7 +671,39 @@ impl IdleProof { } -pub type ProcessTable = crate::id_map::IdMap; +/// Every process by pid, and the pids not yet issued (`toyos_proclife::pids`). +pub struct ProcessTable { + entries: crate::hasher::HashMap, + pids: toyos_proclife::Pids, +} + +impl ProcessTable { + fn new() -> Self { + Self { entries: crate::hasher::HashMap::default(), pids: toyos_proclife::Pids::default() } + } + + pub fn get(&self, pid: Pid) -> Option<&ProcessEntry> { + self.entries.get(&pid) + } + + pub fn get_mut(&mut self, pid: Pid) -> Option<&mut ProcessEntry> { + self.entries.get_mut(&pid) + } + + pub fn remove(&mut self, pid: Pid) -> Option { + self.entries.remove(&pid) + } + + pub fn iter(&self) -> impl Iterator { + self.entries.iter().map(|(&pid, proc)| (pid, proc)) + } + + /// `entry`, at the pid it was admitted with, which no entry has held. + pub fn insert(&mut self, entry: ProcessEntry) { + let pid = entry.pid; + assert!(self.entries.insert(pid, entry).is_none(), "ProcessTable::insert: pid {pid} held twice"); + } +} pub static PROCESS_TABLE: Lock> = Lock::new(None); @@ -814,6 +857,72 @@ pub fn with_process_data(f: impl FnOnce(&mut ProcessData) -> R) -> R { f(&mut guard) } +/// Whom a new process is placed under. +#[derive(Clone, Copy)] +pub enum Parent { + /// No process: init, which the kernel starts. + Root, + /// The spawner, or the process whose `self` its place named. + Under(Pid), +} + +/// A spawn admitted under its parent, holding the pid its child will have and the parent's publication: [`Admission::land`] hands both to the child, and a drop — every way a spawn's build fails — gives both back. +pub struct Admission(Option); + +impl Admission { + /// The question at the top of a spawn, before anything is built: `Gone` for a parent being torn down, and `ResourceExhausted`, which the log names, for a child more than `MAX_DEPTH` below init or no pid left to issue. + pub fn ask(parent: Parent) -> Result { + let place = match parent { + Parent::Root => None, + Parent::Under(place) => Some(place), + }; + let admit = { + let mut guard = PROCESS_TABLE.lock(); + tree::admit_child(guard.as_mut().unwrap(), place) + }; + match admit { + tree::Admit::Yes(admitted) => Ok(Self(Some(admitted))), + tree::Admit::Gone => Err(SyscallError::Gone), + tree::Admit::TooDeep { depth } => { + log!("spawn: refused under pid {} at depth {depth}, more than {} below init", + place.expect("spawn: init is admitted at depth 0"), toyos_proclife::MAX_DEPTH); + Err(SyscallError::ResourceExhausted) + } + tree::Admit::NoPid => { + log!("spawn: refused, every pid below {} is issued", Pid::MAX); + Err(SyscallError::ResourceExhausted) + } + } + } + + pub fn pid(&self) -> Pid { + self.0.as_ref().expect("Admission: taken only by land, which consumes it").pid() + } + + /// Land the child in the `PROCESS_TABLE` hold `insert` puts its entry in and schedules its thread in. A parent claimed since the admission has walked its children, so the child is claimed in this hold too, and the retires answered are the spawner's to post with the lock given up. + pub fn land( + mut self, + table: &mut ProcessTable, + insert: impl FnOnce(&mut ProcessTable, Node) -> R, + ) -> (R, Vec) { + let admitted = self.0.take().expect("Admission: landed once"); + let pid = admitted.pid(); + let (inserted, retire) = tree::land_child(table, admitted, KILLED_EXIT_CODE, insert); + (inserted, scheds(table, pid, retire)) + } +} + +impl Drop for Admission { + fn drop(&mut self) { + let Some(admitted) = self.0.take() else { return }; + let ready = { + let mut guard = PROCESS_TABLE.lock(); + tree::refuse_child(guard.as_mut().unwrap(), admitted) + }; + publish_climb(ready); + } +} + /// Spawn a thread within the current process. pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Option { // Phase 1: parent's data + address space (table lock dropped after). @@ -972,20 +1081,37 @@ fn teardown_resources( (syscall_total, syscall_total_ns) } -/// Table-side teardown bookkeeping: drop the symbol table, total the CPU time of every thread still in the table, and mark the last thread out dead with `mark`. -/// Caller must hold `PROCESS_TABLE`, be that thread and have freed the resources. Returns the object whose exit the caller publishes once the table lock is given up, with that total. -#[must_use = "the exit must be published on the object returned"] -fn teardown_bookkeeping(table: &mut ProcessTable, process_pid: Pid, tid: Tid, code: i32, mark: i32) - -> (Arc, u64) { +/// Table-side teardown bookkeeping: drop the symbol table and total the CPU time of every thread still in the table. +/// Caller must hold `PROCESS_TABLE`, be the last thread out and have freed the resources. +fn teardown_bookkeeping(table: &mut ProcessTable, process_pid: Pid, code: i32) -> u64 { let proc = table.get_mut(process_pid) .expect("teardown_bookkeeping: process not found"); proc.symbols = Arc::new(SymbolTable::empty()); let cpu_ns: u64 = proc.threads.iter().map(|(_, t)| t.sched().map_or(0, scheduler::task_cpu_ns)).sum(); let name = proc.name_str(); log!("exit: {name} pid={process_pid} code={code} cpu={}ms", cpu_ns / 1_000_000); - let object = Arc::clone(&proc.object); - proclife::torn_down(table, process_pid, tid, mark); - (object, cpu_ns) + cpu_ns +} + +/// Publish `first`, then every parent each publication lets go, child before parent: at most `toyos_proclife::MAX_DEPTH` + 1 publications, each with the table lock given up. +/// Once published an entry is reapable, so only its parent's entry is read after. +fn publish_climb(first: Option) { + let mut ready = first; + while let Some(publish) = ready { + let (object, exit) = { + let mut guard = PROCESS_TABLE.lock(); + let proc = guard.as_mut().unwrap().get_mut(publish.pid) + .expect("publish_climb: a process is in the table until its exit is published"); + let exit = proc.exit.take() + .expect("publish_climb: a count reaches zero only after the teardown kept its exit"); + (Arc::clone(&proc.object), exit) + }; + object.publish_exit(exit); + ready = publish.parent.and_then(|parent| { + let mut guard = PROCESS_TABLE.lock(); + tree::published(guard.as_mut().unwrap(), parent, publish.pid) + }); + } } /// One `ProcessStats`, from a process's own data; written once, since `SYS_PROCESS_STATS` samples a live process through the same fields the teardown snapshots. @@ -1020,10 +1146,10 @@ pub fn stats_from( } } -/// The last thread out's teardown of its process, on its own stack: free what the process holds, then publish its exit. Every other thread has left, so none can run in what is freed. -/// The thread is in the process until the bookkeeping marks it, so a machine stop waits for every record and release here. +/// The last thread out's teardown of its process, on its own stack: free what the process holds, then keep its exit until every end below it is published. Every other thread has left, so none can run in what is freed. +/// The thread is in the process until the last table section marks it, so a machine stop waits for every record and release here. /// Waits on nothing: it runs on a killed thread whose one cancel may be spent, and at the exit boundary's preempt depth, where a park asserts. -/// Publish happens after the table lock is released, and once published the entry is reapable, so nothing may read the table for this pid after. +/// The section that marks it also lets the teardown's own hold on the publication go: the exit is published here when nothing below it is left, and otherwise by the last end below it. fn teardown(pid: Pid, tid: Tid, code: i32, mark: i32, process_data: &Arc>) { let main_thread_data = { let guard = PROCESS_TABLE.lock(); @@ -1031,14 +1157,22 @@ fn teardown(pid: Pid, tid: Tid, code: i32, mark: i32, process_data: &Arc) { } } -/// Claim `pid`'s teardown for `code`: the winner gets the threads it retires, every one still in the process but `caller`; a loser, or a process already gone, gets none. -fn claim(pid: Pid, code: i32, caller: Option) -> Vec { +/// Claim `pid`'s teardown for `code`, in one hold of the table lock: the winner gets the threads it retires, every one still in the process but `caller`, and owes `pid`'s children to its walk; a loser, or a process already gone, gets none. +fn claim(pid: Pid, code: i32, caller: Option, owed: &mut Vec) -> Vec { let mut guard = PROCESS_TABLE.lock(); let table = guard.as_mut().unwrap(); - if !proclife::claim_teardown(table, pid, code) { + if !tree::claim(table, pid, code, owed) { return Vec::new(); } - let proc = Processes::get(table, pid).expect("claim: the entry the claim just succeeded on"); - proclife::retire_set(proc, caller) - .into_iter() + let proc = table.get(pid).expect("claim: a claimed process is in the table"); + scheds(table, pid, proclife::retire_set(proc, caller)) +} + +/// The scheduler records of `pid`'s threads `tids`, which a claim of it retires. +fn scheds(table: &ProcessTable, pid: Pid, tids: Vec) -> Vec { + let proc = table.get(pid).expect("scheds: a claimed process is in the table"); + tids.into_iter() .map(|tid| { proc.threads.get(tid).and_then(ThreadEntry::sched).cloned() - .expect("claim: a thread in the table has its scheduler record") + .expect("scheds: a thread in the table has its scheduler record") }) .collect() } +/// End every process an end owes, on the ending or killing thread: one claim per hold of the table lock, each claim's retires posted with it given up, and a reschedule owed between two claims served with nothing held. +fn walk(mut owed: Vec) { + while let Some(pid) = owed.pop() { + for sched in claim(pid, KILLED_EXIT_CODE, None, &mut owed) { + scheduler::post_retire(&sched); + } + if !owed.is_empty() && crate::preempt::need_resched() { + scheduler::yield_now(); + } + } +} + +/// End the calling thread's process with `code`, and everything below it. pub fn exit(code: i32) -> ! { let tid = current_tid(); - for sched in claim(current_process(), code, Some(tid)) { + let mut owed = Vec::new(); + for sched in claim(current_process(), code, Some(tid), &mut owed) { scheduler::post_retire(&sched); } + walk(owed); leave(None); scheduler::exit_current(); } @@ -1554,14 +1708,40 @@ fn with_current_symbols(f: impl FnOnce(&crate::symbols::SymbolTable) -> bool) -> } } -/// Kill the process an object names. +/// Kill the process an object names, and everything below it. /// The handle is the whole authorization, not the parent relationship: a `Process` handle carrying `Rights::MANAGE` says who may, and it can be narrowed away or handed on. `Ok` for an already-gone process: the caller asked for it to be dead and it is. -/// Returns once the victim's retires are posted, never waiting on it: the victim may be killing this caller. Its last thread out publishes the object's exit. +/// Returns once every retire is posted, never waiting on a victim: one may be killing this caller. The object's exit is published once every end below it is. pub fn kill_process(object: &crate::object::process::ProcessObject) -> u64 { - for sched in claim(object.pid(), KILLED_EXIT_CODE, None) { + kill(object.pid()); + 0 +} + +fn kill(pid: Pid) { + let mut owed = Vec::new(); + for sched in claim(pid, KILLED_EXIT_CODE, None, &mut owed) { scheduler::post_retire(&sched); } - 0 + walk(owed); +} + +/// The process whose next spawn `debug_action::KILL_PLACE_AS_SPAWN_LANDS` marked; `Pid::MAX`, which is never issued, for none. +#[cfg(feature = "test-actuators")] +static MARKED_SPAWNER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(Pid::MAX.0); + +/// Mark the calling process's next spawn that reaches its commit. +#[cfg(feature = "test-actuators")] +pub fn debug_mark_spawn() { + MARKED_SPAWNER.store(current_process().0, core::sync::atomic::Ordering::Relaxed); +} + +/// Kill the place of a spawn its caller marked, and take the mark. The loader calls it between the spawn's commit and its landing. +#[cfg(feature = "test-actuators")] +pub fn debug_kill_marked_place(parent: Parent) { + use core::sync::atomic::Ordering::Relaxed; + let Parent::Under(place) = parent else { return }; + if MARKED_SPAWNER.compare_exchange(current_process().0, Pid::MAX.0, Relaxed, Relaxed).is_ok() { + kill(place); + } } /// The shell convention for "died on SIGKILL"; kept because every test that reads one already spells it. diff --git a/kernel/src/sched/kthread.rs b/kernel/src/sched/kthread.rs index 9f2eacb499f..2bba7e7cada 100644 --- a/kernel/src/sched/kthread.rs +++ b/kernel/src/sched/kthread.rs @@ -15,6 +15,7 @@ use crate::process::{ use crate::scheduler::{self, TaskId}; use crate::symbols::SymbolTable; use crate::sync::Lock; +use toyos_proclife::Processes; use super::payload::ThreadSched; @@ -101,6 +102,8 @@ pub fn spawn(name: &str, body: extern "C" fn(u64) -> !, arg: u64) -> ThreadSched // Before the table lock: a panic holding the process table hangs the machine. let claim = Claim::take(name); + let taken = PROCESS_TABLE.lock().as_mut().expect("kthread: spawned before process::init").pids().take(); + let pid = taken.unwrap_or_else(|| panic!("kthread: {name} found every pid issued")); let mut short = [0u8; THREAD_NAME_LEN]; let len = name.len().min(THREAD_NAME_LEN - 1); @@ -112,15 +115,14 @@ pub fn spawn(name: &str, body: extern "C" fn(u64) -> !, arg: u64) -> ThreadSched // One hold across insert and place: a visible pid already has its thread scheduled. let mut guard = PROCESS_TABLE.lock(); let table = guard.as_mut().expect("kthread: spawned before process::init"); - let pid = table.insert_with(|pid| { - ProcessEntry::new( - pid, - short, - Arc::new(Lock::new(kernel_process_data(name))), - Arc::clone(&syms), - ThreadEntry::new(Arc::new(Lock::new(kernel_thread_data()))), - ) - }); + table.insert(ProcessEntry::new( + crate::object::process::ProcessObject::new(pid), + short, + Arc::new(Lock::new(kernel_process_data(name))), + Arc::clone(&syms), + ThreadEntry::new(Arc::new(Lock::new(kernel_thread_data()))), + toyos_proclife::Node::root(), + )); let tid = table.get(pid).expect("kthread: the entry just inserted is gone").main_tid(); claim.publish(TaskId(pid, tid)); // The kernel address space, named so one declaration decides every task's `cr3`. diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 35c9c03c7cd..6983dc7135c 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -47,7 +47,7 @@ use super::machine::{ #[cfg(feature = "test-actuators")] use super::machine::lower_sysinfo_bound; use super::proc::{ - sys_endowments, sys_exit, sys_nanosleep, sys_process_open, sys_process_stats, + spawn_place, sys_endowments, sys_exit, sys_nanosleep, sys_process_open, sys_process_stats, sys_process_wait, sys_rt_enter, sys_spawn, sys_thread_exit, sys_thread_join, sys_thread_spawn, }; use super::vm::{shared_image, sys_dlopen, sys_dlsym, sys_mmap, sys_munmap, sys_query_modules, sys_tls_alloc_block}; @@ -213,13 +213,18 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Err(refused) => return refused, }, }; + // Before an endowment can move the handle that names it. + let parent = match spawn_place(args.place) { + Ok(parent) => parent, + Err(refused) => return refused, + }; let text = match ctx.user_str(UserAddr::new(args.argv_ptr), args.argv_len) { Ok(s) => s, Err(e) => return e.to_u64() }; let cwd = match ctx.user_str(UserAddr::new(args.cwd_ptr), args.cwd_len).and_then(|p| spawn_cwd(&p)) { Ok(cwd) => cwd, Err(e) => return e.to_u64(), }; - if args.endow_count as usize > toyos_abi::syscall::MAX_ENDOWMENTS - || args.labels_len > toyos_abi::syscall::MAX_LABELS_LEN as 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 { return SyscallError::InvalidArgument.to_u64(); } @@ -259,7 +264,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> alloc::vec::Vec::new() }; let argv: alloc::vec::Vec<&str> = text.split('\0').filter(|s| !s.is_empty()).collect(); - sys_spawn(&argv, pending, cwd, env, image) + sys_spawn(&argv, pending, cwd, env, image, parent) } SYS_PROCESS_WAIT => sys_process_wait(RawHandle(a1 as u32), a2), SYS_PROCESS_KILL => { @@ -626,6 +631,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> crate::log::storm::emit_patterned(0, a2); 0 } + DA::KILL_PLACE_AS_SPAWN_LANDS => { + process::debug_mark_spawn(); + 0 + } _ => SyscallError::InvalidArgument.to_u64(), }, SYS_SCHED_INFO => match ctx.copy_out(UserAddr::new(a1), &sys_sched_info()) { diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index b0b573e77fc..475d60a6e8c 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -192,13 +192,9 @@ pub(super) fn sys_namespace_build(ctx: &SyscallContext, args: &NamespaceBuild) - return SyscallError::InvalidArgument.to_u64(); }; let connector = match process::with_process_data(|data| { - data.handles.get::(handle, Rights::TRANSFER) + data.handles.get_sent::(handle, Rights::TRANSFER) }) { Ok(c) => c, - // WrongType returns InvalidArgument here instead of ending the caller: an added connector is often one a peer transferred, not proof of a caller bug. - Err(crate::object::HandleError::WrongType { .. }) => { - return SyscallError::InvalidArgument.to_u64() - } Err(e) => return e.refuse(), }; entries.push((name, connector)); diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index fada5eddc3b..142a1da40cf 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -29,16 +29,32 @@ pub(super) fn sys_exit(code: i32) -> u64 { process::exit(code); } -/// Start a program in `cwd` and return a handle to it; kill the child if the handle can't be installed. +/// `SpawnArgs::place`: the caller itself, or the process a handle carrying `WRITE` names — a handle a peer sent, so a wrong type is refused rather than ending the caller. +pub(super) fn spawn_place(place: u64) -> Result { + if place == u64::from(toyos_abi::HANDLE_INVALID.0) { + return Ok(process::Parent::Under(process::current_process())); + } + let Ok(raw) = u32::try_from(place) else { + return Err(SyscallError::InvalidArgument.to_u64()); + }; + process::with_process_data(|data| { + data.handles.get_sent::(RawHandle(raw), Rights::WRITE) + }) + .map(|object| process::Parent::Under(object.pid())) + .map_err(|refused| refused.refuse()) +} + +/// Start a program in `cwd` under `parent` and return a handle to it; kill the child if the handle can't be installed. pub(super) fn sys_spawn( args: &[&str], pending: crate::loader::PendingHandles, cwd: alloc::string::String, env: Vec, image: Option>, + parent: process::Parent, ) -> u64 { // Nothing to clean up yet: spawn's frame owns the child's resources on error. - let object = match process::spawn(args, pending, cwd, env, image) { + let object = match process::spawn(args, pending, cwd, env, image, parent) { Ok(object) => object, Err(e) => return e.refuse(), }; diff --git a/rust b/rust index 3f6050fc829..6c7f996a4fe 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit 3f6050fc8293153ce890879442735fdaeded9c63 +Subproject commit 6c7f996a4fe3f8d8f08d7d2c9c30054946645e4f diff --git a/src/build.rs b/src/build.rs index 3549fdf2fdd..128e29fb256 100644 --- a/src/build.rs +++ b/src/build.rs @@ -3065,6 +3065,7 @@ mod tests { "tests/metalcase/system.toml", "tests/metaldevicecase/system.toml", "tests/netcase/system.toml", + "tests/proctreecase/system.toml", "tests/testcases/system.toml", "tests/virtjobcase/system.toml", "tests/virtpaniccase/system.toml", diff --git a/src/ci.rs b/src/ci.rs index 1dec513c109..5b0a8120f6e 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -389,6 +389,22 @@ pub(crate) const CONTROLS: &[Control] = &[ Fails("interleave::tests::the_last_one_out_is_in_its_process_until_its_teardown_is_done"), Fails("teardown::tests::only_the_thread_that_empties_a_claimed_process_tears_it_down"), ]), + red(PROCLIFE, "mutate-place-skips-the-insert-recheck", None, &[ + Fails("interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it"), + ]), + red(PROCLIFE, "mutate-refused-spawn-keeps-the-count", None, &[ + Fails("interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it"), + ]), + red(PROCLIFE, "mutate-landed-child-retires-nothing", None, &[ + Fails("interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it"), + Fails("tree::tests::a_child_landed_under_a_place_claimed_since_its_admission_is_claimed_with_it"), + ]), + red(PROCLIFE, "mutate-publish-before-the-children", None, &[ + Fails("interleave::tests::an_exit_publishes_after_every_end_below_it"), + ]), + red(PROCLIFE, "mutate-walk-in-one-hold", None, &[ + Fails("interleave::tests::a_spawn_under_an_unrelated_process_lands_between_two_claims_of_one_walk"), + ]), red(SCHED_SIM, "placement-ignores-staleness", Some("policy"), &[ Fails("a_stopped_cpu_stops_taking_work"), ]), diff --git a/src/kernelkeys.rs b/src/kernelkeys.rs index 6181def2267..2da0203b72a 100644 --- a/src/kernelkeys.rs +++ b/src/kernelkeys.rs @@ -58,6 +58,12 @@ pub const DECLARED: &[Declared] = &[ ty: "HashMap>", keys: "`process::Pid`, minted by the process table", }, + Declared { + file: "kernel/src/process.rs", + ty: "HashMap", + keys: "the pid of the entry's `ProcessObject`, which only `toyos_proclife::Pids::take` issues, \ + at a spawn's admission or a kernel thread's start", + }, ]; /// The one file the scan does not read, by exact path: it *defines* the alias diff --git a/system.toml b/system.toml index 4ad79cf0815..027e8e3b305 100644 --- a/system.toml +++ b/system.toml @@ -1,7 +1,7 @@ # sshd is deliberately not in `[boot] start`. It is built into the image and -# started by hand (`/system/bin/sshd &`) on a machine that wants remote access: it -# listens on every interface, so carrying it in every boot is a listening port -# nobody asked for. +# started by hand (`detach /system/bin/sshd`, so it outlives the shell) on a +# machine that wants remote access: it listens on every interface, so carrying +# it in every boot is a listening port nobody asked for. # `src/build.rs`'s `no_shipped_boot_config_starts_sshd` is the gate. hosted-rustc = false diff --git a/tests/metal/lenovo-20w0003amz.toml b/tests/metal/lenovo-20w0003amz.toml index cbae0141135..501e12cc8cc 100644 --- a/tests/metal/lenovo-20w0003amz.toml +++ b/tests/metal/lenovo-20w0003amz.toml @@ -37,6 +37,9 @@ bios = "N34ET71W (1.71 )" "boot.metaldevicecase.complete_ms" = 1165 "boot.metaldevicecase.panel_max_us" = 3837 "boot.metaldevicecase.panel_us" = 23002 +"boot.proctreecase.complete_ms" = 1153 +"boot.proctreecase.panel_max_us" = 3773 +"boot.proctreecase.panel_us" = 21279 "boot.selftests.complete_ms" = 1172 "boot.selftests.panel_max_us" = 3950 "boot.selftests.panel_us" = 22805 diff --git a/tests/proctreecase/system.toml b/tests/proctreecase/system.toml new file mode 100644 index 00000000000..04237bf85c7 --- /dev/null +++ b/tests/proctreecase/system.toml @@ -0,0 +1,46 @@ +# The boot `process_tree` runs on: a parent's end takes its children down. +# +# A `launcher`, because the subtree under test has children init launches; a +# declared `cat`, which runs until its input ends; and a declared shell, which a +# depth chain launches and which detaches a `cat`. + +[boot] +start = ["logd", "blockd", "fsd", "test-runner"] + +# `every_boot_config_runs_logd` is what refuses a boot config without it: the +# kernel keeps the record ring and writes no file, so such a boot's `/log` is +# empty. It claims no device and serves no port. +[programs.logd] +service = true +syscap = ["logread"] + +# `roster` because the guest asks which of its tree still runs, and `dup` +# because test-runner hands each binary a duplicate of its capability. +[programs.test-runner] +receives = ["launcher"] +syscap = ["dup", "roster"] + +[programs.toybox] + +[programs.shell] +receives = ["launcher"] + +[symlinks] +"bin/cat" = "/system/bin/toybox" + +# The block service: the NVMe controller this machine's DATA is on, driven +# from userland through its claim. Started again when it ends; the claim goes +# back with the process and is minted again for the next. +[programs.blockd] +service = true +restart = true +serves = ["block"] +devices = ["pci:1b36:0010"] + +# The file servers: DATA, the log and the running slot's volume, one process +# each, serving every program the directories of its role. Started again when +# one ends, on the same ports. +[programs.fsd] +restart = true +roles = ["data", "log", "boot"] +receives = ["block"] diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs index 864d46c4ff5..0244faec25b 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_loader.rs @@ -244,6 +244,7 @@ fn spawn_path(path: &str) -> Result { cwd_len: CWD.len() as u64, image: 0, image_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } .map(|pid| pid.0 as u64) @@ -296,6 +297,7 @@ fn spawn_image(path: &str, bytes: &[u8]) -> Result { cwd_len: CWD.len() as u64, image: toyos::AsHandle::as_handle(&object).0 as u64, image_len: bytes.len() as u64, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } .map(|pid| pid.0 as u64) diff --git a/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs b/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs index 35c44595173..fb9fcc85375 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_elf_segments.rs @@ -158,6 +158,7 @@ fn spawn_as(path: &str, image: &[u8]) -> SyscallError { cwd_len: CWD.len() as u64, image: toyos::AsHandle::as_handle(&object).0 as u64, image_len: image.len() as u64, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } .map(|pid| panic!("{path}: spawn succeeded (pid {pid:?}) — the header is malformed")) diff --git a/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs b/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs index d251bc39fff..c422ba0ea3c 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs @@ -16,12 +16,21 @@ //! one argument and both ended in the kernel rather than in an error word, so //! each has an arm here and the last arm is what says the machine survived //! them. +//! +//! **An endowment vector is one entry and one label short of a table**, which +//! the kernel's own `self` fills. So a caller's entry labelled `self` is +//! refused before anything moves, and so is a vector of `MAX_ENDOWMENTS` +//! entries, while one entry fewer starts. use toyos_abi::syscall::{ - self, EndowEntry, MmapFlags, MmapProt, SpawnArgs, SyscallError, MAX_SLOT_MAP, + self, EndowEntry, MmapFlags, MmapProt, SpawnArgs, SyscallError, MAX_ENDOWMENTS, MAX_SLOT_MAP, SELF_LABEL, }; use toyos_abi::RawHandle; +const SELF: &str = "/system/bin/test_rs_abuse_handle_table"; +/// The role a spawned copy of this binary takes: it exits at once. +const CHILD: &str = "endowed"; + /// Where the child starts: `SpawnArgs` names a working directory or the spawn is refused. const CWD: &str = "/"; @@ -38,6 +47,9 @@ const PAIRS: usize = 100_000; const LABELS: &[u8] = b"twice"; fn main() { + if std::env::args().nth(1).as_deref() == Some(CHILD) { + return; + } let region = unsafe { syscall::mmap( core::ptr::null_mut(), @@ -73,6 +85,7 @@ fn main() { cwd_len: CWD.len() as u64, image: 0, image_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }; unsafe { syscall::spawn(&args) } }; @@ -118,13 +131,20 @@ fn main() { let err = spawn_with(0, 2).expect_err("an endowment vector naming one handle twice must be rejected"); assert_eq!(err, SyscallError::InvalidArgument, "wrong error for a repeated endowment"); + a_caller_endows_no_self(); + a_spawn_carries_one_entry_fewer_than_a_table(); + // dup2 picks the slot, so it never went through the allocating path that // carried the cap. + let mut filled = Vec::new(); let mut refused = None; for n in 3..40_000u16 { - if let Err(e) = syscall::dup2(RawHandle(1), n) { - refused = Some((n, e)); - break; + match syscall::dup2(RawHandle(1), n) { + Ok(handle) => filled.push(handle), + Err(e) => { + refused = Some((n, e)); + break; + } } } let (n, e) = refused.expect("dup2 must eventually refuse to grow the handle table"); @@ -134,10 +154,9 @@ fn main() { "handle table reached {n} slots, past the {MAX_HANDLES} cap" ); - // The cap is a live limit, not a latched failure. Every slot below `n` is - // at generation 0, so its handle is the bare slot index. - for slot in 3..n { - syscall::close(RawHandle(u32::from(slot))); + // The cap is a live limit, not a latched failure. + for handle in filled { + syscall::close(handle); } let reused = syscall::dup2(RawHandle(1), 3) .expect("dup2 must work again after closing handles"); @@ -146,3 +165,77 @@ fn main() { unsafe { syscall::munmap(region, REGION) }.expect("munmap"); println!("handle table capped at {MAX_HANDLES} on every insert path (refused at {n})"); } + +/// `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}"); + // SAFETY: every pointer names a live local for the whole call. + unsafe { + syscall::spawn(&SpawnArgs { + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + slot_map_ptr: 0, + slot_map_count: 0, + env_ptr: 0, + env_len: 0, + endow_ptr: entries.as_ptr() as u64, + endow_count: entries.len() as u64, + labels_ptr: labels.as_ptr() as u64, + labels_len: labels.len() as u64, + cwd_ptr: CWD.as_ptr() as u64, + cwd_len: CWD.len() as u64, + image: 0, + image_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), + }) + } +} + +/// An entry labelled `self` would shadow the kernel's in the child's lookup: +/// refused before anything moves, so the pipe end it named is still this +/// process's and still carries a byte. +fn a_caller_endows_no_self() { + let ends = syscall::pipe().expect("a pipe for the self arm"); + let entry = EndowEntry { label_off: 0, label_len: SELF_LABEL.len() as u32, handle: ends.write, _pad: 0 }; + let err = spawn_endowed(&[entry], SELF_LABEL.as_bytes()).expect_err("an endowment labelled self must be refused"); + assert_eq!(err, SyscallError::InvalidArgument, "wrong error for an endowment labelled self"); + assert_eq!(syscall::write(ends.write, b"x"), Ok(1), "the end an endowment labelled self named is gone"); + let mut byte = [0u8; 1]; + assert_eq!(syscall::read(ends.read, &mut byte), Ok(1), "the byte did not arrive"); + syscall::close(ends.read); + syscall::close(ends.write); +} + +/// `MAX_ENDOWMENTS` entries naming distinct `TRANSFER` handles, so the count +/// is the only refusal: refused, and one fewer starts. +fn a_spawn_carries_one_entry_fewer_than_a_table() { + let mut handles = Vec::with_capacity(MAX_ENDOWMENTS); + while handles.len() < MAX_ENDOWMENTS { + let ends = syscall::pipe().expect("a pipe for the count arm"); + handles.extend([ends.read, ends.write]); + } + let mut labels = Vec::new(); + let entries: Vec = handles + .iter() + .enumerate() + .map(|(i, &handle)| { + let label = format!("e{i:02}"); + let entry = + EndowEntry { label_off: labels.len() as u32, label_len: label.len() as u32, handle, _pad: 0 }; + labels.extend_from_slice(label.as_bytes()); + entry + }) + .collect(); + let err = spawn_endowed(&entries, &labels).expect_err("a spawn carrying MAX_ENDOWMENTS entries must be refused"); + assert_eq!(err, SyscallError::InvalidArgument, "wrong error for MAX_ENDOWMENTS entries"); + + let fewer = MAX_ENDOWMENTS - 1; + let used = entries[fewer].label_off as usize; + let child = spawn_endowed(&entries[..fewer], &labels[..used]) + .unwrap_or_else(|e| panic!("a spawn carrying {fewer} entries was refused: {e:?}")); + assert_eq!(syscall::process_wait(child), Ok(0), "the child carrying {fewer} entries"); + syscall::close(child); + syscall::close(handles[fewer]); + println!("an endowment labelled self and {MAX_ENDOWMENTS} entries are refused, and {fewer} start"); +} diff --git a/tests/toyos-rust-tests/src/bin/abuse_page_straddle.rs b/tests/toyos-rust-tests/src/bin/abuse_page_straddle.rs index d35a56bd2aa..f892b92dc5a 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_page_straddle.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_page_straddle.rs @@ -130,6 +130,7 @@ fn main() { cwd_len: CWD.len() as u64, image: 0, image_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }; let placed = (boundary - 8) as *mut SpawnArgs; unsafe { placed.write_volatile(args) }; diff --git a/tests/toyos-rust-tests/src/bin/abuse_spawn_argv.rs b/tests/toyos-rust-tests/src/bin/abuse_spawn_argv.rs index 34b14f0c892..e55964e2448 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_spawn_argv.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_spawn_argv.rs @@ -50,6 +50,7 @@ fn main() { cwd_len: CWD.len() as u64, image: 0, image_len: 0, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }; let err = unsafe { diff --git a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs index f47f83c593c..4c85b6bde62 100644 --- a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs +++ b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs @@ -1,7 +1,7 @@ //! A process is something you hold, and holding it is the whole of the right to //! wait for it. //! -//! **There is no zombie here and no parent.** A pid-keyed wait needed the +//! **There is no zombie here.** A pid-keyed wait needed the //! process table to keep a corpse until somebody claimed it, and rules for who //! was allowed to claim one and what happened when nobody did. The exit code //! lives on the object instead, published once by whichever of exit or kill diff --git a/tests/toyos-rust-tests/src/bin/process_tree.rs b/tests/toyos-rust-tests/src/bin/process_tree.rs new file mode 100644 index 00000000000..10c72441667 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/process_tree.rs @@ -0,0 +1,436 @@ +//! A parent's end takes its children down. +//! +//! **Every end takes the whole subtree.** A starts B; B spawns C, launches D +//! through init and asks init for E; B hands A a handle to each and a copy of +//! its own `self`. Then B ends — killed by A, and by a CPU fault, one B per +//! arm — and the arm asserts, once A's wait on B answers: +//! +//! - C and D have ended, as killed; +//! - E, which init started, runs on: init is the one way to outlive a starter; +//! - after the kill, a spawn and a launch under B's `self` answer `Gone`. +//! +//! B's first act is a spawn the loader refuses once it is admitted under B: its +//! hold on B goes with it, or B is never published and the arm's wait never +//! answers. +//! +//! **The other arms.** A `MANAGE`-only handle is no place, and init refuses a +//! launch whose place is a pipe and answers the launches after it. A launch +//! carries a copy of its place, so std refuses one under a place this process +//! cannot duplicate, rather than spawning it directly. init starts a +//! child only by a launch, so std refuses `under_init` for a program no row +//! declares and for a command carrying an extra slot. A chain alternating spawn +//! and launch — each link launches a shell, and the shell spawns the next link +//! — stops where the kernel refuses a process more than `MAX_DEPTH` below +//! init, and dies whole with its first link. And a `cat` a shell `detach`es +//! outlives the shell. +//! +//! Every wait is unbounded: the harness ceiling is the only clock. + +use std::io::{BufRead, BufReader}; +use std::os::toyos::process::{ChildExt, CommandExt}; +use std::process::{Child, ChildStdin, Command, Stdio}; + +use toyos::endow::{self, Endowments, SVC_LABEL, SYSCAP_LABEL}; +use toyos::ipc::Connection; +use toyos::launch::{self, Launch, Outcome, Parent}; +use toyos::process::Process; +use toyos::syscap::SysCap; +use toyos::{namespace, port, AsHandle}; +use toyos_abi::handle::Rights; +use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; +use toyos_abi::RawHandle; + +const SELF_PATH: &str = "/system/bin/test_rs_process_tree"; +/// `/system/bin/toybox` under the name of an applet that reads its input until +/// it ends: a declared program, so a launch, and one that runs until killed +/// while A holds its input open. +const HELD: &str = "/system/bin/cat"; +const SHELL: &str = "/system/bin/shell"; + +/// The name B's namespace carries the port back to A under. +const BACK: &str = "back"; + +/// B to A: the handles to C, D, B's own `self` and E, in that order. +const MSG_GROWN: u32 = 1; +/// A to B: fault. +const MSG_FAULT: u32 = 2; + +/// `process::KILLED_EXIT_CODE`. +const KILLED: i32 = 137; +/// What `syscall::kill_process(-1)` publishes for a Ring 3 CPU fault. +const CPU_FAULT: i32 = -1; + +/// `toyos_proclife::MAX_DEPTH`, which the kernel refuses a process past. +const MAX_DEPTH: u32 = 64; + +/// The links a chain may grow before a refusal must have stopped it: each +/// adds two levels, so from any start below init the refusal comes sooner. +const LINK_BOUND: u32 = MAX_DEPTH / 2 + 1; + +/// `sched::payload`'s state for a thread whose entry is a zombie, which +/// `sys_sysinfo` also answers for an exited one. +const ZOMBIE: u8 = 3; + +#[derive(Clone, Copy, Debug)] +enum End { + /// B waits on A; A kills it. + Killed, + CpuFault, +} + +fn main() { + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("b") => b(), + Some("c") => park(), + Some("link") => link(args[2].parse().expect("a link's index")), + Some(other) => panic!("unknown role {other:?}"), + None => test(), + } +} + +fn test() { + for end in [End::Killed, End::CpuFault] { + an_end_takes_its_subtree(end); + } + a_manage_only_handle_is_no_place(); + a_pipe_is_no_place(); + a_place_without_dup_is_refused(); + init_is_asked_only_by_a_launch(); + a_chain_stops_at_max_depth_and_dies_whole(); + a_detached_program_outlives_its_shell(); + println!("process_tree: PASS"); +} + +/// The estate's system capability, for the roster. +fn cap() -> &'static SysCap { + static CAP: std::sync::OnceLock = std::sync::OnceLock::new(); + CAP.get_or_init(|| { + Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows every binary it spawns a system capability") + }) +} + +/// What A holds of one B and its subtree. +struct Grown { + b: Child, + conn: Connection, + c: Process, + d: Process, + b_self: RawHandle, + e: Process, + /// The input D and E read, kept open so neither ends by itself. + _held: ChildStdin, +} + +/// Start B, holding a port back to A beside this process's own namespace, and +/// take what it sends once its subtree is grown. +fn grow() -> Grown { + let (acceptor, connector) = port::create().expect("a port of our own"); + let own = endow::namespace().expect("test-runner endows its namespace, `launcher` in it"); + let ns = namespace::build() + .keep_all(own) + .add(BACK, &connector) + .finish() + .expect("a namespace with the port back beside ours"); + let mut b = Command::new(SELF_PATH) + .arg("b") + .endow(SVC_LABEL, ns.into_raw().0) + .stdin(Stdio::piped()) + .spawn() + .expect("spawn B"); + let held = b.stdin.take().expect("B's input"); + let conn = acceptor.accept().expect("B connected back"); + let header = conn.recv_header().expect("B's word that its subtree is grown"); + assert_eq!(header.msg_type, MSG_GROWN, "B said something else"); + let [c, d, b_self, e] = conn.recv_handles_exact::<4>().expect("B sent four handles"); + // SAFETY: B moved each into this table with the frame, and nothing else + // answers for them. + let (c, d, e) = unsafe { (Process::from_raw(c), Process::from_raw(d), Process::from_raw(e)) }; + Grown { b, conn, c, d, b_self, e, _held: held } +} + +fn an_end_takes_its_subtree(end: End) { + let mut grown = grow(); + assert_eq!(grown.c.try_wait(), Err(SyscallError::WouldBlock), "C ended before B did"); + assert_eq!(grown.d.try_wait(), Err(SyscallError::WouldBlock), "D ended before B did"); + + let b_code = match end { + End::Killed => { + grown.b.kill().expect("kill B"); + // Claimed, so admission under it is closed, whether or not its + // teardown is done. + under_b_is_gone(&grown); + KILLED + } + End::CpuFault => { + grown.conn.send_bytes(MSG_FAULT, &[]).expect("tell B to fault"); + CPU_FAULT + } + }; + + let status = grown.b.wait().expect("wait for B"); + assert_eq!(status.code(), Some(b_code), "B ended {end:?} and read {:?}", status.code()); + assert_eq!(grown.c.try_wait(), Ok(KILLED), "C was not ended, as killed, once B's end was published ({end:?})"); + assert_eq!(grown.d.try_wait(), Ok(KILLED), "D, which init launched under B, was not ended with B ({end:?})"); + assert_eq!(grown.e.try_wait(), Err(SyscallError::WouldBlock), "E, started under init, ended with B ({end:?})"); + + grown.e.kill().expect("kill E"); + assert_eq!(grown.e.wait(), Ok(KILLED)); + syscall::close(grown.b_self); + println!(" B {end:?}: C and D ended, as killed; E, under init, ran on"); +} + +/// A spawn and a launch placed under B's `self` both answer `Gone`. +fn under_b_is_gone(grown: &Grown) { + let when = "after its kill"; + match spawn_under(grown.b_self) { + Err(SyscallError::Gone) => {} + Err(other) => panic!("a spawn under B {when} answered {other:?}, not Gone"), + Ok(_) => panic!("a spawn under B {when} started"), + } + let place = syscall::dup(grown.b_self).expect("a copy of B's self to send"); + let conn = endow::service("launcher").expect("this process holds a launcher connector"); + let request = Launch { + program: HELD, + argv: b"/system/bin/cat", + env: b"", + cwd: "/", + extras: &[], + slots: &[], + parent: Parent::Place(place), + }; + let mut home = [0u8; 256]; + match launch::launch(&conn, &request, &mut home) { + Ok(Outcome::Gone) => {} + Ok(Outcome::Started(child)) => panic!("a launch under B {when} started {child:?}"), + Ok(_) => panic!("a launch under B {when} was answered as something other than gone"), + Err(_) => panic!("the launcher did not answer a launch under B {when}"), + } +} + +/// `SYS_SPAWN` of this binary as a C under `place`, killed again if it starts. +fn spawn_under(place: RawHandle) -> Result { + let argv = format!("{SELF_PATH}\0c"); + let args = SpawnArgs { + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + slot_map_ptr: 0, + slot_map_count: 0, + env_ptr: 0, + env_len: 0, + endow_ptr: 0, + endow_count: 0, + labels_ptr: 0, + labels_len: 0, + cwd_ptr: "/".as_ptr() as u64, + cwd_len: 1, + image: 0, + image_len: 0, + place: u64::from(place.0), + }; + // SAFETY: every pointer names a buffer of this frame that outlives the call. + let child = unsafe { syscall::spawn(&args) }?; + // SAFETY: the kernel installed it for this call. + Ok(unsafe { Process::from_raw(child) }) +} + +/// A handle to a live process that carries `MANAGE` and not `WRITE` names no +/// place: the right is refused, and the caller lives. +fn a_manage_only_handle_is_no_place() { + let mut child = Command::new(SELF_PATH).arg("c").spawn().expect("spawn a C"); + let manage = syscall::dup_narrowed(RawHandle(child.as_raw_handle()), Rights::MANAGE) + .expect("a MANAGE-only copy"); + match spawn_under(manage) { + Err(SyscallError::PermissionDenied) => {} + Err(other) => panic!("a MANAGE-only place answered {other:?}"), + Ok(_) => panic!("a MANAGE-only place took a child"), + } + syscall::close(manage); + child.kill().expect("kill the C"); + assert_eq!(child.wait().expect("wait the C").code(), Some(KILLED)); + println!(" a MANAGE-only handle is no place: PermissionDenied"); +} + +/// A pipe's write end carries `WRITE`, so the kernel reaches the type it is +/// not: init answers the launch refused, and the arms after this one are init +/// answering the next. +fn a_pipe_is_no_place() { + let (_read, write) = toyos::pipe_pair().expect("a pipe of our own"); + let place = syscall::dup(write.as_handle()).expect("a duplicate to send"); + let conn = endow::service("launcher").expect("this process holds a launcher connector"); + let request = Launch { + program: HELD, + argv: b"/system/bin/cat", + env: b"", + cwd: "/", + extras: &[], + slots: &[], + parent: Parent::Place(place), + }; + let mut home = [0u8; 256]; + match launch::launch(&conn, &request, &mut home) { + Ok(Outcome::Refused) => {} + Ok(Outcome::Started(child)) => panic!("a launch whose place is a pipe started {child:?}"), + Ok(_) => panic!("a launch whose place is a pipe was answered as something other than refused"), + Err(_) => panic!("the launcher did not answer a launch whose place is a pipe"), + } + println!(" a pipe is no place: init refused the launch"); +} + +/// This process's `self` narrowed to `WRITE`: a place the kernel takes and a +/// launch cannot carry. The duplicate's refusal is the spawn's, and nothing +/// starts in place of the launch. +fn a_place_without_dup_is_refused() { + let place = syscall::dup_narrowed(endow::this_process().as_handle(), Rights::WRITE) + .expect("a WRITE-only copy of this process's self"); + match Command::new(HELD).under(place.0).stdin(Stdio::null()).spawn() { + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {} + Err(e) => panic!("a launch under a place without DUP was refused as {e:?}, not PermissionDenied"), + Ok(mut child) => { + let _ = child.kill(); + panic!("a launch under a place without DUP started its child directly"); + } + } + syscall::close(place); + println!(" a place without DUP: the launch is refused, and nothing starts"); +} + +/// init is reached only by a launch: std refuses `under_init` for what the +/// launcher would not start under init, and starts nothing. +fn init_is_asked_only_by_a_launch() { + let undeclared = Command::new(SELF_PATH).arg("c").under_init().spawn(); + refused(undeclared, "a program no row declares"); + + let (_read, write) = toyos::pipe_pair().expect("a pipe of our own"); + let extra = Command::new(HELD).inherit_handle(5, write.as_handle().0).under_init().spawn(); + refused(extra, "a command carrying an extra slot"); + println!(" init is asked only by a launch: the two others are refused"); +} + +fn refused(spawned: std::io::Result, what: &str) { + match spawned { + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {} + Err(e) => panic!("under_init for {what} was refused as {e:?}, not PermissionDenied"), + Ok(mut child) => { + let _ = child.kill(); + panic!("under_init for {what} started it"); + } + } +} + +/// Each link launches a shell, and the shell spawns the next link: until a +/// start is refused for its depth, which the link or the shell says on the +/// chain's one output. Killing the first link then ends every one. +fn a_chain_stops_at_max_depth_and_dies_whole() { + let mut first = Command::new(SELF_PATH) + .args(["link", "1"]) + .stdout(Stdio::piped()) + .spawn() + .expect("spawn the chain's first link"); + let mut said = BufReader::new(first.stdout.take().expect("the chain's output")); + let mut started = 0; + let stop = loop { + let mut line = String::new(); + assert!(said.read_line(&mut line).expect("read the chain") > 0, "the chain ended without a refusal"); + let line = line.trim().to_string(); + if let Some(k) = line.strip_prefix("link ") { + started = k.parse().expect("a link's index"); + } else if line.starts_with("unbounded") { + panic!("the chain passed link {LINK_BOUND} with no depth refusal: {line}"); + } else if line.contains("refused") || line.ends_with(": not found") { + break line; + } + }; + assert!(started >= 2, "the chain stopped at link {started}: {stop}"); + + first.kill().expect("kill the first link"); + assert_eq!(first.wait().expect("wait the first link").code(), Some(KILLED)); + let live = live_named(&["test_rs_process_tree", "shell"]); + let me = syscall::getpid().raw(); + let others: Vec<_> = live.iter().filter(|(pid, _)| *pid != me).collect(); + assert!(others.is_empty(), "the chain's first link ended and these of it run on: {others:?}"); + println!(" a chain of {started} links stopped at a depth refusal ({stop}) and died whole"); +} + +fn link(k: u32) -> ! { + println!("link {k}"); + if k > LINK_BOUND { + println!("unbounded at link {k}"); + park(); + } + let next = Command::new(SHELL).arg("-c").arg(format!("{SELF_PATH} link {}", k + 1)).spawn(); + if let Err(e) = next { + println!("link {k}: its shell was refused: {e}"); + } + park(); +} + +/// A `cat` a shell `detach`es is init's child, so it runs on once the shell's +/// end is published: it reads the input this process holds open. +fn a_detached_program_outlives_its_shell() { + let mut shell = Command::new(SHELL) + .args(["-c", &format!("detach {HELD}")]) + .stdin(Stdio::piped()) + .spawn() + .expect("run a shell that detaches cat"); + let input = shell.stdin.take().expect("the input the shell and cat read"); + let status = shell.wait().expect("wait the shell"); + assert!(status.success(), "the shell's detach failed: {status:?}"); + let live = live_named(&["cat"]); + assert_eq!(live.len(), 1, "the cat the shell detached ended with the shell: {live:?}"); + drop(input); + println!(" a detached cat outlived its shell"); +} + +/// Every process whose main thread is named one of `names` and has not +/// ended, as `(pid, name)`. +fn live_named(names: &[&str]) -> Vec<(u32, String)> { + const HEADER: usize = toyos::system::SYSINFO_HEADER_SIZE; + const ENTRY: usize = toyos::system::SYSINFO_ENTRY_SIZE; + let mut buf = vec![0u8; HEADER + ENTRY * 1024]; + let n = cap().roster(&mut buf); + assert!((HEADER..=buf.len()).contains(&n), "sysinfo answered {n}"); + (HEADER..) + .step_by(ENTRY) + .take_while(|pos| pos + ENTRY <= n) + .map(|pos| &buf[pos..pos + ENTRY]) + .filter(|entry| entry[9] == 0 && entry[8] != ZOMBIE) + .map(|entry| { + let pid = u32::from_le_bytes(entry[0..4].try_into().unwrap()); + let name = String::from_utf8_lossy(&entry[32..60]).trim_end_matches('\0').to_string(); + (pid, name) + }) + .filter(|(_, name)| names.contains(&name.as_str())) + .collect() +} + +fn park() -> ! { + loop { + std::thread::park(); + } +} + +fn b() -> ! { + let conn = endow::service(BACK).expect("A endowed a port back"); + // Admitted under B, then refused by the loader: its hold on B goes. + assert!(Command::new("/system/bin/no_such_program").spawn().is_err(), "a program that is not there started"); + let c = Command::new(SELF_PATH).arg("c").stdin(Stdio::null()).spawn().expect("B spawns C"); + let d = Command::new(HELD).spawn().expect("B launches D"); + let e = Command::new(HELD).under_init().spawn().expect("B asks init for E"); + let own = endow::this_process(); + let handles = [ + syscall::dup(RawHandle(c.as_raw_handle())).expect("a copy of C"), + syscall::dup(RawHandle(d.as_raw_handle())).expect("a copy of D"), + syscall::dup(own.as_handle()).expect("a copy of B's self"), + syscall::dup(RawHandle(e.as_raw_handle())).expect("a copy of E"), + ]; + conn.send_bytes_with_handles(&handles, MSG_GROWN, &[]).expect("send A the subtree"); + + let header = conn.recv_header().expect("A's word to fault"); + assert_eq!(header.msg_type, MSG_FAULT, "A said something else"); + // SAFETY: a volatile write to address 0, which no region of this process + // covers: it faults, and the kernel ends this process before anything + // after it runs. + unsafe { core::ptr::null_mut::().write_volatile(1) }; + panic!("a write to address 0 did not fault"); +} diff --git a/tests/toyos-rust-tests/src/bin/spawn_image_object.rs b/tests/toyos-rust-tests/src/bin/spawn_image_object.rs index 28b728fd5a4..a4c86de3b8f 100644 --- a/tests/toyos-rust-tests/src/bin/spawn_image_object.rs +++ b/tests/toyos-rust-tests/src/bin/spawn_image_object.rs @@ -47,6 +47,7 @@ fn spawn(image: RawHandle, len: u64) -> Result { cwd_len: CWD.len() as u64, image: image.0 as u64, image_len: len, + place: u64::from(toyos_abi::HANDLE_INVALID.0), }) } } diff --git a/tests/toyos-rust-tests/src/bin/spawn_lands_claimed.rs b/tests/toyos-rust-tests/src/bin/spawn_lands_claimed.rs new file mode 100644 index 00000000000..7cdd26b0aa5 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/spawn_lands_claimed.rs @@ -0,0 +1,112 @@ +//! A spawn whose place is claimed between its commit and its landing lands +//! claimed: the spawn answers its child, the spawner retires it, and the place +//! is published once the child is. +//! +//! That window is the loader's own, so no caller can order a kill inside it: +//! `debug_action::KILL_PLACE_AS_SPAWN_LANDS` has the kernel kill the place of +//! this process's next spawn there. The child parks, so only the retire its +//! spawner posts ends it, and the place's end is published only after the +//! child's: a child claimed and never retired leaves both waits unanswered. +//! +//! Every wait is unbounded: the runner's deadline is the only clock. + +use std::os::toyos::process::CommandExt; +use std::process::{Command, Stdio}; + +use toyos::endow::{self, SVC_LABEL}; +use toyos::process::Process; +use toyos::{namespace, port, AsHandle}; +use toyos_abi::syscall::{self, debug_action, SpawnArgs, SyscallError}; +use toyos_abi::RawHandle; + +const SELF_PATH: &str = "/system/bin/test_rs_spawn_lands_claimed"; + +/// The name the place's namespace carries the port back to the test under. +const BACK: &str = "back"; + +/// The place to the test: a copy of its own `self`. +const MSG_SELF: u32 = 1; + +/// `process::KILLED_EXIT_CODE`. +const KILLED: i32 = 137; + +fn main() { + match std::env::args().nth(1).as_deref() { + Some("place") => place(), + Some("child") => park(), + Some(other) => panic!("unknown role {other:?}"), + None => test(), + } +} + +fn test() { + let (acceptor, connector) = port::create().expect("a port of our own"); + let ns = namespace::build().add(BACK, &connector).finish().expect("a namespace carrying the port back"); + let mut place = Command::new(SELF_PATH) + .arg("place") + .endow(SVC_LABEL, ns.into_raw().0) + .stdin(Stdio::null()) + .spawn() + .expect("spawn the place"); + let conn = acceptor.accept().expect("the place connected back"); + let header = conn.recv_header().expect("the place's word"); + assert_eq!(header.msg_type, MSG_SELF, "the place said something else"); + let [place_self] = conn.recv_handles_exact::<1>().expect("the place sent its self"); + assert!(place.try_wait().expect("ask after the place").is_none(), "the place ended before the marked spawn"); + + assert_eq!( + syscall::debug(debug_action::KILL_PLACE_AS_SPAWN_LANDS), + 0, + "this kernel does not carry SYS_DEBUG, so nothing kills the place" + ); + let child = spawn_under(place_self).expect("a spawn past its commit lands, whatever became of its place"); + match spawn_under(place_self) { + Err(SyscallError::Gone) => {} + Err(other) => panic!("a second spawn under the place answered {other:?}, not Gone"), + Ok(_) => panic!("the kernel did not kill the place of the marked spawn"), + } + assert_eq!(child.wait(), Ok(KILLED), "the child that landed under a claimed place was not ended, as killed"); + let status = place.wait().expect("wait for the place"); + assert_eq!(status.code(), Some(KILLED), "the place was not ended, as killed"); + syscall::close(place_self); + println!("spawn_lands_claimed: the child landed claimed and was retired, and its place was published after it"); +} + +/// `SYS_SPAWN` of this binary as a child under `place`. +fn spawn_under(place: RawHandle) -> Result { + let argv = format!("{SELF_PATH}\0child"); + let args = SpawnArgs { + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + slot_map_ptr: 0, + slot_map_count: 0, + env_ptr: 0, + env_len: 0, + endow_ptr: 0, + endow_count: 0, + labels_ptr: 0, + labels_len: 0, + cwd_ptr: "/".as_ptr() as u64, + cwd_len: 1, + image: 0, + image_len: 0, + place: u64::from(place.0), + }; + // SAFETY: every pointer names a buffer of this frame that outlives the call. + let child = unsafe { syscall::spawn(&args) }?; + // SAFETY: the kernel installed it for this call. + Ok(unsafe { Process::from_raw(child) }) +} + +fn place() -> ! { + let conn = endow::service(BACK).expect("the test endowed a port back"); + let own = syscall::dup(endow::this_process().as_handle()).expect("a copy of this process's self"); + conn.send_bytes_with_handles(&[own], MSG_SELF, &[]).expect("send the test this process's self"); + park() +} + +fn park() -> ! { + loop { + std::thread::park(); + } +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 413a8dc820f..db3d987a60e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -44,6 +44,9 @@ const ACTUATOR_TESTS: &[&str] = &[ // memory, which every other binary in a shared boot moves under it. "handle_lifetime", "shm_release_reclaims", + // Action 22: the kernel kills a spawn's place between the spawn's commit + // and its landing, a window no caller can order a kill inside. + "spawn_lands_claimed", ]; /// What [`ACTUATOR_TESTS`] boots: the one kernel that carries `SYS_DEBUG`, with @@ -74,6 +77,9 @@ const RUST_SKIP: &[&str] = &[ "fault_gate_child", // It takes the machine down; `virt_fatal_halts_the_others_first` runs it. "panic_halts_first", + // Needs a launcher and a declared `cat` and shell, which `tests/testcases` + // does not give: the `process_tree` metal row runs it on tests/proctreecase. + "process_tree", // It asserts nothing at all: it holds a `tests/lancase` boot open for // twenty seconds so the host can reach this machine over the cable. On a // shared boot it would be twenty seconds of nothing. @@ -529,6 +535,13 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + // ---- one image: tests/proctreecase ---- + ( + // A parent's end takes its children down. The guest carries every + // verdict but two the kernel speaks. + "process_tree", + metal::Metal { arms: PROCTREECASE, judge: |b| process_tree(b[0]) }, + ), // ---- one image: tests/metalcase ---- ( "metal_sim_scanout_wc", @@ -639,6 +652,11 @@ const USB_RESET_BOOTS: &[metal::Arm] = &[ const METALCASE: &[metal::Arm] = &[metal::once("metalcase", "tests/metalcase", &[], &[])]; +/// A launcher and a declared `cat` and shell, which `process_tree`'s subtree +/// launches. +const PROCTREECASE: &[metal::Arm] = + &[metal::once("proctreecase", "tests/proctreecase", &[], &["test_rs_process_tree"])]; + /// The cable's own boot: netd in front of the T14's I219, and one job that /// holds the machine up long enough for the host to reach it. The one arm in /// this suite that names a PCI function for the loop to reach the boot over. @@ -2218,6 +2236,26 @@ 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. +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: /system/bin/no_such_program: ")).count(); + if unloaded != 2 { + return Err(format!( + "the loader refused /system/bin/no_such_program {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 init") { + return Err(format!("the kernel's depth refusals were {refused:?}, not one naming depth 65")); + } + Ok(()) +} + /// The kernel reopens init by pid after the last handle to it has gone, and /// no kernel thread's pid opens. fn process_reopen(log: &str) -> Result<(), String> { diff --git a/toyos-abi/src/lib.rs b/toyos-abi/src/lib.rs index 276eb4dbf1b..ee3b40de068 100644 --- a/toyos-abi/src/lib.rs +++ b/toyos-abi/src/lib.rs @@ -48,11 +48,6 @@ impl core::fmt::Display for Pid { } } -impl core::ops::Add for Pid { - type Output = Self; - fn add(self, rhs: Self) -> Self { Pid(self.0 + rhs.0) } -} - /// A thread ID. Identifies a schedulable entity — goes in run queues. /// Every process has at least one thread (the main thread). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 0f00c9fc4aa..56df4fc0839 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -404,9 +404,19 @@ pub struct SpawnArgs { /// `/system/lib` alone. pub image: u64, pub image_len: u64, + /// The process the child is placed under, whose end takes it down: a + /// handle carrying [`Rights::WRITE`] to it — a copy of that process's + /// [`SELF_LABEL`] — or [`HANDLE_INVALID`] for the caller itself. + /// `PermissionDenied` for a handle without `WRITE` and `InvalidArgument` + /// for one to no process, since a place is a handle a peer sent; + /// `Gone` for a process being torn down, and `ResourceExhausted` for a + /// child more than `toyos_proclife::MAX_DEPTH` below init. + /// + /// [`Rights::WRITE`]: crate::handle::Rights::WRITE + pub place: u64, } -const _: () = assert!(core::mem::size_of::() == 112); +const _: () = assert!(core::mem::size_of::() == 120); /// One `(label, handle)` pair of a process's endowment table. /// @@ -434,6 +444,10 @@ const _: () = assert!(core::mem::size_of::() == 16); pub const SYSCAP_LABEL: &str = "syscap"; /// The label for a program's namespace — what its manifest `receives` becomes. pub const SVC_LABEL: &str = "svc"; +/// The label every process starts holding a handle to itself under, carrying +/// `WRITE`, `DUP` and `TRANSFER`: what it hands on for a child to be placed +/// under it ([`SpawnArgs::place`]). The kernel puts it in every table. +pub const SELF_LABEL: &str = "self"; /// `serve:`: the acceptor of a machine-wide port this program serves. pub const SERVE_PREFIX: &str = "serve:"; /// `dev:`: the claim for a device class this program was given. @@ -449,10 +463,12 @@ pub const DEV_PREFIX: &str = "dev:"; /// everything else it gets. pub const PROVIDE_PREFIX: &str = "provide:"; -/// Endowed `(label, handle)` pairs one spawn may carry. Policy on the -/// primitive, refused by name, never truncated — the widest manifest row plus -/// stdio. +/// `(label, handle)` pairs one endowment table holds, the kernel's own +/// [`SELF_LABEL`] among them. Policy on the primitive, refused by name, never +/// truncated — the widest manifest row plus stdio. pub const MAX_ENDOWMENTS: usize = 32; +/// `(label, handle)` pairs one spawn may carry: the kernel adds [`SELF_LABEL`]. +pub const MAX_SPAWN_ENDOWMENTS: usize = MAX_ENDOWMENTS - 1; /// `(child slot, parent handle)` pairs one spawn may carry. /// /// **Derived rather than chosen.** A slot map installs into the child's table, @@ -462,8 +478,10 @@ pub const MAX_ENDOWMENTS: usize = 32; /// pair is a `duplicate_entry` under the parent's own lock — enough of them to /// pass `MAX_HEAP_ALLOC`, where the allocator's refusal is a kernel panic. pub const MAX_SLOT_MAP: usize = RawHandle::MAX_SLOTS; -/// Bytes of label blob one endowment table may carry. +/// Bytes of label blob one endowment table holds, [`SELF_LABEL`]'s among them. pub const MAX_LABELS_LEN: usize = 4096; +/// Bytes of label blob one spawn may carry: the kernel adds [`SELF_LABEL`]'s. +pub const MAX_SPAWN_LABELS_LEN: usize = MAX_LABELS_LEN - SELF_LABEL.len(); use crate::handle::Rights; use crate::pci::{DmaGrant, DmaMapping}; @@ -864,6 +882,11 @@ pub mod debug_action { /// Emit one patterned kernel log record, `logstorm t=0 i= …`, whose /// text the reader regenerates from its two numbers. pub const LOG_PATTERNED: u64 = 21; + /// Mark the caller's next spawn that reaches its commit: the kernel kills + /// the process it is placed under after the commit and before the child + /// lands. That window is the loader's own, so no caller can order a kill + /// inside it; the kill and the landing that follow are the shipped paths. + pub const KILL_PLACE_AS_SPAWN_LANDS: u64 = 22; } /// Every kind of kernel object, in the order the kernel's own `kobject!` diff --git a/toyos-proclife/Cargo.toml b/toyos-proclife/Cargo.toml index 8a1e7811fe0..42b91e0c64c 100644 --- a/toyos-proclife/Cargo.toml +++ b/toyos-proclife/Cargo.toml @@ -63,6 +63,30 @@ mutate-last-out-leaves-before-its-teardown = [] # `interleave::tests::a_join_racing_the_kill_that_takes_its_target` must red # under this. mutate-join-collects-in-a-teardown = [] +# `tree::land_child` stops re-reading its place's claim, so a spawn admitted +# before a kill lands unclaimed under the place after the walk read its children. +# `interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it` +# must red under this. +mutate-place-skips-the-insert-recheck = [] +# `tree::refuse_child` keeps a failed spawn's hold on its place, which then +# waits for a child that will never be and is never published. The same test +# must red under this. +mutate-refused-spawn-keeps-the-count = [] +# `tree::land_child` claims a child landed under a place claimed since its +# admission and answers no thread to retire, so the child runs on claimed, is +# never torn down, and holds its place unpublished. +# `interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it` +# must red under this. +mutate-landed-child-retires-nothing = [] +# `tree::teardown_done` publishes an end with its own teardown, whatever is +# still unpublished below it. `interleave::tests::an_exit_publishes_after_every_end_below_it` +# must red under this. +mutate-publish-before-the-children = [] +# The model's walk claims everything it owes in one lock section, as a kernel +# that held `PROCESS_TABLE` across its walk would. +# `interleave::tests::a_spawn_under_an_unrelated_process_lands_between_two_claims_of_one_walk` +# must red under this. +mutate-walk-in-one-hold = [] [dependencies] toyos-abi = { path = "../toyos-abi" } diff --git a/toyos-proclife/src/interleave.rs b/toyos-proclife/src/interleave.rs index 0edb88edb84..134a58dd2bb 100644 --- a/toyos-proclife/src/interleave.rs +++ b/toyos-proclife/src/interleave.rs @@ -22,24 +22,47 @@ use alloc::vec; use alloc::vec::Vec; use std::collections::HashSet; -use crate::model::World; +use crate::model::{Climb, World}; use crate::table::Processes; +use crate::tree::{self, Admit, Admitted}; use crate::{join, reap, spawn, teardown, Pid, Tid, Watch}; +/// `process::KILLED_EXIT_CODE`, which a walk claims every process below an end +/// with. +const KILLED: i32 = 137; + /// One kernel path, mid-flight. /// /// Each variant's `pc` is the number of lock sections it has completed, and /// every field beside it is a value the real path carries in a local across a /// lock release — which is the whole reason a window exists to explore. A /// thread's own way out after its operation is the [`World`]'s: `depart_step`. +/// +/// An exit's and a kill's `owed` is its walk: the processes below its end not +/// yet claimed, one claim per lock section, each claim's retires posted with +/// the lock given up. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Op { /// `process::exit`: a thread ending its own process — claim, retire the - /// rest, leave. - Exit { pid: Pid, tid: Tid, code: i32, pc: u32, retire: Vec }, - /// `process::kill_process`: claim, retire every thread, return. `by` is the - /// killing thread when the model holds it. - Kill { pid: Pid, code: i32, pc: u32, retire: Vec, by: Option<(Pid, Tid)> }, + /// rest, walk the subtree, leave. + Exit { pid: Pid, tid: Tid, code: i32, pc: u32, retire: Vec<(Pid, Tid)>, owed: Vec }, + /// `process::kill_process`: claim, retire every thread, walk the subtree, + /// return. `by` is the killing thread when the model holds it. + Kill { pid: Pid, code: i32, pc: u32, retire: Vec<(Pid, Tid)>, owed: Vec, by: Option<(Pid, Tid)> }, + /// `loader::spawn` under `place` by `by`'s thread: the admission, the + /// whole of a process built with every lock given up, then the move of + /// the caller's handles and the landing, whose retires the landing + /// answers. A build that `fails` lets the place go instead, and climbs + /// when that was the place's last hold. + SpawnUnder { + place: Pid, + by: (Pid, Tid), + fails: bool, + pc: u32, + admitted: Option, + retire: Vec<(Pid, Tid)>, + climb: Option, + }, /// `process::spawn_thread`: two lock sections with the whole of a thread /// built between them; `block` is the mapped TLS the build carries across. Spawn { pid: Pid, pc: u32, block: Option }, @@ -53,15 +76,24 @@ pub enum Op { impl Op { pub fn exit(pid: Pid, tid: Tid, code: i32) -> Self { - Op::Exit { pid, tid, code, pc: 0, retire: Vec::new() } + Op::Exit { pid, tid, code, pc: 0, retire: Vec::new(), owed: Vec::new() } } pub fn kill(pid: Pid, code: i32) -> Self { - Op::Kill { pid, code, pc: 0, retire: Vec::new(), by: None } + Op::Kill { pid, code, pc: 0, retire: Vec::new(), owed: Vec::new(), by: None } } /// A kill issued by a thread the model holds, which is in the kernel until /// the kill returns. pub fn kill_by(pid: Pid, code: i32, by: (Pid, Tid)) -> Self { - Op::Kill { pid, code, pc: 0, retire: Vec::new(), by: Some(by) } + Op::Kill { pid, code, pc: 0, retire: Vec::new(), owed: Vec::new(), by: Some(by) } + } + /// A spawn under `place` by `by`'s thread, which is in the kernel until it + /// returns. + pub fn spawn_under(place: Pid, by: (Pid, Tid)) -> Self { + Op::SpawnUnder { place, by, fails: false, pc: 0, admitted: None, retire: Vec::new(), climb: None } + } + /// The same spawn, whose build fails once it is admitted. + pub fn spawn_under_failing(place: Pid, by: (Pid, Tid)) -> Self { + Op::SpawnUnder { place, by, fails: true, pc: 0, admitted: None, retire: Vec::new(), climb: None } } pub fn spawn(pid: Pid) -> Self { Op::Spawn { pid, pc: 0, block: None } @@ -82,6 +114,7 @@ impl Op { Op::Exit { pid, tid, .. } | Op::ThreadExit { pid, tid, .. } => Some((pid, tid)), Op::Join { pid, waiter, .. } => Some((pid, waiter)), Op::Kill { by, .. } => by, + Op::SpawnUnder { by, .. } => Some(by), Op::Spawn { .. } | Op::IdlePass { .. } => None, } } @@ -90,6 +123,7 @@ impl Op { match self { Op::Exit { pc, .. } | Op::Kill { pc, .. } + | Op::SpawnUnder { pc, .. } | Op::Spawn { pc, .. } | Op::ThreadExit { pc, .. } | Op::Join { pc, .. } @@ -101,7 +135,7 @@ impl Op { /// cannot run until that process publishes its exit. fn awaits_teardown(&self, world: &World) -> Option { match *self { - Op::Kill { pid, pc: 2, .. } if world.published(pid).is_none() => Some(pid), + Op::Kill { pid, pc: WAIT, .. } if world.published(pid).is_none() => Some(pid), _ => None, } } @@ -110,6 +144,7 @@ impl Op { match self { Op::Exit { .. } => "exit", Op::Kill { .. } => "kill", + Op::SpawnUnder { .. } => "spawn_under", Op::Spawn { .. } => "spawn_thread", Op::ThreadExit { .. } => "thread_exit", Op::Join { .. } => "thread_join", @@ -120,38 +155,58 @@ impl Op { /// Run one lock section. fn step(&mut self, world: &mut World) { match self { - Op::Exit { pid, tid, code, pc, retire } => match *pc { - // The claim and the threads its winner retires, under the table lock. + Op::Exit { pid, tid, code, pc, retire, owed } => match *pc { + // The claim, the threads its winner retires and the children + // it owes, under the table lock. 0 => { - if teardown::claim_teardown(world, *pid, *code) { - *retire = teardown::retire_set(world.get(*pid).expect("just claimed"), Some(*tid)); + if tree::claim(world, *pid, *code, owed) { + let proc = world.get(*pid).expect("just claimed"); + *retire = teardown::retire_set(proc, Some(*tid)).into_iter().map(|t| (*pid, t)).collect(); } - *pc = 1; + *pc = POST; } - // With the lock given up: the retires, then this thread's own way out. - _ => { - for &other in retire.iter() { - world.post_retire(*pid, other); + // With the lock given up: the retires, then the walk's next + // claim, or this thread's own way out. + POST => { + for (victim, thread) in retire.drain(..) { + world.post_retire(victim, thread); + } + if owed.is_empty() { + world.depart(*pid, *tid, None); + *pc = DONE; + } else { + *pc = WALK; } - world.depart(*pid, *tid, None); - *pc = DONE; + } + _ => { + walk_claim(world, owed, retire); + *pc = POST; } }, - Op::Kill { pid, code, pc, retire, by } => { + Op::Kill { pid, code, pc, retire, owed, by } => { match *pc { 0 => { - if teardown::claim_teardown(world, *pid, *code) { - *retire = teardown::retire_set(world.get(*pid).expect("just claimed"), None); - *pc = 1; + if tree::claim(world, *pid, *code, owed) { + let proc = world.get(*pid).expect("just claimed"); + *retire = teardown::retire_set(proc, None).into_iter().map(|t| (*pid, t)).collect(); + *pc = POST; } else { *pc = DONE; } } - 1 => { - for &tid in retire.iter() { - world.post_retire(*pid, tid); + POST => { + for (victim, thread) in retire.drain(..) { + world.post_retire(victim, thread); } - *pc = if cfg!(feature = "mutate-kill-waits-for-its-victims") { 2 } else { DONE }; + *pc = match owed.is_empty() { + false => WALK, + true if cfg!(feature = "mutate-kill-waits-for-its-victims") => WAIT, + true => DONE, + }; + } + WALK => { + walk_claim(world, owed, retire); + *pc = POST; } // The mutation's wait, which `enabled` holds until the victim's exit is published. _ => *pc = DONE, @@ -162,6 +217,64 @@ impl Op { } } } + Op::SpawnUnder { place, by, fails, pc, admitted, retire, climb } => { + match *pc { + // The admission, under the table lock and before anything is built. + 0 => match tree::admit_child(world, Some(*place)) { + Admit::Yes(taken) => { + *admitted = Some(taken); + *pc = 1; + } + Admit::Gone | Admit::TooDeep { .. } | Admit::NoPid => { + world.refuse_spawn(*by); + *pc = DONE; + } + }, + // The build happened with every lock given up, and failed. + 1 if *fails => { + let taken = admitted.take().expect("admitted at the first section"); + world.refuse_spawn(*by); + match tree::refuse_child(world, taken) { + Some(publish) => { + *climb = Some(Climb::Publish(publish)); + *pc = 3; + } + None => *pc = DONE, + } + } + // The caller's handles move, and the child lands, in the + // hold that inserts it. + 1 => { + world.move_handles(*by); + let taken = admitted.take().expect("admitted at the first section"); + let child = taken.pid(); + let ((), owed) = + tree::land_child(world, taken, KILLED, |world, node| world.insert(child, node)); + world.landed(*place, child); + *retire = owed.into_iter().map(|t| (child, t)).collect(); + *pc = 2; + } + // With the lock given up: the retires the landing answered. + 2 => { + for (victim, thread) in retire.drain(..) { + world.post_retire(victim, thread); + } + *pc = DONE; + } + // The failed build let its place's last hold go: this + // spawner publishes it, and climbs. + _ => { + let at = climb.take().expect("a climb is owed here"); + *climb = world.climb_step(at); + if climb.is_none() { + *pc = DONE; + } + } + } + if *pc == DONE { + world.leave_kernel(*by); + } + } Op::Spawn { pid, pc, block } => match *pc { // Phase 1, under the table lock. 0 => { @@ -240,6 +353,28 @@ impl Op { } const DONE: u32 = u32::MAX; +/// An exit's or a kill's section that posts the retires its last claim owes. +const POST: u32 = 1; +/// The walk's next claim. +const WALK: u32 = 2; +/// `mutate-kill-waits-for-its-victims`' wait. +const WAIT: u32 = 3; + +/// The walk's next claim, under the table lock: one process, its retires and +/// the children it owes. Under `mutate-walk-in-one-hold`, every process the +/// walk owes, in this one section. +fn walk_claim(world: &mut World, owed: &mut Vec, retire: &mut Vec<(Pid, Tid)>) { + loop { + let Some(next) = owed.pop() else { return }; + if tree::claim(world, next, KILLED, owed) { + let proc = world.get(next).expect("just claimed"); + retire.extend(teardown::retire_set(proc, None).into_iter().map(|t| (next, t))); + } + if !cfg!(feature = "mutate-walk-in-one-hold") { + return; + } + } +} /// One move the explorer can make: an op's next section, or a thread's next /// step out. @@ -258,6 +393,19 @@ enum Move { /// before is not walked again: every law is a property of the state alone. The /// returned string is the schedule that produced it, in the order it ran. pub fn explore(initial: &World, ops: &[Op]) -> Result { + explore_leaves(initial, ops, &mut |_| {}) +} + +/// [`explore`], answering whether some schedule ends in a world `pred` holds +/// of: a property one ordering has, where [`explore`]'s laws are ones every +/// ordering keeps. +pub fn explore_any(initial: &World, ops: &[Op], pred: impl Fn(&World) -> bool) -> Result { + let mut found = false; + explore_leaves(initial, ops, &mut |leaf| found |= pred(leaf))?; + Ok(found) +} + +fn explore_leaves(initial: &World, ops: &[Op], leaf: &mut dyn FnMut(&World)) -> Result { let mut world = initial.clone(); for op in ops { if let Some(actor) = op.actor() { @@ -266,13 +414,19 @@ pub fn explore(initial: &World, ops: &[Op]) -> Result { } let mut trace = Vec::new(); let mut seen = HashSet::new(); - match walk(world, ops.to_vec(), &mut trace, &mut seen) { + match walk(world, ops.to_vec(), &mut trace, &mut seen, leaf) { Some(found) => Err(found), None => Ok(seen.len()), } } -fn walk(world: World, ops: Vec, trace: &mut Vec, seen: &mut HashSet<(World, Vec)>) -> Option { +fn walk( + world: World, + ops: Vec, + trace: &mut Vec, + seen: &mut HashSet<(World, Vec)>, + leaf: &mut dyn FnMut(&World), +) -> Option { if !seen.insert((world.clone(), ops.clone())) { return None; } @@ -284,6 +438,7 @@ fn walk(world: World, ops: Vec, trace: &mut Vec, seen: &mut HashSet< if moves.is_empty() { let stuck: Vec<&str> = ops.iter().filter(|op| !op.done()).map(Op::label).collect(); if stuck.is_empty() { + leaf(&world); return report(&world.final_faults(), trace); } return report(&[alloc::format!("deadlock: {} each wait and none can move", stuck.join(", "))], trace); @@ -312,7 +467,7 @@ fn walk(world: World, ops: Vec, trace: &mut Vec, seen: &mut HashSet< } } } - if let Some(found) = report(&faults, trace).or_else(|| walk(next_world, next_ops, trace, seen)) { + if let Some(found) = report(&faults, trace).or_else(|| walk(next_world, next_ops, trace, seen, leaf)) { trace.pop(); return Some(found); } @@ -570,4 +725,101 @@ mod tests { let sibling = world.spawn_thread(p); holds(&world, vec![Op::kill_by(p, 137, (p, sibling))]); } + + /// **A spawn racing its place's kill**, every ordering, with the spawner a + /// process that holds the place's `self` — init, serving a launch — and + /// its build landing or failing. A child lands before the claim and the + /// walk takes it, or after it and is claimed as it lands; a failed build + /// lets the place go. Nothing runs on under the place, a refused spawn + /// moved none of its caller's handles, and the place is published in + /// every one. + /// + /// Reds under `mutate-place-skips-the-insert-recheck`, where a child lands + /// unclaimed under the claimed place and outlives it, and under + /// `mutate-refused-spawn-keeps-the-count`, where the place is never + /// published. + #[test] + fn a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it() { + let mut world = World::new(); + let init = world.spawn_process(); + let place = world.spawn_child(init); + world.spawn_child(place); + let launcher = (init, world.main_tid(init)); + let states = holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under(place, launcher)]); + std::println!("a spawn racing its place's kill: {states} states"); + holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under_failing(place, launcher)]); + } + + /// The same race with the place spawning under itself: its own thread is + /// in the spawn when the kill claims it, and its teardown waits for that + /// thread to come out. + #[test] + fn a_spawn_racing_the_kill_of_its_own_spawner() { + let mut world = World::new(); + let init = world.spawn_process(); + let place = world.spawn_child(init); + world.spawn_child(place); + let own = (place, world.main_tid(place)); + holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under(place, own)]); + holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under_failing(place, own)]); + } + + /// An exit takes a subtree two deep below it, and each end is published + /// after every end below it. + /// + /// Reds under `mutate-publish-before-the-children`. + #[test] + fn an_exit_publishes_after_every_end_below_it() { + let mut world = World::new(); + let init = world.spawn_process(); + let p = world.spawn_child(init); + let c = world.spawn_child(p); + world.spawn_child(c); + world.spawn_child(p); + let main = world.main_tid(p); + holds(&world, vec![Op::exit(p, main, 0)]); + } + + /// A child's own exit racing its parent's kill: two walks over one + /// subtree, one claim per process whichever walk makes it. + #[test] + fn a_childs_exit_racing_its_parents_kill() { + let mut world = World::new(); + let init = world.spawn_process(); + let p = world.spawn_child(init); + let c = world.spawn_child(p); + world.spawn_child(c); + let main = world.main_tid(c); + holds(&world, vec![Op::kill(p, KILLED), Op::exit(c, main, 3)]); + } + + /// **The walk gives the table up between two claims.** A spawn under a + /// process the walk does not reach lands, in some ordering, after the + /// walk's first claim and before its last: an end of any size holds the + /// table one process at a time. + /// + /// Reds under `mutate-walk-in-one-hold`. + #[test] + fn a_spawn_under_an_unrelated_process_lands_between_two_claims_of_one_walk() { + let mut world = World::new(); + let init = world.spawn_process(); + let p = world.spawn_child(init); + world.spawn_child(p); + world.spawn_child(p); + let unrelated = world.spawn_child(init); + let launcher = (init, world.main_tid(init)); + let ops = vec![Op::kill(p, KILLED), Op::spawn_under(unrelated, launcher)]; + // Two claimed when it landed: the end's own, and one of the two below it. + let between = match explore_any(&world, &ops, |leaf| { + leaf.inserted_at().iter().any(|&(_, claimed)| claimed == 2) + }) { + Ok(between) => between, + Err(found) => panic!("a lifecycle law broke:\n{found}"), + }; + assert!( + between, + "no ordering landed the unrelated spawn between the walk's two claims below the end: \ + the walk holds the table across its claims", + ); + } } diff --git a/toyos-proclife/src/lib.rs b/toyos-proclife/src/lib.rs index 85967980e66..08c5f89c6ab 100644 --- a/toyos-proclife/src/lib.rs +++ b/toyos-proclife/src/lib.rs @@ -2,7 +2,10 @@ //! //! The states are two and the questions about them are the whole of this //! crate: a thread is [`Scheduled`] or it is a [`Zombie`] with a code, and a -//! process is either being torn down by somebody or it is not. What is hard +//! process is either being torn down by somebody or it is not — and, in +//! [`tree`], where a process stands under the others, which its end takes +//! down with it and when that end is published, and in [`pids`], which pid it +//! has. What is hard //! about the subject is not either state — it is that **two CPUs are inside one //! process's lifecycle at once**, and the defects it has are the interleavings //! rather than the arithmetic. A spawn builds a thread's TLS block, its kernel @@ -52,17 +55,21 @@ extern crate alloc; extern crate std; pub mod join; +pub mod pids; pub mod reap; pub mod spawn; pub mod table; pub mod teardown; +pub mod tree; #[cfg(test)] mod interleave; #[cfg(test)] mod model; +pub use pids::Pids; pub use table::{Lifecycle, Processes}; +pub use tree::{Node, MAX_DEPTH}; pub use toyos_abi::{Pid, Tid}; diff --git a/toyos-proclife/src/model.rs b/toyos-proclife/src/model.rs index 1a8704691f1..d29d78f8b43 100644 --- a/toyos-proclife/src/model.rs +++ b/toyos-proclife/src/model.rs @@ -18,7 +18,8 @@ use alloc::vec::Vec; use crate::table::{Lifecycle, Processes}; use crate::teardown::{self, Leave}; -use crate::{Pid, ThreadLocation, Tid, Watch}; +use crate::tree::{self, Admit, Publish}; +use crate::{Node, Pid, Pids, ThreadLocation, Tid, Watch}; /// One process, as its lifecycle sees it. #[derive(Clone, PartialEq, Eq, Hash)] @@ -30,6 +31,7 @@ pub struct ModelProc { /// How many times a claim was raised on this process. Counted here because /// `begin_teardown` has exactly one caller and the count is the law. claims: u32, + node: Node, } impl Lifecycle for ModelProc { @@ -59,6 +61,12 @@ impl Lifecycle for ModelProc { f(tid, at); } } + fn node(&self) -> &Node { + &self.node + } + fn node_mut(&mut self) -> &mut Node { + &mut self.node + } } /// Where a thread is on its way out, one lock section or pass per step. @@ -68,16 +76,25 @@ enum Out { Leave, /// The last one out: `teardown_resources`, the process's mappings and handles. Free { code: i32, mark: i32 }, - /// The last one out: `teardown_bookkeeping`'s table section, which marks it dead. + /// The last one out: `teardown`'s last table section, which marks it dead, + /// stashes its exit and lets its own hold on its publication go. Mark { code: i32, mark: i32 }, - /// The last one out: `ProcessObject::publish_exit`. - Publish { code: i32 }, + /// A publication that hold owed, and the climb behind it. + Climb(Climb), /// `thread_exit`'s post on its own watch. Post, /// The exit pass: the thread never runs again. Gone, } +/// A publication and the climb behind it, one step each: `publish_exit` with +/// no lock held, then the parent's hold lowered under the lock. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Climb { + Publish(Publish), + Lower { parent: Pid, child: Pid }, +} + /// A thread leaving: the code it chose, and how far it is. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] struct Departure { @@ -89,7 +106,7 @@ struct Departure { #[derive(Clone, PartialEq, Eq, Hash)] pub struct World { procs: BTreeMap, - next_pid: Pid, + pids: Pids, /// The exit each process published, which is what `published_exit` reads — /// on the object in the kernel, and never on the entry. published: BTreeMap, @@ -113,6 +130,19 @@ pub struct World { /// TLS blocks a spawn's phase 2 mapped and no thread owns yet. tls_mapped: BTreeSet, next_tls: u32, + /// Exits torn down and not yet published: the kernel keeps each on its + /// entry until its count lets it go. + stash: BTreeMap, + /// Processes that landed unclaimed under a place whose end was already + /// claimed. + landed_under_claimed: BTreeSet, + /// Each process a spawn op inserted, and how many processes had been + /// claimed when it landed. + inserted_at: Vec<(Pid, usize)>, + /// Spawning threads whose caller's handles a spawn moved. + moved: BTreeSet<(Pid, Tid)>, + /// Spawning threads answered a refusal after their handles moved. + refused_after_move: BTreeSet<(Pid, Tid)>, } impl Processes for World { @@ -132,13 +162,21 @@ impl Processes for World { f(pid); } } + fn pids(&mut self) -> &mut Pids { + &mut self.pids + } } impl World { pub fn new() -> Self { + Self::with_pids(Pids::default()) + } + + /// A world whose spawns take their pids from `pids`. + pub fn with_pids(pids: Pids) -> Self { Self { procs: BTreeMap::new(), - next_pid: Pid(1), + pids, published: BTreeMap::new(), waiters: BTreeSet::new(), released: BTreeSet::new(), @@ -150,6 +188,11 @@ impl World { frees: BTreeMap::new(), tls_mapped: BTreeSet::new(), next_tls: 0, + stash: BTreeMap::new(), + landed_under_claimed: BTreeSet::new(), + inserted_at: Vec::new(), + moved: BTreeSet::new(), + refused_after_move: BTreeSet::new(), } } @@ -171,11 +214,65 @@ impl World { self.tls_mapped.remove(&block); } - /// A process with one thread, which is its main one — what - /// `ProcessEntry::new` builds. + /// A process under none with one thread, which is its main one — what + /// the loader builds for init. pub fn spawn_process(&mut self) -> Pid { - let pid = self.next_pid; - self.next_pid = Pid(pid.0 + 1); + self.land(None) + } + + /// A process placed under `place`, admitted and inserted with nothing + /// between: how a test builds the tree its ops start from. + pub fn spawn_child(&mut self, place: Pid) -> Pid { + self.land(Some(place)) + } + + fn land(&mut self, place: Option) -> Pid { + let Admit::Yes(admitted) = tree::admit_child(self, place) else { + panic!("World::land: {place:?} admits no child"); + }; + let pid = admitted.pid(); + let ((), retire) = tree::land_child(self, admitted, 137, |world, node| world.insert(pid, node)); + assert_eq!(retire, [], "World::land: {place:?} was claimed"); + pid + } + + /// A spawn op's landing, at the end of the hold that made it: records a + /// process unclaimed under a claimed place, and how many processes had + /// been claimed when it landed. + pub fn landed(&mut self, place: Pid, pid: Pid) { + let unclaimed = !self.procs[&pid].tearing_down(); + if unclaimed && self.procs.get(&place).is_some_and(Lifecycle::tearing_down) { + self.landed_under_claimed.insert(pid); + } + let claimed = self.claimed_count(); + self.inserted_at.push((pid, claimed)); + } + + /// A spawn by `by` taking its caller's endowed handles out of its table. + pub fn move_handles(&mut self, by: (Pid, Tid)) { + self.moved.insert(by); + } + + /// A spawn by `by` answering a refusal. + pub fn refuse_spawn(&mut self, by: (Pid, Tid)) { + if self.moved.contains(&by) { + self.refused_after_move.insert(by); + } + } + + /// Each process a spawn op inserted, and how many had been claimed then. + pub fn inserted_at(&self) -> &[(Pid, usize)] { + &self.inserted_at + } + + /// Every process claimed so far, those already reaped included. + pub fn claimed_count(&self) -> usize { + let reaped = self.published.keys().filter(|pid| !self.procs.contains_key(pid)).count(); + self.procs.values().filter(|p| p.claims > 0).count() + reaped + } + + /// `pid`'s entry, with one thread, which is its main one. + pub fn insert(&mut self, pid: Pid, node: Node) { let mut threads = BTreeMap::new(); threads.insert(Tid(0), ThreadLocation::Scheduled); self.procs.insert( @@ -186,10 +283,10 @@ impl World { threads, next_tid: Tid(1), claims: 0, + node, }, ); self.mapped.insert((pid, Tid(0))); - pid } /// Insert a thread the way `spawn_thread`'s phase 3 does — in the table and @@ -314,12 +411,16 @@ impl World { } Out::Mark { code, mark } => { teardown::torn_down(self, pid, tid, mark); - Out::Publish { code } - } - Out::Publish { code } => { - self.publish_exit(pid, code); - Out::Post + self.stash.insert(pid, code); + match tree::teardown_done(self, pid) { + Some(publish) => Out::Climb(Climb::Publish(publish)), + None => Out::Post, + } } + Out::Climb(climb) => match self.climb_step(climb) { + Some(next) => Out::Climb(next), + None => Out::Post, + }, Out::Post => { if chosen.is_some() { self.post(Watch::Thread(pid, tid)); @@ -344,6 +445,21 @@ impl World { .map(|(&(_, tid), _)| tid) } + /// One step of a climb, by whichever thread's lowering owed it. + pub fn climb_step(&mut self, climb: Climb) -> Option { + match climb { + Climb::Publish(publish) => { + let code = self + .stash + .remove(&publish.pid) + .expect("a publication takes the exit its teardown stashed"); + self.publish_exit(publish.pid, code); + publish.parent.map(|parent| Climb::Lower { parent, child: publish.pid }) + } + Climb::Lower { parent, child } => tree::published(self, parent, child).map(Climb::Publish), + } + } + /// `ProcessObject::publish_exit`, assertion and all: two publishes mean two /// teardowns claimed one process. pub fn publish_exit(&mut self, pid: Pid, code: i32) { @@ -409,6 +525,20 @@ impl World { "pid {pid} tid {tid}: its entry and its mapped TLS went while a sibling was still in the process", )); } + // L9. An end is published after every end below it. + for (&pid, proc) in &self.procs { + if let Some(parent) = proc.node.parent() { + if self.published.contains_key(&parent) && !self.published.contains_key(&pid) { + out.push(alloc::format!( + "pid {parent} was published with pid {pid} still unpublished below it", + )); + } + } + } + // L10. Nothing lands unclaimed under a place once its end is claimed. + for pid in &self.landed_under_claimed { + out.push(alloc::format!("pid {pid} landed unclaimed under a place whose end was already claimed")); + } out } @@ -418,13 +548,25 @@ impl World { /// case, so checking it at every state would report every schedule. /// And **L5** — every TLS block a spawn mapped ends owned or released. /// And **L6** — every claimed process is torn down: its exit published, - /// every killed thread gone. + /// every killed thread gone. And **L11** — an end takes every process + /// below it. And **L12** — a spawn that answers a refusal moved none of + /// its caller's handles. pub fn final_faults(&self) -> Vec { let mut out = self.faults(); + for (pid, tid) in &self.refused_after_move { + out.push(alloc::format!("pid {pid} tid {tid}: a refused spawn moved its caller's handles")); + } for (&pid, proc) in &self.procs { if proc.claims > 0 && !self.published.contains_key(&pid) { out.push(alloc::format!("pid {pid} was claimed for teardown and never published an exit")); } + if let Some(parent) = proc.node.parent() { + let ended = self.published.contains_key(&parent) + || self.procs.get(&parent).is_some_and(|p| p.claims > 0); + if ended && !self.published.contains_key(&pid) { + out.push(alloc::format!("pid {pid} outlived the end of pid {parent} above it")); + } + } } for &(pid, tid) in &self.killed { let gone = self.departing.get(&(pid, tid)).is_some_and(|d| d.at == Out::Gone); diff --git a/toyos-proclife/src/pids.rs b/toyos-proclife/src/pids.rs new file mode 100644 index 00000000000..ab1345ccbb0 --- /dev/null +++ b/toyos-proclife/src/pids.rs @@ -0,0 +1,94 @@ +//! Which pid a new process gets. +//! +//! **A pid names one process for the life of the machine.** Once an entry +//! holds a pid it is never issued again, so a pid carried across a lock +//! release names that process or nothing. [`Pid::MAX`] is never issued: it is +//! the per-CPU word for no process. +//! +//! A spawn takes its pid at admission, before anything is built, and a spawn +//! refused after that gives it back: no entry held it, so nothing can name +//! it, and a refused spawn spends none. Once every pid below [`Pid::MAX`] is +//! issued, admission refuses by name. + +use alloc::vec::Vec; + +use crate::Pid; + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Pids { + /// The lowest pid never taken; [`Pid::MAX`] once every one has been. + next: Pid, + /// Taken by a spawn refused since, held by no entry: the next taken. + /// At most one per spawn in flight at once. + returned: Vec, +} + +impl Default for Pids { + fn default() -> Self { + Self { next: Pid(0), returned: Vec::new() } + } +} + +impl Pids { + /// A table whose next new pid is `next`, as one that has issued every + /// pid below it. + #[cfg(test)] + pub fn issued_below(next: Pid) -> Self { + Self { next, returned: Vec::new() } + } + + /// A pid no entry has held, or `None` once every pid below [`Pid::MAX`] + /// has been issued. + pub fn take(&mut self) -> Option { + if let Some(pid) = self.returned.pop() { + return Some(pid); + } + if self.next == Pid::MAX { + return None; + } + let pid = self.next; + self.next = Pid(pid.0 + 1); + Some(pid) + } + + /// `pid`, which [`Self::take`] answered to a spawn refused before any + /// entry held it. + pub fn give_back(&mut self, pid: Pid) { + assert!(pid < self.next, "Pids::give_back: pid {pid} was never taken"); + assert!(!self.returned.contains(&pid), "Pids::give_back: pid {pid} given back twice"); + self.returned.push(pid); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pids_are_issued_in_order_from_zero() { + let mut pids = Pids::default(); + assert_eq!([pids.take(), pids.take(), pids.take()], [Some(Pid(0)), Some(Pid(1)), Some(Pid(2))]); + } + + /// The last pid issued is the one below [`Pid::MAX`], and the next take + /// is refused rather than overflowing into it. + #[test] + fn the_last_pid_is_one_below_max_and_none_follows_it() { + let mut pids = Pids::issued_below(Pid(u32::MAX - 1)); + assert_eq!(pids.take(), Some(Pid(u32::MAX - 1))); + assert_eq!(pids.take(), None); + assert_eq!(pids.take(), None); + } + + /// A pid given back is the next one taken, after every pid is issued too. + #[test] + fn a_pid_given_back_is_taken_again() { + let mut pids = Pids::issued_below(Pid(u32::MAX - 2)); + let first = pids.take().unwrap(); + let last = pids.take().unwrap(); + assert_eq!(pids.take(), None); + pids.give_back(first); + pids.give_back(last); + assert_eq!([pids.take(), pids.take(), pids.take()], [Some(last), Some(first), None]); + } +} diff --git a/toyos-proclife/src/table.rs b/toyos-proclife/src/table.rs index 15780b940d5..a225a21bcfe 100644 --- a/toyos-proclife/src/table.rs +++ b/toyos-proclife/src/table.rs @@ -14,7 +14,7 @@ //! an order sorts what it collected, and the two that do //! ([`crate::teardown::retire_set`] and [`crate::reap::finished_pids`]) say so. -use crate::{Pid, ThreadLocation, Tid}; +use crate::{Node, Pid, Pids, ThreadLocation, Tid}; /// One process's lifecycle state: the whole of what a spawn, an exit, a kill, a /// join or a reap reads or writes about it. @@ -50,6 +50,11 @@ pub trait Lifecycle { /// Every thread of this process, in whatever order the container has. fn each_thread(&self, f: &mut dyn FnMut(Tid, ThreadLocation)); + + /// Where this process stands in the tree. Read and written by + /// [`crate::tree`] alone. + fn node(&self) -> &Node; + fn node_mut(&mut self) -> &mut Node; } /// The lifecycle face of the process table. @@ -69,4 +74,7 @@ pub trait Processes { /// Every process in the table, in whatever order the container has. fn each_pid(&self, f: &mut dyn FnMut(Pid)); + + /// The pids not yet issued. + fn pids(&mut self) -> &mut Pids; } diff --git a/toyos-proclife/src/teardown.rs b/toyos-proclife/src/teardown.rs index 61397cdddb8..6c95f3e3375 100644 --- a/toyos-proclife/src/teardown.rs +++ b/toyos-proclife/src/teardown.rs @@ -10,7 +10,7 @@ //! //! **The last thread out tears the process down.** Every thread leaves by its //! own hand ([`leave`]), and the one whose leaving empties a claimed process -//! frees what the process holds and publishes its exit, on its own stack. +//! frees what the process holds, on its own stack. //! Nothing a thread can still run in is freed before then, and no thread waits //! for another to leave. A second publish is an assertion failure in //! `ProcessObject::publish_exit`, by design. @@ -57,9 +57,9 @@ pub fn retire_set(proc: &P, caller: Option) -> Vec { #[must_use = "the last thread out owes its process's teardown"] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Leave { - /// It is emptying a claimed process: it tears the process down, publishes - /// `code`, and stays in the process until [`torn_down`] marks it dead with - /// `mark`, so the machine's stop counts its teardown as running. + /// It is emptying a claimed process: it tears the process down, and stays + /// in the process until [`torn_down`] marks it dead with `mark`, so the + /// machine's stop counts its teardown as running. Last { code: i32, mark: i32 }, /// Some thread is still in the process, or nobody has claimed it. NotLast, diff --git a/toyos-proclife/src/tree.rs b/toyos-proclife/src/tree.rs new file mode 100644 index 00000000000..71ee6165282 --- /dev/null +++ b/toyos-proclife/src/tree.rs @@ -0,0 +1,326 @@ +//! Where a process stands under the others, and what its end takes with it. +//! +//! **Every process has one parent and any number of children, and an end takes +//! the whole subtree.** A spawn's parent is its *place*: the spawner, or a +//! process whose `self` the spawner was handed. The kernel starts init with no +//! parent, init starts everything else, and nothing reparents. +//! +//! Three questions, each asked under `PROCESS_TABLE` and each one lock hold: +//! +//! - **May a child be placed here?** [`admit_child`] at the top of a spawn, +//! before anything is built: a place being torn down takes nothing more, +//! and a child deeper than [`MAX_DEPTH`] below init is refused. It takes +//! the child's pid ([`crate::pids`]). +//! - **Who does an end take?** [`claim`] one process at a time: its claim is +//! what closes admission under it, and the same hold reads its children. +//! A child admitted before the claim and landed after it is claimed in the +//! hold that lands it ([`land_child`]), and its spawner ends it. +//! - **When is an end published?** Once its own teardown is done and every +//! child's end is published: a count admission raises, and a child's +//! publication, a refused spawn or the process's own teardown lowers +//! ([`teardown_done`], [`published`], [`refuse_child`]). The thread that +//! lowers it to zero publishes, then climbs to the parent: at most +//! [`MAX_DEPTH`] + 1 publications, child before parent. + +use alloc::vec::Vec; + +use crate::table::{Lifecycle, Processes}; +use crate::{teardown, Pid, Tid}; + +/// How far below init a process may be. Bounds the climb one teardown can +/// owe. +pub const MAX_DEPTH: u32 = 64; + +/// One process's place in the tree. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Node { + parent: Option, + depth: u32, + /// Placed under it and not yet published. + children: Vec, + /// Its own teardown, and each child admitted under it and not yet + /// published or refused. + holds: u32, +} + +impl Node { + /// A process placed under none: init at depth 0, and a kernel thread's. + pub fn root() -> Self { + Self { parent: None, depth: 0, children: Vec::new(), holds: 1 } + } + + pub fn parent(&self) -> Option { + self.parent + } + + pub fn depth(&self) -> u32 { + self.depth + } +} + +/// A spawn's first answer. +#[must_use = "an admitted spawn holds its place's publication and its pid until it lands or is refused"] +#[derive(PartialEq, Eq, Debug)] +pub enum Admit { + Yes(Admitted), + /// The place is being torn down or has gone. + Gone, + /// The child would be `depth` below init, past [`MAX_DEPTH`]. + TooDeep { depth: u32 }, + /// Every pid below [`Pid::MAX`] is issued. + NoPid, +} + +/// A spawn admitted under its place, which it keeps unpublished, and the pid +/// it took, until [`land_child`] lands it or [`refuse_child`] gives both back. +#[must_use = "an admitted spawn holds its place's publication and its pid until it lands or is refused"] +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Admitted { + /// `None` for init, which the kernel starts under no process. + place: Option, + depth: u32, + pid: Pid, +} + +impl Admitted { + /// The pid the child will have. + pub fn pid(&self) -> Pid { + self.pid + } +} + +/// The next publication a lowered count owes: `pid`, then its `parent`'s turn. +#[must_use = "a process whose count reached zero is published by whoever lowered it"] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct Publish { + pub pid: Pid, + pub parent: Option, +} + +/// The question at the top of a spawn under `place`, or of init's under +/// none, before anything is built. +pub fn admit_child(table: &mut T, place: Option) -> Admit { + let depth = match place { + None => 0, + Some(place) => { + let Some(proc) = table.get(place) else { return Admit::Gone }; + if proc.tearing_down() { + return Admit::Gone; + } + let depth = proc.node().depth + 1; + if depth > MAX_DEPTH { + return Admit::TooDeep { depth }; + } + depth + } + }; + let Some(pid) = table.pids().take() else { return Admit::NoPid }; + if let Some(place) = place { + table.get_mut(place).expect("admit_child: the place read above").node_mut().holds += 1; + } + Admit::Yes(Admitted { place, depth, pid }) +} + +/// Land the admitted child, in one hold: `insert` puts its entry in the table +/// at its pid with the node it is handed. A child whose place was claimed since +/// the admission is claimed for `code` before the hold ends, and the threads +/// answered are its, for its spawner to retire: none for any other child. +#[must_use = "a child claimed as it landed is its spawner's to retire"] +pub fn land_child( + table: &mut T, + admitted: Admitted, + code: i32, + insert: impl FnOnce(&mut T, Node) -> R, +) -> (R, Vec) { + let Admitted { place, depth, pid } = admitted; + let claimed = match place { + None => false, + Some(place) => { + let proc = table + .get_mut(place) + .expect("land_child: an admitted spawn keeps its place unpublished, so in the table"); + proc.node_mut().children.push(pid); + // The mutation this feature stages: the child lands as if its + // place were live, after the walk read its children. + proc.tearing_down() && !cfg!(feature = "mutate-place-skips-the-insert-recheck") + } + }; + let inserted = insert(table, Node { parent: place, depth, children: Vec::new(), holds: 1 }); + if !claimed { + return (inserted, Vec::new()); + } + assert!( + teardown::claim_teardown(table, pid, code), + "land_child: pid {pid}, inserted in this hold, was claimed by another", + ); + // The mutation this feature stages: the child is claimed and nobody + // retires it. + if cfg!(feature = "mutate-landed-child-retires-nothing") { + return (inserted, Vec::new()); + } + let child = table.get(pid).expect("land_child: inserted in this hold"); + (inserted, teardown::retire_set(child, None)) +} + +/// A spawn refused after its admission: its pid goes back, and its hold on +/// the place goes. +pub fn refuse_child(table: &mut T, admitted: Admitted) -> Option { + table.pids().give_back(admitted.pid); + // The mutation this feature stages: the hold stays, and the place waits + // for a child that will never be. + if cfg!(feature = "mutate-refused-spawn-keeps-the-count") { + return None; + } + lower(table, admitted.place?) +} + +/// Claim `pid` for `code` and owe its children to the walk, all in this one +/// hold. `false` for a process gone or claimed by another end, whose own walk +/// owes its children. +#[must_use = "a caller that did not win the claim must not retire anything"] +pub fn claim(table: &mut T, pid: Pid, code: i32, owed: &mut Vec) -> bool { + if !teardown::claim_teardown(table, pid, code) { + return false; + } + let proc = table.get(pid).expect("claim: the entry the claim just succeeded on"); + owed.extend_from_slice(&proc.node().children); + true +} + +/// The last one out of `pid` has torn it down: its own hold goes. +pub fn teardown_done(table: &mut T, pid: Pid) -> Option { + // The mutation this feature stages: the end is published with its own + // teardown, whatever is still unpublished below it, and no child's + // publication climbs. + if cfg!(feature = "mutate-publish-before-the-children") { + let parent = table.get(pid).expect("teardown_done: a process torn down is in the table").node().parent; + return Some(Publish { pid, parent }); + } + lower(table, pid) +} + +/// `child` is published: it leaves `parent`'s children, and its hold goes. +pub fn published(table: &mut T, parent: Pid, child: Pid) -> Option { + if cfg!(feature = "mutate-publish-before-the-children") { + if let Some(proc) = table.get_mut(parent) { + proc.node_mut().children.retain(|&c| c != child); + } + return None; + } + let proc = table + .get_mut(parent) + .expect("published: a parent is unpublished, so in the table, while a child holds it"); + proc.node_mut().children.retain(|&c| c != child); + lower(table, parent) +} + +fn lower(table: &mut T, pid: Pid) -> Option { + let node = table + .get_mut(pid) + .expect("lower: a process is in the table until it is published, and a hold keeps it unpublished") + .node_mut(); + node.holds = node.holds.checked_sub(1).expect("lower: a process lowered past its last hold"); + (node.holds == 0).then_some(Publish { pid, parent: node.parent }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::World; + use crate::Pids; + + /// A chain from init down to `MAX_DEPTH`: the last is admitted, and one + /// more below it is refused naming the depth it would have had. + #[test] + fn a_chain_is_refused_at_max_depth_plus_one_below_init() { + let mut world = World::new(); + let init = world.spawn_process(); + let mut at = init; + for depth in 1..=MAX_DEPTH { + at = world.spawn_child(at); + assert_eq!(world.get(at).unwrap().node().depth(), depth); + } + assert_eq!(admit_child(&mut world, Some(at)), Admit::TooDeep { depth: MAX_DEPTH + 1 }); + let parent = world.get(at).unwrap().node().parent().unwrap(); + let Admit::Yes(admitted) = admit_child(&mut world, Some(parent)) else { + panic!("a child at MAX_DEPTH itself was refused"); + }; + assert_eq!(refuse_child(&mut world, admitted), None); + } + + #[test] + fn a_place_being_torn_down_admits_nothing() { + let mut world = World::new(); + let init = world.spawn_process(); + let place = world.spawn_child(init); + assert!(claim(&mut world, place, 137, &mut Vec::new())); + assert_eq!(admit_child(&mut world, Some(place)), Admit::Gone); + assert_eq!(admit_child(&mut world, Some(Pid(99))), Admit::Gone); + } + + /// A child admitted before its place's claim and landed after it is + /// claimed in the hold that lands it, its spawner is answered its thread + /// to retire, and it holds the place's publication until its own. + #[test] + fn a_child_landed_under_a_place_claimed_since_its_admission_is_claimed_with_it() { + let mut world = World::new(); + let init = world.spawn_process(); + let place = world.spawn_child(init); + let Admit::Yes(admitted) = admit_child(&mut world, Some(place)) else { panic!("admitted") }; + assert!(claim(&mut world, place, 137, &mut Vec::new())); + assert_eq!(teardown_done(&mut world, place), None, "published with a child admitted under it"); + let child = admitted.pid(); + let ((), retire) = land_child(&mut world, admitted, 137, |world, node| world.insert(child, node)); + assert_eq!(retire, [world.main_tid(child)], "the child's spawner was not answered its thread to retire"); + assert_eq!(world.get(child).unwrap().teardown_code(), Some(137)); + assert_eq!(teardown_done(&mut world, child), Some(Publish { pid: child, parent: Some(place) })); + assert_eq!(published(&mut world, place, child), Some(Publish { pid: place, parent: Some(init) })); + } + + /// A published child leaves its parent's children, so a parent's walk + /// reads only what is still unpublished below it. + #[test] + fn a_published_child_leaves_its_parents_children() { + let mut world = World::new(); + let init = world.spawn_process(); + let child = world.spawn_child(init); + assert!(claim(&mut world, child, 0, &mut Vec::new())); + assert_eq!(teardown_done(&mut world, child), Some(Publish { pid: child, parent: Some(init) })); + assert_eq!(published(&mut world, init, child), None); + let mut owed = Vec::new(); + assert!(claim(&mut world, init, 137, &mut owed)); + assert_eq!(owed, [], "init's walk owes pid {child}, which was published"); + } + + /// Once every pid below `Pid::MAX` is issued a spawn is refused by name + /// and holds its place by nothing, and a refused spawn's pid is the next + /// one admitted: refused spawns spend none. + #[test] + fn a_spawn_past_the_last_pid_is_refused_and_a_refused_spawn_spends_none() { + let mut world = World::with_pids(Pids::issued_below(Pid(u32::MAX - 2))); + let init = world.spawn_process(); + let Admit::Yes(admitted) = admit_child(&mut world, Some(init)) else { panic!("the last pid was refused") }; + let last = admitted.pid(); + assert_eq!(last, Pid(u32::MAX - 1)); + assert_eq!(admit_child(&mut world, Some(init)), Admit::NoPid); + assert_eq!(refuse_child(&mut world, admitted), None); + for _ in 0..3 { + let Admit::Yes(again) = admit_child(&mut world, Some(init)) else { + panic!("a refused spawn spent pid {last}"); + }; + assert_eq!(again.pid(), last); + assert_eq!(refuse_child(&mut world, again), None); + } + let Admit::Yes(lands) = admit_child(&mut world, Some(init)) else { panic!("pid {last} was spent") }; + let ((), retire) = land_child(&mut world, lands, 137, |world, node| world.insert(last, node)); + assert_eq!(retire, []); + assert_eq!(admit_child(&mut world, Some(init)), Admit::NoPid); + assert_eq!(admit_child(&mut world, None), Admit::NoPid); + // init's own hold and its child's are all that hold it. + assert!(claim(&mut world, last, 0, &mut Vec::new())); + assert_eq!(teardown_done(&mut world, last), Some(Publish { pid: last, parent: Some(init) })); + assert!(claim(&mut world, init, 0, &mut Vec::new())); + assert_eq!(teardown_done(&mut world, init), None); + assert_eq!(published(&mut world, init, last), Some(Publish { pid: init, parent: None })); + } +} diff --git a/toyos-userbound/src/span.rs b/toyos-userbound/src/span.rs index fb4299bf3bf..0462f49a543 100644 --- a/toyos-userbound/src/span.rs +++ b/toyos-userbound/src/span.rs @@ -107,7 +107,7 @@ mod tests { ("Stat", 24, 8), ("SchedInfo", 24, 8), ("FramebufferInfo", 32, 4), - ("SpawnArgs", 112, 8), + ("SpawnArgs", 120, 8), ("NamespaceBuild", 56, 8), ("InboxSetup", 16, 8), ("ProcessStats", 128, 8), diff --git a/toyos/src/endow.rs b/toyos/src/endow.rs index 33d0d51f00c..fdbd966b076 100644 --- a/toyos/src/endow.rs +++ b/toyos/src/endow.rs @@ -22,10 +22,11 @@ use toyos_abi::syscall::{ use crate::ipc::Connection; use crate::namespace::Namespace; use crate::port::{Acceptor, Connector}; +use crate::process::Process; use crate::syscap::SysCap; use crate::{Device, OwnedHandle, RawHandle}; -pub use toyos_abi::syscall::{DEV_PREFIX, PROVIDE_PREFIX, SERVE_PREFIX, SVC_LABEL, SYSCAP_LABEL}; +pub use toyos_abi::syscall::{DEV_PREFIX, PROVIDE_PREFIX, SELF_LABEL, SERVE_PREFIX, SVC_LABEL, SYSCAP_LABEL}; /// Why a service name did not become a connection. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -143,6 +144,15 @@ pub fn namespace() -> Option<&'static Namespace> { NAMESPACE.get().as_ref() } +/// This process's handle to itself, which it hands on for a child to be +/// placed under it: `WRITE`, `DUP` and `TRANSFER`, from the kernel, under +/// [`SELF_LABEL`]. Borrowed rather than taken, as the namespace is: every +/// launch duplicates it. Panics for a process whose `self` was taken before +/// this first ran: the kernel starts every process holding it. +pub fn this_process() -> &'static Process { + THIS_PROCESS.get() +} + /// Make `ns` this process's namespace, for the one process no parent endows /// one: init, which builds the machine's ports itself and resolves its own /// files through them as every program does. Refused when this process already @@ -289,9 +299,11 @@ impl Once { static TABLE: EndowTable = EndowTable(Once::new()); static NAMESPACE: NamespaceCell = NamespaceCell(Once::new()); +static THIS_PROCESS: ProcessCell = ProcessCell(Once::new()); struct EndowTable(Once); struct NamespaceCell(Once>); +struct ProcessCell(Once); impl EndowTable { fn get(&'static self) -> &'static Endowments { @@ -305,6 +317,14 @@ impl NamespaceCell { } } +impl ProcessCell { + fn get(&'static self) -> &'static Process { + self.0.get_or_init(|| { + Endowments::get().take::(SELF_LABEL).expect("this process's `self` was taken by another") + }) + } +} + /// One `SYS_ENDOWMENTS` call: a count, that many entries, then the label blob. fn read_table() -> Endowments { const ENTRY: usize = 16; diff --git a/toyos/src/launch.rs b/toyos/src/launch.rs index bbc4a7b06e9..1a1b3204408 100644 --- a/toyos/src/launch.rs +++ b/toyos/src/launch.rs @@ -13,6 +13,11 @@ //! same bound `SYS_SPAWN` has. What it *adds* is that the child's namespace is //! its own manifest row rather than a narrowing of its parent's. //! +//! **A launch names its parent**, by one of two words: a place, which is a +//! handle to a process carrying `WRITE` — the caller's own `self`, duplicated — +//! that init spawns the program under, so the caller's end takes it down; or +//! init, the one way for a program to outlive whoever started it. +//! //! The wire is a single frame plus one handle batch, and this module is both //! halves of it — std's `Command` encodes and `init` decodes. @@ -34,8 +39,12 @@ pub const MSG_NOT_DECLARED: u32 = 3; /// endowment. The reason is in init's log, not in this frame — a caller can do /// nothing differently about any of them. pub const MSG_REFUSED: u32 = 4; +/// The place it named is a process whose end has begun: nothing started, and +/// nothing will under it. +pub const MSG_GONE: u32 = 5; -/// Connectors one launch may carry from its caller. +/// Handles beyond stdio one launch may carry from its caller: the connectors +/// it transfers, and the place it names. /// /// Policy on the primitive, refused by name. Five and not more because the /// batch also carries the three stdio handles and @@ -48,7 +57,22 @@ pub const MAX_LAUNCH_EXTRAS: usize = 5; /// caller that wants anything else in a child's table spawns it directly. pub const MAX_LAUNCH_SLOTS: usize = 3; -const HEADER: usize = 32; +const HEADER: usize = 36; + +/// The header's last word for a launch that names a place. +const PARENT_PLACE: u32 = 1; +/// The header's last word for a launch that asks init to be the parent. +const PARENT_INIT: u32 = 2; + +/// Whom a launch asks init to place the program under. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Parent { + /// The process a handle carrying `WRITE` names, moved as the batch's last + /// handle. + Place(H), + /// init itself. + Init, +} /// What a caller asks init to start. /// @@ -79,6 +103,9 @@ pub struct Launch<'a> { /// first — which is the same rule `SpawnArgs`'s two vectors state, with the /// duplication moved to the caller because a launch has only the one verb. pub slots: &'a [(u32, RawHandle)], + /// Whom the program is placed under. A place is **moved** with the batch, + /// and counts against [`MAX_LAUNCH_EXTRAS`]. + pub parent: Parent, } /// Why a request would not go on the wire. @@ -91,15 +118,17 @@ pub enum EncodeError { } impl Launch<'_> { - /// The handles this launch moves, slots first. + /// The handles this launch moves: slots, then extras, then the place. pub fn handles(&self) -> ([RawHandle; MAX_LAUNCH_EXTRAS + MAX_LAUNCH_SLOTS], usize) { let mut out = [RawHandle(0); MAX_LAUNCH_EXTRAS + MAX_LAUNCH_SLOTS]; let mut n = 0; - for (_, handle) in self.slots { - out[n] = *handle; - n += 1; - } - for (_, handle) in self.extras { + let place = match self.parent { + Parent::Place(place) => Some(place), + Parent::Init => None, + }; + let slots = self.slots.iter().map(|(_, h)| h); + let extras = self.extras.iter().map(|(_, h)| h); + for handle in slots.chain(extras).chain(&place) { out[n] = *handle; n += 1; } @@ -108,7 +137,11 @@ impl Launch<'_> { /// Write the request blob into `buf`, answering its length. pub fn encode(&self, buf: &mut [u8]) -> Result { - if self.extras.len() > MAX_LAUNCH_EXTRAS || self.slots.len() > MAX_LAUNCH_SLOTS { + let (place, word) = match self.parent { + Parent::Place(_) => (1, PARENT_PLACE), + Parent::Init => (0, PARENT_INIT), + }; + if self.extras.len() + place > MAX_LAUNCH_EXTRAS || self.slots.len() > MAX_LAUNCH_SLOTS { return Err(EncodeError::TooMany); } let names_len: usize = self.extras.iter().map(|(n, _)| n.len() + 1).sum(); @@ -132,6 +165,7 @@ impl Launch<'_> { self.env.len() as u32, self.cwd.len() as u32, names_len as u32, + word, ]; for (i, v) in lens.iter().enumerate() { buf[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes()); @@ -169,6 +203,7 @@ pub struct Request<'a> { pub extra_count: usize, slots: &'a [u8], names: &'a [u8], + parent: u32, } impl<'a> Request<'a> { @@ -183,7 +218,8 @@ impl<'a> Request<'a> { let (slot_count, extra_count) = (field(0), field(1)); let (slots_len, program_len, argv_len, env_len, cwd_len, names_len) = (field(2), field(3), field(4), field(5), field(6), field(7)); - if extra_count > MAX_LAUNCH_EXTRAS + let parent = field(8) as u32; + if extra_count + usize::from(parent == PARENT_PLACE) > MAX_LAUNCH_EXTRAS || slot_count > MAX_LAUNCH_SLOTS || slots_len != slot_count * 4 { @@ -214,7 +250,22 @@ impl<'a> Request<'a> { if names.iter().filter(|&&b| b == 0).count() != extra_count { return None; } - Some(Self { program, argv, env, cwd, extra_count, slots, names }) + Some(Self { program, argv, env, cwd, extra_count, slots, names, parent }) + } + + /// Whom the request names as the parent: `None` for neither word, which + /// is a launch init refuses. + pub fn parent(&self) -> Option> { + match self.parent { + PARENT_PLACE => Some(Parent::Place(())), + PARENT_INIT => Some(Parent::Init), + _ => None, + } + } + + /// The handles the batch carries: the slots, the extras, then the place. + pub fn handle_count(&self) -> usize { + self.slot_count() + self.extra_count + usize::from(self.parent == PARENT_PLACE) } /// The child slots the first handles of the batch are for, in order. @@ -259,6 +310,8 @@ pub enum Outcome<'a> { NotDeclared { home: &'a str }, /// Declared, and it did not start. Refused, + /// The place it named has begun to end. + Gone, } /// Send one launch and read its answer, a `HOME` in `answer`. @@ -295,6 +348,37 @@ pub fn launch<'a>( Ok(Outcome::NotDeclared { home }) } MSG_REFUSED => Ok(Outcome::Refused), + MSG_GONE => Ok(Outcome::Gone), _ => Err(LaunchError::Sent(IpcError::Malformed)), } } + +#[cfg(test)] +mod tests { + use super::*; + + /// The header's last word names the parent: a place, init, or — any other + /// word — neither, which init refuses. + #[test] + fn a_request_names_its_parent_by_one_word() { + let mut buf = [0u8; 256]; + let request = |parent| Launch { + program: "/system/bin/cat", + argv: b"", + env: b"", + cwd: "/", + extras: &[], + slots: &[], + parent, + }; + for (parent, named) in [(Parent::Place(RawHandle(9)), Parent::Place(())), (Parent::Init, Parent::Init)] { + let len = request(parent).encode(&mut buf).expect("encode a launch"); + assert_eq!(Request::decode(&buf[..len]).expect("decode it").parent(), Some(named)); + } + let len = request(Parent::Init).encode(&mut buf).expect("encode a launch"); + for word in [0, 3, u32::MAX] { + buf[HEADER - 4..HEADER].copy_from_slice(&word.to_le_bytes()); + assert_eq!(Request::decode(&buf[..len]).expect("decode it").parent(), None, "word {word}"); + } + } +} diff --git a/userland/init/src/main.rs b/userland/init/src/main.rs index 7fa2469fc46..3e55817b735 100644 --- a/userland/init/src/main.rs +++ b/userland/init/src/main.rs @@ -46,6 +46,11 @@ //! before it runs, and everything else the session user's home, made at boot. //! A launch of a program no row names is answered with the session's, which //! the caller's direct spawn carries in place of its own. +//! +//! **A launched program is spawned under the place its request names** — a +//! copy of the caller's `self` — so the caller's end takes it down; a request +//! that asks for init is the one way to outlive the caller, and one naming +//! neither is refused. use std::cell::{Cell, RefCell}; use std::collections::{BTreeMap, VecDeque}; @@ -61,7 +66,7 @@ use toyos_manifest::{Manifest, Program}; use toyos::endow::Endowments; use toyos::fs::CAPABILITY_PREFIX; use toyos::ipc::{self, Connection, RxStep}; -use toyos::launch::{self, Request}; +use toyos::launch::{self, Parent, Request}; use toyos::namespace::{self, Namespace}; use toyos::poller::{Poller, READABLE}; use toyos::port::{self, Acceptor, Connector}; @@ -1426,22 +1431,22 @@ impl Init<'_> { // will actually be paired with a handle. A frame whose two counts // disagree would otherwise leave the unpaired handles behind. let names: Vec<&str> = request.extra_names().collect(); - if received != request.slot_count() + request.extra_count - || names.len() != request.extra_count - { + if received != request.handle_count() || names.len() != request.extra_count { say!( "init: launcher: a frame promising {} handles under {} names carried {received}", - request.slot_count() + request.extra_count, + request.handle_count(), names.len(), ); return; } // Past every refusal that does not know which handle is which, so ownership - // can be split. Both halves still release on every path below. + // can be split. Every part still releases on every path below. let all = held.take(); - let (slot_handles, extra_handles) = all.split_at(request.slot_count()); + let (slot_handles, rest) = all.split_at(request.slot_count()); + let (extra_handles, place_handle) = rest.split_at(request.extra_count); let slots = Moved(slot_handles.to_vec()); + let place = Moved(place_handle.to_vec()); // Owned, so they close when this call returns: `SYS_NAMESPACE_BUILD` copies // a connector into the namespace and leaves the caller's handle, and init's // copy of a client's connector has no life beyond this launch. @@ -1463,6 +1468,19 @@ impl Init<'_> { return; } + // **A launch names its parent**: the place it carries, spawned under, or + // init. One that names neither is refused rather than given to init, + // which would let it outlive a caller that never asked it to. + let under = match request.parent() { + Some(Parent::Place(())) => Some(place.0[0]), + Some(Parent::Init) => None, + None => { + say!("init: launcher: refused a launch that names no parent"); + let _ = conn.try_signal(launch::MSG_REFUSED); + return; + } + }; + // **The caller's path, not the row's, and `argv[0]` is why.** `declared` // has already established that the two name one binary, so this grants // nothing extra — and `/system/bin/echo` spawned as `/system/bin/toybox` is a toybox that @@ -1481,6 +1499,9 @@ impl Init<'_> { } } command.current_dir(request.cwd); + if let Some(place) = under { + command.under(place.0); + } for arg in request.argv.split(|&b| b == 0).skip(1).filter(|a| !a.is_empty()) { if let Ok(arg) = std::str::from_utf8(arg) { command.arg(arg); @@ -1574,6 +1595,14 @@ impl Init<'_> { Err(_) => toyos_abi::syscall::close(handle), } } + // std's word for the kernel's `Gone` for the place, and nothing else + // here answers it: the command is prepared, so its spawn calls no + // file server, and every refusal `start` makes before the spawn is + // `Other`. + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe && under.is_some() => { + say!("init: launcher: {} was not started: the place it names is ending", program.name); + let _ = conn.try_signal(launch::MSG_GONE); + } Err(e) => { say!("init: launcher: cannot start {}: {e}", program.name); let _ = conn.try_signal(launch::MSG_REFUSED); @@ -1584,8 +1613,8 @@ impl Init<'_> { /// Handles a launch moved into init, released on every path out of it. /// -/// A `Drop` and not a close at each `return`: there are seven ways out of -/// `serve_launch` and a client picks which one by what it sends. +/// A `Drop` and not a close at each `return`: a client picks which way out of +/// `serve_launch` it takes by what it sends. struct Moved(Vec); impl Moved { diff --git a/userland/shell/src/main.rs b/userland/shell/src/main.rs index 4e88747e6dd..74aeebd14b0 100644 --- a/userland/shell/src/main.rs +++ b/userland/shell/src/main.rs @@ -629,6 +629,7 @@ fn execute_simple(cmd: &SimpleCommand, piped_stdin: Option { print_help(); set_status_code(0); return true; } + "detach" => return detach(cmd), _ => {} } @@ -706,6 +707,33 @@ fn provided() -> &'static [(&'static str, Connector)] { }) } +/// `detach [args...]`: start it with init as its parent — the one way +/// for a program to outlive this shell — and do not wait for it. Nothing this +/// shell was provided goes with it: that belongs to the session it outlives. +fn detach(cmd: &SimpleCommand) -> bool { + let Some(program) = cmd.args.get(1) else { + println!("detach: which program?"); + set_status_code(2); + return false; + }; + let cwd = + env::current_dir().map(|p| p.display().to_string()).unwrap_or_else(|_| String::from("/")); + let mut command = Command::new(toyos_manifest::package::launch_path(program, &cwd)); + command.args(&cmd.args[2..]).under_init(); + apply_redirect(&mut command, cmd); + match command.spawn() { + Ok(_) => { + set_status_code(0); + true + } + Err(e) => { + println!("detach: {program}: {e}"); + set_status_code(126); + false + } + } +} + fn build_command(cmd: &SimpleCommand) -> Option { if cmd.args.is_empty() { return None; } // The launcher refuses a path it would have to normalize, and the shell is @@ -785,7 +813,8 @@ fn set_status_code(code: i32) { } fn print_help() { - println!("Builtins: cd, clear, exit, export, help"); + println!("Builtins: cd, clear, detach, exit, export, help"); + println!("detach : start it under init, so it outlives this shell"); println!("Operators: | (pipe), && (and), || (or), ; (sequence)"); println!("Redirects: > (truncate), >> (append)"); println!("Variables: $VAR, ${{VAR}}, $? (exit status)"); diff --git a/userland/toybox/src/ps.rs b/userland/toybox/src/ps.rs index 754d678b557..6005bd1a08b 100644 --- a/userland/toybox/src/ps.rs +++ b/userland/toybox/src/ps.rs @@ -111,8 +111,6 @@ pub fn main(_args: Vec) { }; let window_ns = uptime_ns.saturating_sub(first_uptime); - // No PPID column: a process has no parent. What started it gave it what it - // holds and kept a handle, and neither of those is a number the table has. println!("{:>5} {:>3} {:>2} {:>8} {:>5} {:>5} {}", "PID", "TID", "S", "CPU", "%CPU", "MEM", "NAME");