diff --git a/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md b/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md index 77313587a3e..ab9fa611618 100644 --- a/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md +++ b/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md @@ -19,8 +19,7 @@ On top of retention, in dependency order: `Process` handle narrowed to `Rights::READ`. `SYS_PROCESS_STATS` takes a `Process` handle and a handle is the whole of the right, so nothing hands a diagnostic tool a way to sample a daemon: `/system/bin/init` holds the only `Process` - handles for what `[boot] start` names and the only `SysCap` carrying - `Rights::MANAGE`, which is what `SYS_PROCESS_OPEN` takes. So "where is + handles for what `[boot] start` names. So "where is soundd's / the compositor's / netd's time going?" is unanswerable from a shell, and `audio_idle_suspend` name-matches `SYS_SYSINFO` entries out of a byte buffer to sample a running daemon twice. Nothing in the kernel has to 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 f6a9f59809d..cae76ce5baa 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 @@ -37,20 +37,6 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md ## Stages -0. **`SYS_PROCESS_OPEN` goes**. Deleted: `sys_process_open` and every - name only it reaches — `process_open`, `SysCap::open_process`, `MANAGE` in - init's `SysCap`, the `reopenable` column, `process::process_object`, - `reopen_selftest` and `sched::kthread::open_selftest` with their actuator, - guest test, judge and rows — and - `process_lifecycle`'s pid-open arm, its only caller; 110 enters - `retired_syscalls!`. `issues/kernel/the-capability-end-state-is-twelve-answers.md` - and `issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md`, which - argue from it, change in the same landing. *Exit*: a call of 110 answers - `NotSupported` and the log names it retired; `git grep` finds no deleted - name outside `toyos-symbols/tests/fixtures/input-test.bin`, a frozen binary - whose symbols are test data. Negative control: the stage reverted whole, - where `sys_process_open` answers 110. Oracle: rustc's name resolution, - which fails the build of any caller left. 1. **An end is an event**. `read_watch` and `has_data` answer for a `Process`, whose watch becomes an `Arc` as an `Acceptor`'s is, and `close_ends_polls` answers `false` for one; init's waiter threads go. diff --git a/issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md b/issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md new file mode 100644 index 00000000000..3689e076bcc --- /dev/null +++ b/issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md @@ -0,0 +1,29 @@ +--- +status: open +kind: defect +opened: 2026-10-02 +--- + +# A kill on a spawn's handle before its child lands claims nothing + +`PendingHandles::commit` (`kernel/src/loader/start.rs`) puts the spawner's +handle to its child in the spawner's table before `loader::spawn` lands the +child in the process table, and a handle's number is its table's own +arithmetic, so another thread of the spawner can name the handle in between. +A `SYS_PROCESS_KILL` on it there claims nothing: +`toyos_proclife::teardown::claim_teardown` answers `false` for a pid not in +the table, and `process::kill_process` answers `Ok`. The child then lands and +runs. `kill_process`'s "`Ok` for an already-gone process: the caller asked for +it to be dead and it is" does not cover a process not yet there. + +Read off the code; no test reaches it, and no caller can order a kill inside +a spawn. A close, a dup, a transfer, a wait and a stats read of the handle in +the same window are sound. + +**Owner**: the spawn's commit, `kernel/src/loader/start.rs`. + +*Exit*: a kill on a handle whose process has not landed ends that process — +the landing claims it, as it claims a child under a claimed place — or the +handle resolves only once the child has landed; `toyos-proclife` scripts the +kill as a step between the commit and the landing, and no schedule leaves the +child running. 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 deleted file mode 100644 index f05cf31a534..00000000000 --- a/issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -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/issues/kernel/the-capability-end-state-is-twelve-answers.md b/issues/kernel/the-capability-end-state-is-twelve-answers.md index c1a385a9556..0d4f4b9cb73 100644 --- a/issues/kernel/the-capability-end-state-is-twelve-answers.md +++ b/issues/kernel/the-capability-end-state-is-twelve-answers.md @@ -30,16 +30,6 @@ committed, four are open" was this line's own summary and stopped being true when those four rulings landed; "The ruling set" below is a record of decisions taken, not a queue waiting on anybody. -Four of the rulings are enforced in code, each at a site that demands a right -where none was demanded before: `SYS_SHUTDOWN` takes a `SysCap` carrying -`Rights::POWER` (`kernel/src/syscall/machine.rs:46-48`); `SYS_SYSINFO`'s -roster takes `Rights::ROSTER`, demanded only once the buffer has room for an -entry (`kernel/src/syscall/machine.rs:84`, `:94`), spelled `roster` in -`toyos-manifest/src/lib.rs:80`; and `SYS_PROCESS_OPEN` takes `Rights::MANAGE` -(`kernel/src/syscall/proc.rs:89-91`), which is what makes question 3's -"a pid is not authority" checkable — the ABI says so at the field -(`toyos-abi/src/syscall.rs:1882-1884`). - **Every `4a98107f^:kernel/src/arch/syscall.rs` citation below points into history**: that one-file syscall layer is what `4a98107f` split, and the syscalls are `kernel/src/syscall/` now, twelve files with `dispatch.rs` @@ -100,20 +90,13 @@ closed. ## 3. Are PIDs and TIDs identity-only, or can naming one confer authority? — COMMITTED -Identity-only, with one named exception that is itself gated. Every arm taking a -pid: `SYS_GETPID` answers the caller's own -(`4a98107f^:kernel/src/arch/syscall.rs:490`), and `SYS_PROCESS_OPEN` turns a pid into a -`Process` handle only when the caller also presents a `SysCap` carrying -`Rights::MANAGE` (`:1602`), which the kernel mints once, for `/system/bin/init` -(`kernel/src/loader/mod.rs:938`). `ProcessStats.pid` says so at the field: "Not -authority — nothing takes a pid but `SYS_PROCESS_OPEN`, which takes a `SysCap` -beside it" (`toyos-abi/src/syscall.rs:1803`). Tids are process-local names: +Identity-only. No arm takes a pid, and `SYS_GETPID` answers the caller's own +(`4a98107f^:kernel/src/arch/syscall.rs:490`). `ProcessStats.pid` says so at the +field: "Not authority — no syscall takes a pid" (`toyos-abi/src/syscall.rs`'s +`ProcessStats`). Tids are process-local names: `SYS_THREAD_JOIN` resolves through `thread_sched(caller, tid)` and `collect_thread_zombie(table, tid, parent_pid)`, both keyed on the caller's own pid (`4a98107f^:kernel/src/arch/syscall.rs:2393`, `kernel/src/process.rs:1412`, `:848`). -Four pid-addressed syscalls were deleted and their numbers retired rather than -reused — 26 `SYS_WAITPID`, 33 `SYS_FIND_PID`, 37 `SYS_GRANT_SHARED`, 65 -`SYS_KILL` (`4a98107f^:kernel/src/arch/syscall.rs:63`). ## 4. Can a process enumerate objects it lacks authority over? — RULED 2026-08-20, IMPLEMENTED 2026-08-22 @@ -291,11 +274,10 @@ provably no longer holds it (`kernel/src/object/ops.rs:47`). Every device-driving syscall then presents that handle and the kernel checks the *class*, not merely the type: "a process holding the NIC has no more business setting the resolution than one holding nothing" -(`4a98107f^:kernel/src/arch/syscall.rs:895`). `SYS_RT_ENTER` and `SYS_PROCESS_OPEN` are -the same shape on `Rights::RT` and `Rights::MANAGE` (`:1655`, `:1602`), narrowed -per program by `toyos_manifest::syscap_rights` -(`toyos-manifest/src/lib.rs:73`) — `system.toml` grants exactly two, `logread` -to `logd` and `rt` to `soundd`. +(`4a98107f^:kernel/src/arch/syscall.rs:895`). `SYS_RT_ENTER` is the same shape +on `Rights::RT` (`:1655`), narrowed per program by +`toyos_manifest::syscap_rights` (`toyos-manifest/src/lib.rs:73`) — `system.toml` +grants exactly two, `logread` to `logd` and `rt` to `soundd`. One inconsistency, known and unobservable: three arms demand three different rights on the same claim handle — `Rights::WRITE` diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index ea0305d1cd3..ac770b2b669 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -141,9 +141,6 @@ actuators! { /// Run the revoked-backing controls after mount. revoked_backing_selftest = "revoked-backing-selftest"; - /// Reopen init by pid once it is spawned, and open every kernel thread's pid, the way `SYS_PROCESS_OPEN` does. - process_reopen_selftest = "process-reopen-selftest"; - /// Have Ctrl+Alt+D's report painter go fatal holding the panel's latch: a /// fatal path meeting a painter that will never let go. panel_painter_stalls = "panel-painter-stalls"; diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 722a0d2015b..df9809060e7 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -28,8 +28,8 @@ use crate::object::{ops, HandleTable, KObjectRef}; use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::{PAGE_2M, PAGE_BYTES}; use crate::process::{ - Admission, ElfInfo, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Parent, Pid, - ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserStack, + Admission, ElfInfo, Endowments, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Parent, + Pid, ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserStack, PROCESS_TABLE, }; use crate::sync::Lock; @@ -335,8 +335,10 @@ fn rela_dyn_from_sections( } } -/// Load a program and place its main thread under `parent`, answering the object -/// a handle to the new process names. +/// Load a program and place its main thread under `parent`, answering its pid +/// and what `commit` left its caller holding of it. `commit` builds the +/// child's handle table around the child's handle to itself, once nothing is +/// left to refuse. /// /// `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 @@ -346,14 +348,14 @@ fn rela_dyn_from_sections( /// `Refusal`, not `-> !`, is the error type: every failure below owns a /// partly built process (address space, stack, kernel stack), and nothing /// unwinds, so the error must travel out as a value rather than strand it. -pub fn spawn( +pub fn spawn( argv: &[&str], - pending: PendingHandles, + commit: impl FnOnce(crate::object::HandleEntry) -> Result<(HandleTable, Endowments, H), crate::object::Refusal>, cwd: String, env: Vec, image: Option>, parent: Parent, -) -> Result, crate::object::Refusal> { +) -> Result<(Pid, H), 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()); @@ -366,7 +368,7 @@ pub fn spawn( let backing: Arc = match image { Some(image) => image, None => { - // Scoped, not held across the match: dropping `pending` on any `return` here takes the VFS lock. + // Scoped, not held across the match: dropping `commit` on any `return` here takes the VFS lock. let opened = vfs::lock().open_backing(path); match opened { Ok(b) => b, @@ -590,7 +592,7 @@ pub fn spawn( let object = crate::object::process::ProcessObject::new(pid); // The point of no return: every failure above answers the caller with its // table untouched. - let (handles, endowments) = pending.commit(KObjectRef::Process(Arc::clone(&object)))?; + let (handles, endowments, held) = commit(start::own_handle(&object))?; let proc_data = Arc::new(Lock::new(ProcessData { handles, cwd, @@ -671,13 +673,16 @@ pub fn spawn( scheduler::post_retire(sched); } + #[cfg(feature = "test-actuators")] + crate::process::debug_hold_marked_spawn(parent, &object); + 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)", path, pid, tid, dst.0, base, entry, child_pt.lock().root().phys(), sym_bytes / 1024, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000, (t_deps - t2) / 1_000_000, (t_tls - t_deps) / 1_000_000, (t3 - t0) / 1_000_000); - Ok(object) + Ok((pid, held)) } /// The libraries an executable's `DT_NEEDED` entries name, and the paths they were found at. @@ -876,7 +881,6 @@ pub fn spawn_init() -> Pid { .union(Rights::TRANSFER) .union(Rights::DEVICE) .union(Rights::RT) - .union(Rights::MANAGE) .union(Rights::LOG) .union(Rights::WAIT) .union(Rights::POWER) @@ -886,18 +890,20 @@ 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 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, - }], - labels: label.as_bytes().to_vec(), + let mut entries = alloc::vec![toyos_abi::syscall::EndowEntry { + label_off: 0, + label_len: label.len() as u32, + handle: cap_handle, + _pad: 0, + }]; + let mut labels = label.as_bytes().to_vec(); + // Built by the kernel and owing nobody anything: no table but init's own holds it. + let commit = |own| { + start::endow_self(&mut handles, &mut entries, &mut labels, own); + Ok((handles, Endowments::new(entries, labels), ())) }; - match spawn(&[INIT_PATH], pending, String::from("/"), Vec::new(), None, Parent::Root) { - Ok(object) => object.pid(), + match spawn(&[INIT_PATH], commit, String::from("/"), Vec::new(), None, Parent::Root) { + Ok((pid, ())) => 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 33463f16302..5d42b4c7ae8 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -1,10 +1,13 @@ -//! Loads a built process onto a CPU and builds the handle table it starts -//! with. The frame a new stack starts from and the trampolines it returns into -//! are the architecture's (`arch::entry`). +//! Loads a built process onto a CPU, builds the handle table it starts with +//! and puts its spawner's handle to it in the spawner's table. The frame a new +//! stack starts from and the trampolines it returns into are the +//! architecture's (`arch::entry`). +use alloc::sync::Arc; use alloc::vec::Vec; -use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal}; +use crate::object::process::ProcessObject; +use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal}; use crate::process::{ process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE, }; @@ -42,26 +45,19 @@ pub(crate) fn make_name(path: &str) -> [u8; crate::process::THREAD_NAME_LEN] { name } -/// A child's table, and the endowments that have not left the parent yet. -// 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 { 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 }, +/// A caller's request for a child's table: `endow` has not left the caller's table yet. +// The move must be last: an earlier move leaves a failed spawn's caller holding handles that name nothing. +pub struct PendingHandles { + 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. - /// `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), - }; + /// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged. + /// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer. + pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> { + let Self { mut table, endow, mut labels } = self; let data_arc = process_data(); let mut data = data_arc.lock(); @@ -94,7 +90,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() + 1) { + if !table.has_room(moving.len() + 1) || !data.handles.has_room(1) { return Err(SyscallError::ResourceExhausted.into()); } @@ -109,17 +105,25 @@ impl PendingHandles { .expect("a child table with verified room refused an endowment"); entries.push(entry); } + // Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last. + let held = ops::install(&mut data.handles, own.object().clone()) + .expect("a caller's table with verified room refused its child"); drop(data); endow_self(&mut table, &mut entries, &mut labels, own); - Ok((table, Endowments::new(entries, labels))) + Ok((table, Endowments::new(entries, labels), held)) } } -/// 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) { +/// A new process's handle to itself, the first its object has: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on. +pub(super) fn own_handle(object: &Arc) -> HandleEntry { let rights = Rights::WRITE.union(Rights::DUP).union(Rights::TRANSFER); + HandleEntry::new(KObjectRef::Process(Arc::clone(object)), rights) +} + +/// Install `own` in its own table under [`SELF_LABEL`]. Its caller verified the room. +pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec, labels: &mut Vec, own: HandleEntry) { let handle = table - .install(HandleEntry::new(own, rights)) + .install(own) .expect("a child table with verified room refused its own handle"); entries.push(EndowEntry { label_off: labels.len() as u32, @@ -200,5 +204,5 @@ pub fn build_child_handles( let mut raw = alloc::vec![0u8; endow.len()]; endow.read_at(0, &mut raw); - Ok(PendingHandles::Moving { table: handles, endow: raw, labels: labels.to_vec() }) + Ok(PendingHandles { table: handles, endow: raw, labels: labels.to_vec() }) } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 9369fc7b1a7..755b6754601 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -463,12 +463,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { let pid = process::spawn_init(); log!("spawned {} pid={pid}", process::INIT_PATH); - // Here and not beside the other controls: it needs a process the table answers for. - #[cfg(feature = "boot-actuators")] - if actuator::process_reopen_selftest() { - object::process::reopen_selftest(pid); - } - // The proof the boot up to here needed no disk: ROOT and init's image both // came out of memory. log!("{} {}", rootfs::INIT_WITHOUT_A_DISK, block::census::commands_issued()); @@ -556,12 +550,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // Last thing before enter_idle_loop: nothing can run before it, and a klogd spawned earlier would idle through phases 5-7 with no drainer. log::console::start(); - // Here: the last kernel thread is spawned. - #[cfg(feature = "boot-actuators")] - if actuator::process_reopen_selftest() { - sched::kthread::open_selftest(); - } - smp::set_ready(); // After the release, because a shootdown waits on CPUs that are not diff --git a/kernel/src/object/handle.rs b/kernel/src/object/handle.rs index 6cb035aeb0f..e9509cd4797 100644 --- a/kernel/src/object/handle.rs +++ b/kernel/src/object/handle.rs @@ -102,7 +102,7 @@ pub struct HandleEntry { } impl HandleEntry { - /// The only constructor; resurrecting an already-retired object is a kernel bug — which a `reopenable` row never is, since something outside every table still answers for it. + /// The only constructor; resurrecting an already-retired object is a kernel bug. pub fn new(object: KObjectRef, rights: Rights) -> Self { let core = object.core(); assert!( @@ -135,18 +135,13 @@ impl Drop for HandleEntry { fn drop(&mut self) { let core = self.object.core(); if core.handle_count.fetch_sub(1, Ordering::AcqRel) == 1 { - // A `reopenable` object is not retired by its last handle going: the process - // table still answers for it, and `SYS_PROCESS_OPEN` turns a pid — untrusted - // input — back into a handle, which may not assert. - if !self.object.reopenable() { - let first = !core.retired.swap(true, Ordering::AcqRel); - assert!( - first, - "handle_count resurrected after zero on {} (koid {})", - self.object.kind(), - core.koid().raw(), - ); - } + let first = !core.retired.swap(true, Ordering::AcqRel); + assert!( + first, + "handle_count resurrected after zero on {} (koid {})", + self.object.kind(), + core.koid().raw(), + ); // Deferred, not run inline: a release hook must never run under this lock. if self.object.defers_release() { super::enqueue_zero_handles(self.object.clone()); diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index 92dcb65bc27..e530fe80c0d 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -78,8 +78,7 @@ pub struct ObjectCore { koid: Koid, /// Table slots, in-flight transfers and spawn endowments — never the `Arc` strong count. handle_count: AtomicU32, - /// A `sealed` row only: set once when its last handle goes, and a second arrival - /// is a kernel bug caught by `HandleEntry`'s drop assert. Never set on a `reopenable` row. + /// Set once; a second arrival is a kernel bug caught by `HandleEntry`'s drop assert. retired: AtomicBool, /// This type's census counter; decremented by `ObjectCore`'s own drop. live: &'static AtomicU64, @@ -129,10 +128,9 @@ pub trait KObjectVariant: ZeroHandles + Send + Sync + Sized + 'static { } /// Declares the closed set of object types; each row says whether its last -/// handle is deferred ([`ZeroHandles`]) or immediate, and whether losing that -/// handle is the object's last name (`sealed`) or not (`reopenable`). +/// handle is deferred ([`ZeroHandles`]) or immediate. macro_rules! kobject { - ($($kind:ident $naming:ident $variant:ident => $ty:ty),+ $(,)?) => { + ($($kind:ident $variant:ident => $ty:ty),+ $(,)?) => { /// Every kind of thing a handle can name; matched exhaustively with no /// wildcard arm, so a new row is a compile error at every dispatch site. #[derive(Clone)] @@ -166,17 +164,9 @@ macro_rules! kobject { $(Self::$variant(_) => kobject!(@defers $kind),)+ } } - - /// Whether something outside every handle table answers for this object. - fn reopenable(&self) -> bool { - match self { - $(Self::$variant(_) => kobject!(@reopen $naming),)+ - } - } } $( - kobject!(@pair $kind $naming); kobject!(@empty_hook $kind $ty); impl KObjectVariant for $ty { @@ -224,19 +214,6 @@ macro_rules! kobject { (@defers deferred) => { true }; (@defers immediate) => { false }; - (@reopen sealed) => { false }; - (@reopen reopenable) => { true }; - - (@pair deferred sealed) => {}; - (@pair immediate sealed) => {}; - (@pair immediate reopenable) => {}; - (@pair deferred reopenable) => { - compile_error!( - "a `deferred reopenable` row would run its zero-handle hook once per \ - zero-crossing, and `ZeroHandles::on_zero_handles` runs exactly once" - ); - }; - (@empty_hook deferred $ty:ty) => {}; (@empty_hook immediate $ty:ty) => { impl ZeroHandles for $ty { @@ -246,26 +223,26 @@ macro_rules! kobject { } kobject! { - deferred sealed PipeRead => pipe::PipeReadEnd, - deferred sealed PipeWrite => pipe::PipeWriteEnd, - deferred sealed Connection => service::ConnectionEnd, - deferred sealed Device => device::DeviceClaim, - deferred sealed Acceptor => port::Acceptor, + deferred PipeRead => pipe::PipeReadEnd, + deferred PipeWrite => pipe::PipeWriteEnd, + deferred Connection => service::ConnectionEnd, + deferred Device => device::DeviceClaim, + deferred Acceptor => port::Acceptor, // Kind name is the ABI's; `CENSUS_KIND` asserts the two lists agree. - deferred sealed Inbox => inbox::InboxObject, + deferred Inbox => inbox::InboxObject, // The flush that makes releasing it safe waits for every CPU, so it runs from the queue, never inline. - deferred sealed SharedMem => shm::SharedMemObject, + deferred SharedMem => shm::SharedMemObject, // A service with no clients right now is not a service that has stopped. - immediate sealed Connector => port::Connector, + immediate Connector => port::Connector, // Immutable once built: its `Arc`s go with the last reference and nothing observes it. - immediate sealed Namespace => namespace::Namespace, + immediate Namespace => namespace::Namespace, // A file's flush and cache reference ride the last `Arc`; `read`/`write` on a file never park. - immediate sealed File => file::FileObject, - immediate sealed Console => device::ConsoleObject, + immediate File => file::FileObject, + immediate Console => device::ConsoleObject, // The authority is the rights on the handle; a handle going away *is* the whole event. - immediate sealed SysCap => syscap::SysCap, + immediate SysCap => syscap::SysCap, // The last handle's loss is the loss of the *ability to wait*, not proof the process should stop. - immediate reopenable Process => process::ProcessObject, + immediate Process => process::ProcessObject, } /// Objects whose last handle has gone, waiting for release with nothing held; diff --git a/kernel/src/object/process.rs b/kernel/src/object/process.rs index d8438b6ca7d..3b651631146 100644 --- a/kernel/src/object/process.rs +++ b/kernel/src/object/process.rs @@ -84,43 +84,3 @@ impl ProcessObject { self.watch.post(); } } - -/// Control for the `reopenable` row: a process the table still answers for takes -/// a fresh handle after its last one has gone. `sys_process_open`'s own two steps -/// with the `SysCap` demand left off; the second install is where it used to assert. -#[cfg(feature = "boot-actuators")] -pub(crate) fn reopen_selftest(pid: Pid) { - use super::handle::HandleTable; - use super::{ops, KObjectRef}; - use toyos_abi::handle::Rights; - - let Some(object) = crate::process::process_object(pid) else { - crate::log!("process-reopen: FAIL (pid {} names no process)", pid.raw()); - return; - }; - let mut table = HandleTable::new(); - let opened = match ops::install(&mut table, KObjectRef::Process(Arc::clone(&object))) { - Ok(h) => h, - Err(e) => { - crate::log!("process-reopen: FAIL (the first install was refused: {e:?})"); - return; - } - }; - match table.remove(opened) { - Ok(entry) => drop(entry), - Err(e) => { - crate::log!("process-reopen: FAIL (the first handle would not close: {e})"); - return; - } - } - let retired = object.core().retired(); - let reopened = ops::install(&mut table, KObjectRef::Process(Arc::clone(&object))) - .ok() - .and_then(|h| table.get::(h, Rights::WAIT).ok()) - .is_some_and(|reached| reached.pid() == pid); - let verdict = if reopened && !retired { "PASS" } else { "FAIL" }; - crate::log!( - "process-reopen: {verdict} (pid={} retired={retired} reopened={reopened})", - pid.raw(), - ); -} diff --git a/kernel/src/process.rs b/kernel/src/process.rs index d7fb1d44161..54ef3ee91e4 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -365,7 +365,6 @@ impl ProcessEntry { } } pub fn pid(&self) -> Pid { self.pid } - pub fn object(&self) -> &Arc { &self.object } pub fn name(&self) -> &[u8; THREAD_NAME_LEN] { &self.name } pub fn name_str(&self) -> &str { core::str::from_utf8(&self.name).unwrap_or("?").trim_end_matches('\0') @@ -752,18 +751,7 @@ pub fn try_for_each_thread(mut f: impl FnMut(ThreadCensus<'_>)) -> bool { true } -/// The object a handle to `pid` would name, for a process still in the table. -/// A kernel thread's pid names none: it has no Ring 3 boundary a kill could end it at. -pub fn process_object(pid: Pid) -> Option> { - let guard = PROCESS_TABLE.lock(); - let proc = guard.as_ref()?.get(pid)?; - if crate::sched::kthread::is_kernel_task(TaskId(pid, proc.main_tid())) { - return None; - } - Some(Arc::clone(proc.object())) -} - -/// Accounting for a process. `None` only in the window between a live process and its published exit (the process being torn down right now). +/// Accounting for a process. pub fn stats_of( object: &crate::object::process::ProcessObject, ) -> Option { @@ -1744,6 +1732,35 @@ pub fn debug_kill_marked_place(parent: Parent) { } } +/// The process whose next landed spawn `debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS` marked; `Pid::MAX` for none. +#[cfg(feature = "test-actuators")] +static HELD_SPAWNER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(Pid::MAX.0); + +/// Mark the calling process's next spawn whose child lands. +#[cfg(feature = "test-actuators")] +pub fn debug_mark_spawn_hold() { + HELD_SPAWNER.store(current_process().0, core::sync::atomic::Ordering::Relaxed); +} + +/// Park the thread of a spawn its caller marked until `child`'s exit is published, and take the mark. The loader calls it once the child has landed. +#[cfg(feature = "test-actuators")] +pub fn debug_hold_marked_spawn(parent: Parent, child: &crate::object::process::ProcessObject) { + use core::sync::atomic::Ordering::Relaxed; + let Parent::Under(_) = parent else { return }; + if HELD_SPAWNER.compare_exchange(current_process().0, Pid::MAX.0, Relaxed, Relaxed).is_ok() { + let parkable = scheduler::Parkable::at_entry(); + // A cancelled wait is the spawner's own end, which the spawn's return meets. + let _ = crate::watch::wait_until( + &parkable, + child.watch(), + 0, + toyos_sched::task::WaitClass::Other, + crate::time::Deadline::never(), + || child.finished(), + ); + } +} + /// The shell convention for "died on SIGKILL"; kept because every test that reads one already spells it. pub const KILLED_EXIT_CODE: i32 = 137; diff --git a/kernel/src/sched/kthread.rs b/kernel/src/sched/kthread.rs index 2bba7e7cada..d26ceac57ca 100644 --- a/kernel/src/sched/kthread.rs +++ b/kernel/src/sched/kthread.rs @@ -73,23 +73,6 @@ pub fn is_kernel_task(id: TaskId) -> bool { ROWS.iter().any(|row| row.load(Ordering::Relaxed) == packed) } -/// Control for `process::process_object`'s refusal: no kernel thread's pid names a process a handle could hold. -#[cfg(feature = "boot-actuators")] -pub fn open_selftest() { - let pids: Vec<_> = ROWS - .iter() - .map(|row| row.load(Ordering::Relaxed)) - .filter(|&task| task != NO_TASK) - .map(|task| { - assert_ne!(task, CLAIMING, "kthread: a row is still being claimed after the last spawn"); - TaskId::unpack(task).0 - }) - .collect(); - let opened = pids.iter().filter(|&&pid| crate::process::process_object(pid).is_some()).count(); - let verdict = if !pids.is_empty() && opened == 0 { "PASS" } else { "FAIL" }; - crate::log!("process-open-kthread: {verdict} ({} kernel threads, {opened} opened)", pids.len()); -} - /// Start a kernel thread running `body(arg)` on its own kernel stack and return its scheduler faces. pub fn spawn(name: &str, body: extern "C" fn(u64) -> !, arg: u64) -> ThreadSched { let (stack, entry_sp) = crate::loader::alloc_kernel_stack( diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 6983dc7135c..f8a777987df 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::{ - spawn_place, sys_endowments, sys_exit, sys_nanosleep, sys_process_open, sys_process_stats, + spawn_place, sys_endowments, sys_exit, sys_nanosleep, 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}; @@ -276,9 +276,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Err(e) => e.refuse(), } } - SYS_PROCESS_OPEN => { - sys_process_open(RawHandle(a1 as u32), process::Pid::from_raw(a2 as u32)) - } // No right: the one caller marks both ends of a pair, so requiring one would refuse the other. SYS_MARK_TTY => with_object(RawHandle(a1 as u32), Rights::NONE, ops::mark_tty), @@ -635,6 +632,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> process::debug_mark_spawn(); 0 } + DA::HOLD_SPAWN_UNTIL_CHILD_ENDS => { + process::debug_mark_spawn_hold(); + 0 + } _ => SyscallError::InvalidArgument.to_u64(), }, SYS_SCHED_INFO => match ctx.copy_out(UserAddr::new(a1), &sys_sched_info()) { diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 142a1da40cf..c54120404ca 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -1,7 +1,6 @@ //! Process and thread syscalls: spawn, wait, exit, and self-ops on a thread. //! -//! A handle is the authority over a process; a pid alone is not. Only -//! `sys_process_open` mints a handle from a pid, gated on a `SysCap`. +//! A handle is the authority over a process; a pid alone is not. //! //! The parking calls clone what they wait on out of the table before //! blocking, so no guard is held across a park. @@ -9,7 +8,6 @@ use alloc::vec::Vec; use crate::watch; -use crate::object::{ops, KObjectRef}; use crate::time::{Deadline, Duration}; use crate::UserAddr; use crate::process; @@ -19,7 +17,7 @@ use toyos_abi::syscall::*; use toyos_sched::task::WaitClass; use super::cancelled; -use super::handles::{demand_syscap, handle_result}; +use super::handles::demand_syscap; pub(super) fn sys_thread_exit(code: i32) -> u64 { process::thread_exit(code); @@ -44,7 +42,7 @@ pub(super) fn spawn_place(place: u64) -> Result { .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. +/// Start a program in `cwd` under `parent` and return the handle to it the spawn's commit put in the caller's table. pub(super) fn sys_spawn( args: &[&str], pending: crate::loader::PendingHandles, @@ -53,21 +51,10 @@ pub(super) fn sys_spawn( 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, parent) { - Ok(object) => object, - Err(e) => return e.refuse(), - }; - let installed = process::with_process_data(|data| { - ops::install(&mut data.handles, KObjectRef::Process(object.clone())) - }); - match installed { - Ok(h) => h.0 as u64, - Err(e) => { - // Unnamed, it can be neither waited on nor killed later, so it is killed here. - process::kill_process(&object); - e.to_u64() - } + // Nothing to clean up: spawn's frame owns the child's resources on error. + match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) { + Ok((_, handle)) => u64::from(handle.0), + Err(e) => e.refuse(), } } @@ -109,19 +96,6 @@ pub(super) fn sys_process_wait(h: RawHandle, flags: u64) -> u64 { } } -/// Mint a `Process` handle for a pid, gated on a `SysCap` carrying [`Rights::MANAGE`]. -pub(super) fn sys_process_open(syscap: RawHandle, pid: process::Pid) -> u64 { - if let Err(e) = demand_syscap(syscap, Rights::MANAGE) { - return e.refuse(); - } - let Some(object) = process::process_object(pid) else { - return SyscallError::NotFound.to_u64(); - }; - process::with_process_data(|data| { - handle_result(ops::install(&mut data.handles, KObjectRef::Process(object))) - }) -} - /// Enter the real-time band, gated on a `SysCap` carrying [`Rights::RT`]. pub(super) fn sys_rt_enter(syscap: RawHandle) -> u64 { // Gated by manifest, not audio ownership: winning the sound-card race would grant the band too. diff --git a/src/ci.rs b/src/ci.rs index c1f90245836..4ff74000bfc 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -406,6 +406,13 @@ pub(crate) const CONTROLS: &[Control] = &[ 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(PROCLIFE, "mutate-spawner-handle-after-the-landing", None, &[ + Fails("interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it"), + Fails("interleave::tests::a_spawn_racing_the_kill_of_its_own_spawner"), + ]), + red(PROCLIFE, "mutate-spawner-handle-before-the-childs-own", None, &[ + Fails("interleave::tests::a_sibling_closing_a_spawns_handle_before_the_spawn_returns"), + ]), red(SCHED_SIM, "placement-ignores-staleness", Some("policy"), &[ Fails("a_stopped_cpu_stops_taking_work"), ]), diff --git a/src/metal.rs b/src/metal.rs index 5669ad49cc6..f54dadccedf 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -751,7 +751,6 @@ pub const FLASHABLE: &[&str] = &[ // the boot goes on to userland and ends the way an unarmed one does; what // an armed image leaves behind is a longer log. "pci-cap-selftest", - "process-reopen-selftest", "revoked-backing-selftest", "leak-rollback-selftest", "lapic-spurious-selftest", 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 c422ba0ea3c..5042587c153 100644 --- a/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/toyos-rust-tests/src/bin/abuse_handle_table.rs @@ -17,6 +17,10 @@ //! each has an arm here and the last arm is what says the machine survived //! them. //! +//! **A spawn's answer is an insert into its caller's table**, whose room is +//! checked before the spawn's endowments move: at the cap the spawn is +//! refused with every handle it named still the caller's. +//! //! **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` @@ -154,10 +158,18 @@ fn main() { "handle table reached {n} slots, past the {MAX_HANDLES} cap" ); + let last = filled.pop().expect("the fill installed a handle"); + let entry = EndowEntry { label_off: 0, label_len: LABELS.len() as u32, handle: last, _pad: 0 }; + let spawned = spawn_endowed(&[entry], LABELS); + // The cap is a live limit, not a latched failure. for handle in filled { syscall::close(handle); } + // Judged once the table has room again: a panic at the cap aborts with no report. + assert_eq!(spawned, Err(SyscallError::ResourceExhausted), "a spawn from a full table was not refused"); + // Still this process's: the close ends it if the refused spawn moved it. + syscall::close(last); let reused = syscall::dup2(RawHandle(1), 3) .expect("dup2 must work again after closing handles"); syscall::close(reused); diff --git a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs index 4c85b6bde62..5a63f02dd6f 100644 --- a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs +++ b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs @@ -9,10 +9,7 @@ //! wait before it parks and is woken by the publish, and two holders both get //! the answer. //! -//! Each arm below is one sentence of that paragraph, and three of them assert -//! the *opposite* of what the pid-keyed shape did: reading the code does not -//! spend it, a process that never started the child can still wait for it, and -//! a pid on its own reaches nothing at all. +//! Each arm below is one sentence of that paragraph. //! //! One arm is about the wait rather than the shape. //! `an_unrelated_wake_does_not_end_the_wait` provokes a wake that is not this @@ -31,7 +28,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::OnceLock; use toyos::endow::{Endowments, SYSCAP_LABEL}; -use toyos::AsHandle; use toyos::process::Process; use toyos::syscap::SysCap; use toyos_abi::syscall::{self, SyscallError}; @@ -65,9 +61,8 @@ fn test() { two_handles_answer_the_same(); a_kill_publishes_like_an_exit(); a_handle_is_the_whole_of_the_right(); - a_pid_is_not_authority(); an_undefined_wait_flag_bit_is_refused(); - println!("a process is a handle: the code is read, not claimed, and a pid grants nothing"); + println!("a process is a handle: the code is read, not claimed"); } /// `WNOHANG` is the whole of `SYS_PROCESS_WAIT`'s flag word; the other 63 bits @@ -211,8 +206,7 @@ fn a_thread_of_mine_has_exited() -> bool { /// The estate's system capability, taken once. /// /// **Once, because taking is a swap**: a second `take` of the same label finds -/// `HANDLE_INVALID` and answers `None`, and two arms here want the same cap — -/// one for the `MANAGE` refusal, one for the roster below. +/// `HANDLE_INVALID` and answers `None`. fn cap() -> &'static SysCap { static CAP: OnceLock = OnceLock::new(); CAP.get_or_init(|| { @@ -272,20 +266,6 @@ fn a_handle_is_the_whole_of_the_right() { println!(" a process that did not start the child waited for it, and so did the one that did"); } -/// A pid is a name everybody can say, and saying it is not a key. The one call -/// that turns one into a handle needs a capability carrying `MANAGE`, and the -/// kernel mints exactly one — `/system/bin/init`'s. The test estate's carries `DEVICE` -/// and `DUP`, which is what makes this refusal non-vacuous: the handle resolves, -/// and it is the right that is missing. -fn a_pid_is_not_authority() { - assert_eq!( - syscall::process_open(cap().as_handle(), syscall::getpid()), - Err(SyscallError::PermissionDenied), - "a capability without MANAGE opened a process by pid", - ); - println!(" a pid does not become a handle without MANAGE"); -} - /// A child that exits with `code` when this process says so, and the write end /// that says so: dropping it is what lets the child go. /// diff --git a/tests/toyos-rust-tests/src/bin/spawn_child_ends_first.rs b/tests/toyos-rust-tests/src/bin/spawn_child_ends_first.rs new file mode 100644 index 00000000000..660f7cef2de --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/spawn_child_ends_first.rs @@ -0,0 +1,106 @@ +//! A spawn whose child has ended before the spawn answers still answers a +//! handle to it, and the handle reads the child's code. +//! +//! A child's own table holds its `self` and closes when the child ends, so a +//! spawner's handle minted after that would be the first on an object whose +//! last had gone. No caller can order a child's end inside the spawn that +//! starts it: `debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS` has the kernel hold +//! this process's next spawn, once its child has landed, until the child's +//! exit is published. +//! +//! The same hold is what lets a refused spawn be asked whether it started a +//! child: one that had landed would have run to its end, and spoken, before +//! the spawn answered. +//! +//! Every wait is unbounded: the runner's deadline is the only clock. + +use toyos_abi::syscall::{self, debug_action, SpawnArgs, SyscallError}; +use toyos_abi::RawHandle; + +const SELF_PATH: &str = "/system/bin/test_rs_spawn_child_ends_first"; + +/// What the child exits with. +const CODE: i32 = 7; + +fn main() { + match std::env::args().nth(1).as_deref() { + Some("exit") => std::process::exit(CODE), + Some("speak") => { + assert_eq!(syscall::write(RawHandle(1), b"x"), Ok(1), "the child's one byte"); + } + Some(other) => panic!("unknown role {other:?}"), + None => test(), + } +} + +fn test() { + hold_the_next_spawn(); + let child = spawn("exit", &[]).expect("a spawn whose child ended inside it"); + assert_eq!( + syscall::process_wait_nonblock(child), + Ok(CODE), + "the child had not ended when its spawn answered, so the kernel held nothing" + ); + syscall::close(child); + a_spawn_from_a_full_table_starts_no_child(); + println!("spawn_child_ends_first: the spawn answered a child that had already ended, and a refused one started none"); +} + +/// A spawn whose caller's table has no slot for its answer is refused with +/// nothing of the child started: the pipe its child would have spoken into +/// ends empty. The mark is left standing, since the spawn never lands. +fn a_spawn_from_a_full_table_starts_no_child() { + let ends = syscall::pipe().expect("a pipe for the child to speak into"); + let mut filled = Vec::new(); + let full = loop { + match syscall::dup(ends.read) { + Ok(handle) => filled.push(handle), + Err(refused) => break refused, + } + }; + assert_eq!(full, SyscallError::ResourceExhausted, "the table did not fill"); + hold_the_next_spawn(); + let spawned = spawn("speak", &[[1, ends.write.0]]); + for handle in filled { + syscall::close(handle); + } + // Judged once the table has room again: a panic at the cap aborts with no report. + assert_eq!(spawned, Err(SyscallError::ResourceExhausted), "a spawn from a full table was not refused"); + syscall::close(ends.write); + let mut byte = [0u8; 1]; + assert_eq!(syscall::read(ends.read, &mut byte), Ok(0), "the refused spawn's child started and spoke"); + syscall::close(ends.read); +} + +fn hold_the_next_spawn() { + assert_eq!( + syscall::debug(debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS), + 0, + "this kernel does not carry SYS_DEBUG, so nothing holds the spawn" + ); +} + +/// `SYS_SPAWN` of this binary in `role`, duplicating each `[child_slot, +/// parent_handle]` pair of `slot_map` into the child. +fn spawn(role: &str, slot_map: &[[u32; 2]]) -> Result { + let argv = format!("{SELF_PATH}\0{role}"); + let args = SpawnArgs { + argv_ptr: argv.as_ptr() as u64, + argv_len: argv.len() as u64, + slot_map_ptr: slot_map.as_ptr() as u64, + slot_map_count: slot_map.len() as u64, + 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(toyos_abi::HANDLE_INVALID.0), + }; + // SAFETY: every pointer names a buffer of this frame that outlives the call. + unsafe { syscall::spawn(&args) } +} diff --git a/tests/toyos.rs b/tests/toyos.rs index db3d987a60e..8fa6317722d 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -47,6 +47,9 @@ const ACTUATOR_TESTS: &[&str] = &[ // 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", + // Action 23: the kernel holds a spawn, once its child has landed, until + // the child has ended — a child's end no caller can order inside its spawn. + "spawn_child_ends_first", ]; /// What [`ACTUATOR_TESTS`] boots: the one kernel that carries `SYS_DEBUG`, with @@ -547,7 +550,7 @@ const METAL: &[(&str, metal::Metal)] = &[ "metal_sim_scanout_wc", metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), - // ---- one image, eleven actuators, ten tests ---- + // ---- one image ---- // The cheapest cluster there is: every one of these arms a check that runs // at init, logs its verdict and does nothing else, so they cost one flash // between them. **Nothing had to be promoted into `kernel/src/params.rs`** @@ -557,10 +560,6 @@ const METAL: &[(&str, metal::Metal)] = &[ "pci_capability_walk", metal::Metal { arms: SELFTESTS, judge: |b| pci_cap_selftest(b[0].kernel().text()) }, ), - ( - "process_reopen_selftest", - metal::Metal { arms: SELFTESTS, judge: |b| process_reopen(b[0].kernel().text()) }, - ), ( "read_fault_selftests", metal::Metal { arms: SELFTESTS, judge: |b| read_fault_probes(b[0].kernel().text()) }, @@ -690,17 +689,15 @@ const LANTALKCASE: &[metal::Arm] = &[metal::Arm { /// One boot for every in-kernel self-test that logs its verdict at init and /// does nothing else. /// -/// **Eleven actuators in one image.** They cost the machine one flash between -/// them because none of them changes what the machine *is*: each stages inputs -/// the hardware cannot produce — a crafted capability list, a malformed -/// descriptor, a vector nothing claims — runs a check over them and prints a -/// count. +/// They cost the machine one flash between them because none of them changes +/// what the machine *is*: each stages inputs the hardware cannot produce — a +/// crafted capability list, a malformed descriptor, a vector nothing claims — +/// runs a check over them and prints a count. const SELFTESTS: &[metal::Arm] = &[metal::once( "selftests", "tests/testcases", &[ "pci-cap-selftest", - "process-reopen-selftest", "revoked-backing-selftest", "leak-rollback-selftest", "lapic-spurious-selftest", @@ -2256,21 +2253,6 @@ fn process_tree(back: &metal::Readback) -> Result<(), String> { 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> { - for control in ["process-reopen:", "process-open-kthread:"] { - let Some(verdict) = log.lines().find(|l| l.contains(control)) else { - return Err(format!("{control} never ran:\n{log}")); - }; - if !verdict.contains("PASS") { - return Err(format!("{}\n{log}", verdict.trim())); - } - eprintln!(" [process] {}", verdict.trim()); - } - Ok(()) -} - /// A backing read after deletion is refused on both writable mounts, and a page-cache slot whose fill the device refused is unbound. fn read_fault_probes(log: &str) -> Result<(), String> { let probe = "revoke-selftest: /tmp/revoke_probe"; diff --git a/toyos-abi/src/handle.rs b/toyos-abi/src/handle.rs index a0e1a324cc0..400c89b405a 100644 --- a/toyos-abi/src/handle.rs +++ b/toyos-abi/src/handle.rs @@ -79,7 +79,7 @@ impl Rights { pub const MAP: Rights = Rights(1 << 4); /// Block on it, or name it in an [`OP_WATCH`](crate::inbox::OP_WATCH). pub const WAIT: Rights = Rights(1 << 5); - /// Kill a process; on a `SysCap`, open one by pid. + /// Kill a process. pub const MANAGE: Rights = Rights(1 << 6); /// On a `SysCap`: enter the RT band. pub const RT: Rights = Rights(1 << 7); diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 56df4fc0839..8f07d596494 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -263,15 +263,6 @@ pub const SYS_PROCESS_WAIT: u64 = 108; /// /// [`Rights::MANAGE`]: crate::handle::Rights::MANAGE pub const SYS_PROCESS_KILL: u64 = 109; -/// A `Process` handle for a pid, gated by [`Rights::MANAGE`] on a `SysCap`. -/// See [`process_open`]. -/// -/// The one place a pid becomes authority, and only `init` holds a cap that -/// carries the right — so the set of processes that can reach a process they -/// did not start is exactly what init endowed. -/// -/// [`Rights::MANAGE`]: crate::handle::Rights::MANAGE -pub const SYS_PROCESS_OPEN: u64 = 110; /// Mint a device claim for a class, gated by [`Rights::DEVICE`] on a `SysCap`. /// Only `init` holds such a cap, so the set of processes that can ever @@ -887,6 +878,11 @@ pub mod debug_action { /// 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; + /// Mark the caller's next spawn whose child lands: its thread waits there, + /// before the spawn answers, until the child's exit is published. A child + /// ending inside the spawn that started it is a race no caller can order; + /// the landing and the exit either side of the wait are the shipped paths. + pub const HOLD_SPAWN_UNTIL_CHILD_ENDS: u64 = 23; } /// Every kind of kernel object, in the order the kernel's own `kobject!` @@ -945,7 +941,10 @@ pub fn get_env(buf: &mut [u8]) -> usize { /// Answers a `Process` handle carrying `WAIT|MANAGE|READ|DUP|TRANSFER`. A /// caller that wants nothing to do with the child closes it; a caller that /// wants to hand it on transfers it. There is no pid-addressed way back to a -/// process, so this handle is the whole of what a spawn confers. +/// process, so this handle is the whole of what a spawn confers. Its slot is +/// taken before an endowment moves: a caller whose table has none is refused +/// `ResourceExhausted` with its table as it was, whatever its endowments +/// would have freed. /// /// # Safety /// The raw pointer fields in `SpawnArgs` must point to valid memory. @@ -984,13 +983,6 @@ pub fn process_kill(proc: RawHandle) -> Result<(), SyscallError> { check_unit(syscall(SYS_PROCESS_KILL, proc.0 as u64, 0, 0, 0)) } -/// A `Process` handle for `pid`, presenting a `SysCap` that carries -/// `Rights::MANAGE`. -pub fn process_open(syscap: RawHandle, pid: Pid) -> Result { - check(syscall(SYS_PROCESS_OPEN, syscap.0 as u64, pid.0 as u64, 0, 0)) - .map(|h| RawHandle(h as u32)) -} - /// Copy records into `out`, oldest first and merged by `at_ns`, advancing /// `cursor`. Answers how many records were written, and `0` when there is /// nothing new. @@ -2355,10 +2347,9 @@ pub struct ProcessStats { pub fault_zero_count: u32, pub fault_ns: u64, pub io_read_ops: u32, - /// The process's own pid. Not authority — nothing takes a pid but - /// [`SYS_PROCESS_OPEN`], which takes a `SysCap` beside it — but it is the - /// name a diagnostic prints, and this is where a holder of a handle reads - /// it. + /// The process's own pid. Not authority — no syscall takes a pid — but it + /// is the name a diagnostic prints, and this is where a holder of a handle + /// reads it. pub pid: u32, pub io_read_bytes: u64, pub blocked_io_ns: u64, diff --git a/toyos-proclife/Cargo.toml b/toyos-proclife/Cargo.toml index 42b91e0c64c..8c61bfca070 100644 --- a/toyos-proclife/Cargo.toml +++ b/toyos-proclife/Cargo.toml @@ -87,6 +87,20 @@ mutate-publish-before-the-children = [] # `interleave::tests::a_spawn_under_an_unrelated_process_lands_between_two_claims_of_one_walk` # must red under this. mutate-walk-in-one-hold = [] +# The model's spawn mints its caller's handle to the child once the spawn has +# landed and its retires are posted, as a kernel that installed it after +# `loader::spawn` returned would: a child claimed as it lands closes its table, +# its own `self` with it, before that. +# `interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it` +# must red under this. +mutate-spawner-handle-after-the-landing = [] +# The model's spawn mints its caller's handle to the child under the caller's +# lock and the child's own once that lock is given up, as a kernel whose commit +# installed the one before it minted the other would: another thread of the +# caller closes the only handle the object has. +# `interleave::tests::a_sibling_closing_a_spawns_handle_before_the_spawn_returns` +# must red under this. +mutate-spawner-handle-before-the-childs-own = [] [dependencies] toyos-abi = { path = "../toyos-abi" } diff --git a/toyos-proclife/src/interleave.rs b/toyos-proclife/src/interleave.rs index 134a58dd2bb..494524afbea 100644 --- a/toyos-proclife/src/interleave.rs +++ b/toyos-proclife/src/interleave.rs @@ -50,10 +50,12 @@ pub enum Op { /// 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. + /// whole of a process built with every lock given up, then the commit + /// under the caller's own lock — its handles move, and its handle to the + /// child is minted beside the child's own — then, with that lock given + /// up, 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. `child` is the process the landing made. SpawnUnder { place: Pid, by: (Pid, Tid), @@ -62,6 +64,7 @@ pub enum Op { admitted: Option, retire: Vec<(Pid, Tid)>, climb: Option, + child: 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. @@ -70,6 +73,11 @@ pub enum Op { ThreadExit { pid: Pid, tid: Tid, code: i32, pc: u32 }, /// `sys_thread_join`: collect or arm, then re-check. Join { pid: Pid, target: Tid, waiter: Tid, pc: u32 }, + /// `sys_close` by `by`'s thread on its process's handle to `object`, named + /// by the number a spawn will answer: one section, the table's own lock. + /// A close that finds no handle is its caller's own end, which + /// [`Op::Exit`] scripts, and does nothing here. + Close { by: (Pid, Tid), object: Pid, pc: u32 }, /// The idle loop's `reap_finished`. IdlePass { pc: u32 }, } @@ -89,11 +97,11 @@ impl Op { /// 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 } + Op::SpawnUnder { place, by, fails: false, pc: 0, admitted: None, retire: Vec::new(), climb: None, child: 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 } + Op::SpawnUnder { place, by, fails: true, pc: 0, admitted: None, retire: Vec::new(), climb: None, child: None } } pub fn spawn(pid: Pid) -> Self { Op::Spawn { pid, pc: 0, block: None } @@ -104,6 +112,9 @@ impl Op { pub fn join(pid: Pid, target: Tid, waiter: Tid) -> Self { Op::Join { pid, target, waiter, pc: 0 } } + pub fn close(by: (Pid, Tid), object: Pid) -> Self { + Op::Close { by, object, pc: 0 } + } pub fn idle_pass() -> Self { Op::IdlePass { pc: 0 } } @@ -114,7 +125,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::SpawnUnder { by, .. } | Op::Close { by, .. } => Some(by), Op::Spawn { .. } | Op::IdlePass { .. } => None, } } @@ -127,6 +138,7 @@ impl Op { | Op::Spawn { pc, .. } | Op::ThreadExit { pc, .. } | Op::Join { pc, .. } + | Op::Close { pc, .. } | Op::IdlePass { pc, .. } => *pc == DONE, } } @@ -148,6 +160,7 @@ impl Op { Op::Spawn { .. } => "spawn_thread", Op::ThreadExit { .. } => "thread_exit", Op::Join { .. } => "thread_join", + Op::Close { .. } => "close", Op::IdlePass { .. } => "idle pass", } } @@ -217,7 +230,7 @@ impl Op { } } } - Op::SpawnUnder { place, by, fails, pc, admitted, retire, climb } => { + Op::SpawnUnder { place, by, fails, pc, admitted, retire, climb, child } => { match *pc { // The admission, under the table lock and before anything is built. 0 => match tree::admit_child(world, Some(*place)) { @@ -237,28 +250,53 @@ impl Op { match tree::refuse_child(world, taken) { Some(publish) => { *climb = Some(Climb::Publish(publish)); - *pc = 3; + *pc = CLIMB; } None => *pc = DONE, } } - // The caller's handles move, and the child lands, in the - // hold that inserts it. + // The commit, under the caller's own lock. 1 => { world.move_handles(*by); + let made = admitted.as_ref().expect("admitted at the first section").pid(); + // The mutation this feature stages: the child's own + // handle waits for the caller's lock to be given up. + if !cfg!(feature = "mutate-spawner-handle-before-the-childs-own") { + world.mint(made, made); + } + // The mutation this feature stages: the caller's + // handle waits for the spawn to land. + if !cfg!(feature = "mutate-spawner-handle-after-the-landing") { + world.mint(made, by.0); + } + *pc = LAND; + } + // With that lock given up: the child lands in the hold + // that inserts it. + LAND => { let taken = admitted.take().expect("admitted at the first section"); - let child = taken.pid(); + let made = taken.pid(); + if cfg!(feature = "mutate-spawner-handle-before-the-childs-own") { + world.mint(made, made); + } 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; + tree::land_child(world, taken, KILLED, |world, node| world.insert(made, node)); + world.landed(*place, made); + *retire = owed.into_iter().map(|t| (made, t)).collect(); + *child = Some(made); + *pc = LANDED; } // With the lock given up: the retires the landing answered. - 2 => { + LANDED => { for (victim, thread) in retire.drain(..) { world.post_retire(victim, thread); } + *pc = if cfg!(feature = "mutate-spawner-handle-after-the-landing") { LATE_HANDLE } else { DONE }; + } + // The mutation's install, once the spawn has landed and + // its retires are posted. + LATE_HANDLE => { + world.mint(child.expect("landed two sections back"), by.0); *pc = DONE; } // The failed build let its place's last hold go: this @@ -342,6 +380,11 @@ impl Op { world.leave_kernel((*pid, *waiter)); } } + Op::Close { by, object, pc } => { + world.close(*object, by.0); + world.leave_kernel(*by); + *pc = DONE; + } Op::IdlePass { pc } => { for pid in reap::finished_pids(world) { world.reap(pid); @@ -359,6 +402,14 @@ const POST: u32 = 1; const WALK: u32 = 2; /// `mutate-kill-waits-for-its-victims`' wait. const WAIT: u32 = 3; +/// `mutate-spawner-handle-after-the-landing`'s install. +const LATE_HANDLE: u32 = 4; +/// A spawn's landing. +const LAND: u32 = 2; +/// The section that posts the retires a spawn's landing answered. +const LANDED: u32 = 3; +/// A failed spawn's climb. +const CLIMB: u32 = 5; /// 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 @@ -735,9 +786,11 @@ mod tests { /// every one. /// /// Reds under `mutate-place-skips-the-insert-recheck`, where a child lands - /// unclaimed under the claimed place and outlives it, and under + /// unclaimed under the claimed place and outlives it, under /// `mutate-refused-spawn-keeps-the-count`, where the place is never - /// published. + /// published, and under `mutate-spawner-handle-after-the-landing`, where a + /// child claimed as it lands closes its table before its spawner's handle + /// is minted. #[test] fn a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it() { let mut world = World::new(); @@ -764,6 +817,28 @@ mod tests { holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under_failing(place, own)]); } + /// **A spawner's other thread closes the spawn's handle the moment it is + /// in their table**, every ordering, alone and with the place's kill + /// racing both: a handle's number is its table's own arithmetic, so the + /// handle is nameable before the spawn that answers it has returned. + /// + /// Reds under `mutate-spawner-handle-before-the-childs-own`, where that + /// handle is the object's only one until the caller's lock is given up. + #[test] + fn a_sibling_closing_a_spawns_handle_before_the_spawn_returns() { + let mut world = World::new(); + let init = world.spawn_process(); + let place = world.spawn_child(init); + let sibling = (place, world.spawn_thread(place)); + let own = (place, world.main_tid(place)); + let Admit::Yes(next) = tree::admit_child(&mut world.clone(), Some(place)) else { + panic!("a live place admitted no child"); + }; + let made = next.pid(); + holds(&world, vec![Op::spawn_under(place, own), Op::close(sibling, made)]); + holds(&world, vec![Op::kill(place, KILLED), Op::spawn_under(place, own), Op::close(sibling, made)]); + } + /// An exit takes a subtree two deep below it, and each end is published /// after every end below it. /// diff --git a/toyos-proclife/src/model.rs b/toyos-proclife/src/model.rs index d29d78f8b43..a1a09156731 100644 --- a/toyos-proclife/src/model.rs +++ b/toyos-proclife/src/model.rs @@ -3,8 +3,9 @@ //! //! `#[cfg(test)]`, so none of it reaches a kernel build. What it adds beyond //! the two traits is the *consequences* a decision hands back and the kernel -//! performs — a watch's post, a retire, a `publish_exit`, an idle -//! pass taking an entry — because the laws worth checking are about the order +//! performs — a watch's post, a retire, a `publish_exit`, a handle minted or +//! closed, an idle pass taking an entry — because the laws worth +//! checking are about the order //! those happen in, and a model that only held the two states could not see //! one. //! @@ -143,6 +144,13 @@ pub struct World { moved: BTreeSet<(Pid, Tid)>, /// Spawning threads answered a refusal after their handles moved. refused_after_move: BTreeSet<(Pid, Tid)>, + /// Handles to a process's object: the object, and the process whose table + /// holds one. + handles: BTreeSet<(Pid, Pid)>, + /// Objects whose last handle has gone. + retired: BTreeSet, + /// Objects a handle was minted on after their last had gone. + minted_retired: BTreeSet, } impl Processes for World { @@ -193,6 +201,9 @@ impl World { inserted_at: Vec::new(), moved: BTreeSet::new(), refused_after_move: BTreeSet::new(), + handles: BTreeSet::new(), + retired: BTreeSet::new(), + minted_retired: BTreeSet::new(), } } @@ -231,11 +242,41 @@ impl World { panic!("World::land: {place:?} admits no child"); }; let pid = admitted.pid(); + // As a spawn's commit leaves them: its place's handle to it, and its own. + if let Some(place) = place { + self.mint(pid, place); + } + self.mint(pid, pid); let ((), retire) = tree::land_child(self, admitted, 137, |world, node| world.insert(pid, node)); assert_eq!(retire, [], "World::land: {place:?} was claimed"); pid } + /// `HandleEntry::new`: `holder`'s table takes a handle to `object`. + pub fn mint(&mut self, object: Pid, holder: Pid) { + if self.retired.contains(&object) { + self.minted_retired.insert(object); + } + self.handles.insert((object, holder)); + } + + /// `HandleEntry`'s drop: `holder`'s handle to `object` goes, if it has + /// one, and an object left with no handle is retired. + pub fn close(&mut self, object: Pid, holder: Pid) { + if self.handles.remove(&(object, holder)) && !self.handles.iter().any(|&(held, _)| held == object) { + self.retired.insert(object); + } + } + + /// `teardown_resources` draining `pid`'s table. + fn close_table(&mut self, pid: Pid) { + let closed: Vec = + self.handles.iter().filter(|&&(_, holder)| holder == pid).map(|&(object, _)| object).collect(); + for object in closed { + self.close(object, 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. @@ -407,6 +448,7 @@ impl World { Out::Free { code, mark } => { *self.frees.entry(pid).or_insert(0) += 1; self.mapped.retain(|&(p, _)| p != pid); + self.close_table(pid); Out::Mark { code, mark } } Out::Mark { code, mark } => { @@ -539,6 +581,11 @@ impl World { for pid in &self.landed_under_claimed { out.push(alloc::format!("pid {pid} landed unclaimed under a place whose end was already claimed")); } + // L13. No handle is minted on an object whose last one has gone: + // `HandleEntry::new` asserts it. + for pid in &self.minted_retired { + out.push(alloc::format!("pid {pid}: a handle to it was minted after its last one had gone")); + } out } diff --git a/toyos/src/syscap.rs b/toyos/src/syscap.rs index 099c715847d..1501c2f5834 100644 --- a/toyos/src/syscap.rs +++ b/toyos/src/syscap.rs @@ -1,10 +1,9 @@ //! The capability whose whole authority is in the rights on the handle. //! //! Some things are reachable no other way — minting a device claim, entering -//! the real-time band, turning a pid into a process handle, listing every -//! process in the machine, reading what the machine is made of, and taking its -//! power away, off or back to firmware — and each is one bit on a handle to -//! this. The kernel makes exactly one at boot, for `init`, so the set of +//! the real-time band, listing every process in the machine, reading what the +//! machine is made of, and taking its power away, off or back to firmware — +//! and each is one bit on a handle to this. The kernel makes exactly one at boot, for `init`, so the set of //! processes that can ever do any of them is exactly what init endowed. use toyos_abi::handle::Rights; @@ -134,24 +133,11 @@ impl SysCap { /// A second handle to this capability carrying **less**. /// /// How init gives a program the RT band and nothing else: rights only - /// shrink, so the dup can never mint a claim or open a process however the - /// holder asks. + /// shrink, so the dup can never mint a claim however the holder asks. pub fn narrowed(&self, rights: Rights) -> Result { syscall::dup_narrowed(self.0.raw(), rights).map(|h| Self(OwnedHandle(h))) } - /// A `Process` handle for a pid. - /// - /// The one place a pid becomes authority over anything, and only a cap - /// carrying [`Rights::MANAGE`] reaches it — which in the whole system is - /// `init`'s. - pub fn open_process(&self, pid: toyos_abi::Pid) -> Result { - let raw = syscall::process_open(self.0.raw(), pid)?; - // SAFETY: the kernel installed this handle in this process's table for - // this call and no other. - Ok(unsafe { crate::process::Process::from_raw(raw) }) - } - /// Give up ownership, for a handle about to be endowed. pub fn into_raw(self) -> RawHandle { self.0.into_raw()