diff --git a/Cargo.lock b/Cargo.lock index f505f0c7..ef7ebdcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,10 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "driver-primitives" +version = "0.1.0" + [[package]] name = "equivalent" version = "1.0.2" @@ -110,6 +114,7 @@ dependencies = [ "bitfield", "bitflags 2.12.1", "cc", + "driver-primitives", "fdt", "hashbrown 0.14.5", "linked_list_allocator", diff --git a/Cargo.toml b/Cargo.toml index 3d6aed15..27d4f9ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ [workspace] resolver = "3" +# These crates are their own isolated workspaces (own toolchain/CI); the kernel +# depends on driver-primitives by path but must not absorb it as a member. +exclude = ["driver-primitives", "redox-virtio-drivers"] [package] name = "kernel" @@ -22,6 +25,7 @@ redox-path = "0.2.0" #redox_syscall = { version = "0.8.1", default-features = false } redox_syscall = { git = "https://gitlab.redox-os.org/redox-os/syscall.git", default-features = false } rmm = { path = "rmm", default-features = false } +driver-primitives = { path = "driver-primitives" } slab = { version = "0.4", default-features = false } smallvec = { version = "1.15.1", default-features = false } spin = { version = "0.9.8" } diff --git a/docs/security-wall.md b/docs/security-wall.md new file mode 100644 index 00000000..fc5398f6 --- /dev/null +++ b/docs/security-wall.md @@ -0,0 +1,107 @@ +# The Capability Security Wall + +Status: **ENFORCING and BOOT-VERIFIED.** Scheme creation is authorized through the +capability wall (`scheme/mod.rs` → `security::authorize_scheme_create`); the kernel +boots to `redox login:` in QEMU with the full daemon chain up (pcid → virtio-gpud → +display), no panic — every boot-time scheme creation authorized through the wall. +Behaviour is preserved for `uid == 0`, but the authority is now a **revocable +capability** (a revoked domain is refused even at uid 0). Remaining work is migrating +the other `euid`-gated seams (`fchown`, proc ops) the same way, and tightening policy. + +> Boot-test note: `WALL_CAPACITY` must stay small — the kit `HandleTable` is a +> fixed-capacity array built on the kernel stack at boot; an oversized value overflows +> the stack (double fault in `kmain`). 256 boots cleanly; 4096 did not. + +The kernel's security is moving from ambient identity checks (`euid == 0`, scattered) +to **held, immutable, revocable capabilities** — composed from the `driver-primitives` +kit and driven by a kernel orchestrator. This is the primitive + orchestrator doctrine: +the kit provides dumb mechanism; the kernel owns all policy. + +## What exists now (verified) + +- **Primitive — `driver-primitives::authority::AuthorityWall`.** A composed capability + table: `HandleTable` (generation-checked, unforgeable handles) + `SeqCounter` (issue + epoch). Three invariants: + - *Immutable by construction* — a capability's `Authority` is fixed at mint; no setter. + - *Capability-to-construct* — authorization is *holding* a wall-minted `CapToken`; a + forged or revoked token is rejected by the generation check, not an advisory compare. + - *Revocation is destruction* — `revoke` bumps the slot generation, invalidating every + outstanding copy at once. + 7 unit tests; fmt + clippy `-D warnings` + bare-metal build green in CI. +- **Orchestrator — `kernel::security`.** Owns the single wall plus a domain index + (`BTreeMap`). `init()` (called from `kmain`) grants the root domain + `Authority::ALL`. Facade: `grant_domain`, `authorize_domain`, `revoke_domain` — identity + is **structural** (the kernel names the domain from the acting context, never asserted). + `make check` green; zero new warnings. + +Nothing enforces yet — `authorize_domain` is wired but not consulted at a gate. That is +deliberate: enforcement cannot land until distribution lands (below), and both need a +boot test. + +## The target seam + +`SchemeList::kdup`, `src/scheme/mod.rs:312-325`: + +```rust +Handle::SchemeCreationCapability => (), // :312 — real capability (already structural) +// ... b"create-scheme" payload check ... // :316 +if caller.uid != 0 { // :323 — the ambient identity wall to replace + return Err(Error::new(EACCES)); +} +``` + +Possession of the scheme-creation fd is *already* a real wall; the `uid != 0` line is the +painted-on advisory check beside it. The end state replaces line 323 with: + +```rust +if !security::authorize_domain(domain_of(caller), Authority::CREATE_SCHEME) { + return Err(Error::new(EACCES)); +} +``` + +where `domain_of(caller)` is derived structurally from the caller's context +(`owner_proc_id`, `context.rs:159`, or `pid`, `:164` — decision below). + +## Why enforcement cannot land yet (the honest blocker) + +`authorize_domain` denies any domain that holds no capability. Today only the root domain +is granted. If the gate flipped now, **every non-root scheme-creating daemon would be +denied and boot/driver-init would break.** Enforcement therefore requires **capability +distribution first**: the orchestrator must grant each context its authority as it is +created. And because a wrong distribution policy wedges boot, the change must be +**boot-verified in QEMU** — it is not provable by `make check` (compile only). + +## Activation plan (each step boot-tested) + +1. **DomainId mapping.** Fix `DomainId = owner_proc_id` (process-granular; a process is the + natural authority domain). Resolve the bootstrap process's id and confirm `init()`'s + `ROOT_DOMAIN` equals it (or grant the real bootstrap id). *Boot-test:* unchanged boot. +2. **Distribution on spawn.** Hook context/process creation: `security::grant_domain(child, + policy(parent, child))`. Initial policy = inherit the parent's authority (preserves + today's behavior exactly), so the capability graph mirrors the current process tree. + *Boot-test:* unchanged boot; every domain that creates a scheme today now holds + `CREATE_SCHEME`. +3. **Gate flip behind a feature.** Add cargo feature `cap-enforce-scheme-create` (default + **off** → no behavior change → default boot safe). Under the feature, replace + `scheme/mod.rs:323` with the `authorize_domain` check. *Boot-test with the feature on:* + boot must still reach userspace and drivers must still create their schemes. +4. **Tighten policy.** Once inherit-all enforces cleanly, narrow it: only the domains that + legitimately create schemes receive `CREATE_SCHEME`; everything else is denied. This is + the actual security gain. *Boot-test:* the system still boots; an unauthorized domain is + refused (a positive enforcement test). +5. **Revocation on teardown.** Call `revoke_domain` from the context exit path so a dead + domain's capability is destroyed. *Boot-test:* spawn/exit cycles leak no capabilities + (`live()` returns to baseline). +6. **Remove the ambient check.** With enforcement boot-proven, delete the `uid != 0` line + (and pursue the other `euid`-gated seams the same way). Make the feature default-on, then + non-optional. + +## Distribution discipline (do not regress) + +- Identity is **derived**, never a caller argument. +- One capability per domain; re-granting **revokes** the old one (no rights edit). +- No mutable policy state on the data path; `authorize_domain` is an O(log n) map lookup + + the wall's O(1) generation check, under one lock — keep it off the hottest paths or move + to a per-context cached token if it shows up in profiles. +- Never bolt an advisory parallel check beside the real fd/cap wall; the gate either is the + capability check or it is not there. diff --git a/driver-primitives/src/authority.rs b/driver-primitives/src/authority.rs new file mode 100644 index 00000000..a3a5dcbc --- /dev/null +++ b/driver-primitives/src/authority.rs @@ -0,0 +1,654 @@ +//! Authority wall — a composed capability primitive for cross-domain authority. +//! +//! This is the kit's security mechanism, built by *composition* of the dumber +//! primitives ([`HandleTable`] for unforgeable naming, [`SeqCounter`] for issue +//! provenance). Like every primitive here it is **dumb mechanism, zero policy**: +//! it mints, checks, and revokes capabilities. *Who* should hold which authority +//! is the orchestrator's decision (on Redox, the kernel/bootstrap) — never the +//! wall's. +//! +//! ## The doctrine, in three invariants +//! +//! - **Immutable by construction.** A capability's rights ([`Authority`]) are +//! fixed at mint and never edited. There is no rights setter. "Changing" a +//! domain's authority is *revoke then re-issue*, which yields a fresh token — +//! so a stale view of authority can never be mistaken for the current one. +//! - **Capability-to-construct.** Authorization is *holding* a [`CapToken`] the +//! wall minted. A forged token, or one whose capability was revoked, is +//! rejected structurally by [`HandleTable`]'s generation check — not by an +//! advisory comparison that a caller could skip. There is nothing to route +//! around: you cannot construct an authorizing token without the wall issuing +//! it. +//! - **Revocation is destruction.** [`revoke`](AuthorityWall::revoke) removes the +//! record, which bumps the slot generation, so every outstanding copy of the +//! token stops authorizing at once. No downgrade-in-place, no global revocation +//! list to consult on the hot path. +//! +//! A token is bound to the domain it was minted for, so presenting another +//! domain's token to authorize *your* domain fails the check. Identity is +//! *derived from the held capability*, never asserted by the caller. +//! +//! [`HandleTable`]: crate::handle::HandleTable +//! [`SeqCounter`]: crate::fence::SeqCounter + +use crate::fence::SeqCounter; +use crate::handle::{Handle, HandleTable}; + +/// A security domain identity (a process, driver daemon, or service). Opaque and +/// immutable; the wall never interprets it beyond equality. Ordered so an +/// orchestrator can index domains in a map. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct DomainId(pub u64); + +/// The set of authorities a capability conveys. An immutable bitset, fixed at +/// mint; the holder cannot widen or edit it. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct Authority(u32); + +impl Authority { + /// No authority. + pub const NONE: Self = Self(0); + /// May create a new scheme (mint a service endpoint). + pub const CREATE_SCHEME: Self = Self(1 << 0); + /// May map physical/device memory. + pub const MAP_MEMORY: Self = Self(1 << 1); + /// May claim an IRQ line. + pub const CLAIM_IRQ: Self = Self(1 << 2); + /// May spawn a new context. + pub const SPAWN: Self = Self(1 << 3); + /// May mint capabilities for *other* domains (delegation authority). + pub const DELEGATE: Self = Self(1 << 4); + + const KNOWN: u32 = 0b1_1111; + /// Every defined authority (the bootstrap/root set). + pub const ALL: Self = Self(Self::KNOWN); + + /// Construct from raw bits, keeping only defined authorities. + pub const fn from_bits_truncate(bits: u32) -> Self { + Self(bits & Self::KNOWN) + } + + /// The raw bits, for audit/serialization. + pub const fn bits(self) -> u32 { + self.0 + } + + /// Whether `self` conveys every authority in `needed`. + pub const fn contains(self, needed: Authority) -> bool { + (self.0 & needed.0) == needed.0 + } + + /// The union of two authority sets. + pub const fn union(self, other: Authority) -> Authority { + Authority(self.0 | other.0) + } + + /// Whether this set is empty. + pub const fn is_empty(self) -> bool { + self.0 == 0 + } +} + +/// An immutable authority record. Private fields, no setters: rights are fixed +/// when [`AuthorityWall::issue`] mints the capability. +#[derive(Clone, Copy, Debug)] +struct CapRecord { + domain: DomainId, + authority: Authority, + epoch: u64, +} + +/// The opaque token a domain holds as proof of authority. Possession *is* the +/// proof. Cheap to copy and store. +/// +/// It pairs a generation-checked [`Handle`] with the wall's monotonic issue +/// `epoch`. Both are compared on every check, so a slot reuse — even an +/// astronomically unlikely `u32` generation *wrap* — can never make a stale token +/// alias a new occupant: the `u64` epoch never repeats. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct CapToken { + handle: Handle, + epoch: u64, +} + +/// The wall is at capacity; no further capability can be issued until one is +/// revoked. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct AtCapacity; + +/// Why a [`delegate`](AuthorityWall::delegate) was refused. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DelegateError { + /// The delegator's token is forged/revoked, lacks [`Authority::DELEGATE`], or + /// the requested subset exceeds the authority the delegator holds. You cannot + /// delegate authority you do not have, nor without the delegation right. + Unauthorized, + /// The wall is at capacity. + AtCapacity, +} + +/// A capability-authority wall holding up to `N` live capabilities. +/// +/// Dumb mechanism only — it issues, checks, and revokes. It holds **no policy** +/// about who deserves a capability; that is the orchestrator's job, expressed by +/// *which* `(domain, authority)` pairs it chooses to [`issue`](Self::issue) and to +/// *whom* it hands the returned token. +#[derive(Clone)] +pub struct AuthorityWall { + caps: HandleTable, + epoch: SeqCounter, +} + +impl Default for AuthorityWall { + fn default() -> Self { + Self::new() + } +} + +impl AuthorityWall { + /// Create an empty wall (no capabilities issued). + pub fn new() -> Self { + Self { + caps: HandleTable::new(), + epoch: SeqCounter::new(), + } + } + + /// Orchestrator mint: issue an immutable capability granting `authority` to + /// `domain`, returning the token to hand to that domain. Possession of the + /// returned [`CapToken`] is the sole proof of the authority — no identity is + /// asserted. Fails with [`AtCapacity`] if the wall is full. + pub fn issue( + &mut self, + domain: DomainId, + authority: Authority, + ) -> Result { + let epoch = self.epoch.next_point(); + match self.caps.alloc(CapRecord { + domain, + authority, + epoch, + }) { + Ok(handle) => Ok(CapToken { handle, epoch }), + Err(_) => Err(AtCapacity), + } + } + + /// Primitive check (O(1), immutable read): does `token` currently authorize + /// `needed` for `domain`? Returns `false` for a forged token, a token bound to + /// a different domain, a revoked (stale) token, or insufficient authority. The + /// record's rights are read, never mutated. + pub fn authorizes(&self, token: CapToken, domain: DomainId, needed: Authority) -> bool { + match self.caps.get(token.handle) { + Some(record) => { + record.epoch == token.epoch + && record.domain == domain + && record.authority.contains(needed) + } + None => false, + } + } + + /// The `(domain, authority, epoch)` a live token conveys, for audit/tracing. + /// `None` if the token is forged or revoked. + pub fn inspect(&self, token: CapToken) -> Option<(DomainId, Authority, u64)> { + self.caps + .get(token.handle) + .filter(|record| record.epoch == token.epoch) + .map(|record| (record.domain, record.authority, record.epoch)) + } + + /// Revoke the capability behind `token` by *destroying* its record. The slot's + /// generation is bumped, so every outstanding copy of the token stops + /// authorizing immediately. Returns `true` if a live capability was revoked, + /// `false` if the token was already stale/forged. There is no rights + /// downgrade: to reduce a domain's authority, revoke and re-issue. + pub fn revoke(&mut self, token: CapToken) -> bool { + // Match handle AND epoch before destroying, so a stale token can never + // revoke a reused slot's new occupant. + if self + .caps + .get(token.handle) + .is_some_and(|r| r.epoch == token.epoch) + { + self.caps.remove(token.handle).is_some() + } else { + false + } + } + + /// Delegate a **subset** of authority to another domain — the edge that turns + /// flat grants into a delegation graph. + /// + /// The `delegator` must hold a live capability conveying both + /// [`Authority::DELEGATE`] *and* every authority in `subset`: you cannot + /// delegate more than you hold, nor without the delegation right. On success a + /// fresh, **independent** capability is minted for `to_domain` (revoking it + /// does not touch the delegator's, and vice-versa — cascade is a policy the + /// orchestrator layers on top, e.g. a graph min-cut). Identity stays derived: + /// the delegatee proves its authority by holding the returned token. + pub fn delegate( + &mut self, + delegator: CapToken, + to_domain: DomainId, + subset: Authority, + ) -> Result { + // Copy the delegator's authority out so the immutable borrow ends before + // the mutable mint below. + let held = match self.caps.get(delegator.handle) { + Some(record) if record.epoch == delegator.epoch => record.authority, + _ => return Err(DelegateError::Unauthorized), + }; + if !held.contains(Authority::DELEGATE) || !held.contains(subset) { + return Err(DelegateError::Unauthorized); + } + self.issue(to_domain, subset) + .map_err(|_| DelegateError::AtCapacity) + } + + /// Number of live capabilities. + pub const fn live(&self) -> usize { + self.caps.len() + } + + /// Maximum number of concurrent live capabilities. + pub const fn capacity(&self) -> usize { + self.caps.capacity() + } + + /// Whether the wall can issue no further capability without a revoke. + pub const fn is_full(&self) -> bool { + self.caps.is_full() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ROOT: DomainId = DomainId(0); + const DRIVER: DomainId = DomainId(2); + + #[test] + fn issued_token_authorizes_only_its_grant() { + let mut wall: AuthorityWall<8> = AuthorityWall::new(); + let token = wall + .issue(DRIVER, Authority::CLAIM_IRQ.union(Authority::MAP_MEMORY)) + .unwrap(); + + // Conveyed authorities pass, individually and together. + assert!(wall.authorizes(token, DRIVER, Authority::CLAIM_IRQ)); + assert!(wall.authorizes(token, DRIVER, Authority::MAP_MEMORY)); + assert!(wall.authorizes( + token, + DRIVER, + Authority::CLAIM_IRQ.union(Authority::MAP_MEMORY) + )); + + // An authority it was NOT granted fails. + assert!(!wall.authorizes(token, DRIVER, Authority::CREATE_SCHEME)); + } + + #[test] + fn token_is_bound_to_its_domain() { + let mut wall: AuthorityWall<8> = AuthorityWall::new(); + let token = wall.issue(DRIVER, Authority::SPAWN).unwrap(); + + // Presenting the driver's token to authorize a different domain fails: + // identity is derived from the capability, not asserted by the caller. + assert!(wall.authorizes(token, DRIVER, Authority::SPAWN)); + assert!(!wall.authorizes(token, ROOT, Authority::SPAWN)); + } + + #[test] + fn revocation_is_destruction_and_invalidates_every_copy() { + let mut wall: AuthorityWall<8> = AuthorityWall::new(); + let token = wall.issue(ROOT, Authority::ALL).unwrap(); + let copy = token; // a second holder of the same token + + assert!(wall.authorizes(token, ROOT, Authority::CREATE_SCHEME)); + assert!(wall.revoke(token)); + + // Both the original and the copy stop authorizing at once. + assert!(!wall.authorizes(token, ROOT, Authority::CREATE_SCHEME)); + assert!(!wall.authorizes(copy, ROOT, Authority::CREATE_SCHEME)); + // A second revoke is a no-op. + assert!(!wall.revoke(copy)); + assert_eq!(wall.live(), 0); + } + + #[test] + fn reissued_slot_does_not_revive_an_old_token() { + let mut wall: AuthorityWall<1> = AuthorityWall::new(); + let old = wall.issue(ROOT, Authority::ALL).unwrap(); + assert!(wall.revoke(old)); + + // The single slot is reused for a new, narrower grant. + let new = wall.issue(DRIVER, Authority::CLAIM_IRQ).unwrap(); + + // The stale token must not authorize against the new occupant, even though + // it indexes the same slot — the generation differs. + assert!(!wall.authorizes(old, ROOT, Authority::CREATE_SCHEME)); + assert!(!wall.authorizes(old, DRIVER, Authority::CLAIM_IRQ)); + assert!(wall.authorizes(new, DRIVER, Authority::CLAIM_IRQ)); + } + + #[test] + fn forged_token_authorizes_nothing() { + let real: AuthorityWall<4> = AuthorityWall::new(); + // A token cannot be constructed outside this module without the wall + // issuing it; even a default-looking handle resolves to nothing. + let mut minted: AuthorityWall<4> = AuthorityWall::new(); + let token = minted.issue(ROOT, Authority::ALL).unwrap(); + // The same token value means nothing against a *different* wall instance. + assert!(!real.authorizes(token, ROOT, Authority::CREATE_SCHEME)); + } + + #[test] + fn capacity_is_bounded_and_epochs_are_monotonic() { + let mut wall: AuthorityWall<2> = AuthorityWall::new(); + let a = wall.issue(ROOT, Authority::ALL).unwrap(); + let b = wall.issue(DRIVER, Authority::SPAWN).unwrap(); + assert!(wall.is_full()); + assert_eq!(wall.issue(DRIVER, Authority::SPAWN), Err(AtCapacity)); + + // Epoch (issue provenance) strictly increases. + let (_, _, ea) = wall.inspect(a).unwrap(); + let (_, _, eb) = wall.inspect(b).unwrap(); + assert!(eb > ea); + + // Freeing one makes room again. + assert!(wall.revoke(a)); + assert!(!wall.is_full()); + let c = wall.issue(DRIVER, Authority::CLAIM_IRQ).unwrap(); + let (_, _, ec) = wall.inspect(c).unwrap(); + assert!(ec > eb); + } + + #[test] + fn authority_bitset_is_immutable_and_truncating() { + let full = Authority::ALL; + assert!(full.contains(Authority::CREATE_SCHEME)); + assert!(full.contains(Authority::DELEGATE)); + assert!(Authority::NONE.is_empty()); + // Unknown bits are dropped, so a holder cannot smuggle undefined authority. + assert_eq!(Authority::from_bits_truncate(0xFFFF_FFFF), Authority::ALL); + } + + #[test] + fn delegation_needs_the_right_and_cannot_exceed_what_is_held() { + const THIRD: DomainId = DomainId(3); + let mut wall: AuthorityWall<8> = AuthorityWall::new(); + + // A domain with CLAIM_IRQ but NOT delegate cannot hand it on. + let no_delegate = wall.issue(DRIVER, Authority::CLAIM_IRQ).unwrap(); + assert_eq!( + wall.delegate(no_delegate, THIRD, Authority::CLAIM_IRQ), + Err(DelegateError::Unauthorized) + ); + + // A delegator with DELEGATE + CLAIM_IRQ may delegate CLAIM_IRQ... + let delegator = wall + .issue(DRIVER, Authority::DELEGATE.union(Authority::CLAIM_IRQ)) + .unwrap(); + assert!(wall + .delegate(delegator, THIRD, Authority::CLAIM_IRQ) + .is_ok()); + + // ...but NOT an authority it does not itself hold. + assert_eq!( + wall.delegate(delegator, THIRD, Authority::CREATE_SCHEME), + Err(DelegateError::Unauthorized) + ); + } + + #[test] + fn delegated_capability_is_independent_of_the_delegator() { + const THIRD: DomainId = DomainId(3); + let mut wall: AuthorityWall<8> = AuthorityWall::new(); + let delegator = wall.issue(ROOT, Authority::ALL).unwrap(); + + let delegated = wall + .delegate(delegator, THIRD, Authority::CLAIM_IRQ) + .unwrap(); + // The delegatee holds exactly the delegated subset, bound to its own domain. + assert!(wall.authorizes(delegated, THIRD, Authority::CLAIM_IRQ)); + assert!(!wall.authorizes(delegated, THIRD, Authority::CREATE_SCHEME)); + assert!(!wall.authorizes(delegated, ROOT, Authority::CLAIM_IRQ)); + + // Revoking the delegator does NOT revoke the delegatee (cascade is an + // orchestrator policy, e.g. a graph min-cut — not automatic here). + assert!(wall.revoke(delegator)); + assert!(wall.authorizes(delegated, THIRD, Authority::CLAIM_IRQ)); + } + + // ----- Bounded exhaustive model-check ------------------------------------ + // + // Beyond the spot tests above, drive the *real* wall through every reachable + // sequence of operations (issue / revoke / delegate) up to a bounded depth + // over a small finite universe, and at every state assert the wall's + // `authorizes` exactly matches a ground-truth reference model. This proves + // the security invariants — no-forge, immutability, domain-binding, + // revocation-completeness (incl. slot/generation reuse), and the delegation + // bound — hold across all those interleavings, not just the hand-picked ones. + + #[derive(Clone)] + struct RefCap { + token: CapToken, + domain: DomainId, + authority: Authority, + alive: bool, + } + + #[allow(clippy::too_many_arguments)] + fn explore( + wall: AuthorityWall, + refs: Vec, + depth: usize, + domains: &[DomainId], + auths: &[Authority], + needs: &[Authority], + budget: &mut u64, + explored: &mut u64, + ) { + // Ground-truth check: the wall agrees with the reference model for every + // issued token, every domain, and every queried authority. + for r in &refs { + for &dd in domains { + for &need in needs { + let expected = r.alive && dd == r.domain && r.authority.contains(need); + assert_eq!( + wall.authorizes(r.token, dd, need), + expected, + "authorizes mismatch (tok dom={:?} auth={:?} alive={}; ask dom={:?} need={:?})", + r.domain, r.authority, r.alive, dd, need + ); + } + } + } + *explored += 1; + if depth == 0 || *budget == 0 { + return; + } + *budget -= 1; + + // issue(domain, authority) + for &d in domains { + for &a in auths { + let mut w = wall.clone(); + let mut rs = refs.clone(); + if let Ok(t) = w.issue(d, a) { + rs.push(RefCap { + token: t, + domain: d, + authority: a, + alive: true, + }); + } + explore(w, rs, depth - 1, domains, auths, needs, budget, explored); + if *budget == 0 { + return; + } + } + } + // revoke(token) — every live capability + for i in 0..refs.len() { + if !refs[i].alive { + continue; + } + let mut w = wall.clone(); + let mut rs = refs.clone(); + assert!(w.revoke(rs[i].token), "revoke of a live token must succeed"); + rs[i].alive = false; + explore(w, rs, depth - 1, domains, auths, needs, budget, explored); + if *budget == 0 { + return; + } + } + // delegate(token, to_domain, subset) + for i in 0..refs.len() { + if !refs[i].alive { + continue; + } + for &to in domains { + for &subset in auths { + let mut w = wall.clone(); + let mut rs = refs.clone(); + let held = rs[i].authority; + match w.delegate(rs[i].token, to, subset) { + Ok(nt) => { + assert!( + held.contains(Authority::DELEGATE) && held.contains(subset), + "delegate succeeded without DELEGATE or beyond held authority" + ); + rs.push(RefCap { + token: nt, + domain: to, + authority: subset, + alive: true, + }); + } + Err(DelegateError::Unauthorized) => { + assert!( + !(held.contains(Authority::DELEGATE) && held.contains(subset)), + "delegate refused despite sufficient authority" + ); + } + Err(DelegateError::AtCapacity) => {} + } + explore(w, rs, depth - 1, domains, auths, needs, budget, explored); + if *budget == 0 { + return; + } + } + } + } + } + + #[test] + fn bounded_exhaustive_invariant_verification() { + const DOMAINS: [DomainId; 2] = [DomainId(7), DomainId(8)]; + // Issued/delegated authority shapes, incl. a DELEGATE-bearing one. + const AUTHS: [Authority; 3] = [ + Authority::CLAIM_IRQ, + Authority::CREATE_SCHEME, + Authority::DELEGATE.union(Authority::CLAIM_IRQ), + ]; + // Authorities queried against, incl. one never issued (`MAP_MEMORY`). + const NEEDS: [Authority; 5] = [ + Authority::NONE, + Authority::CLAIM_IRQ, + Authority::CREATE_SCHEME, + Authority::DELEGATE, + Authority::MAP_MEMORY, + ]; + const DEPTH: usize = 5; + + let mut budget: u64 = 800_000; + let mut explored: u64 = 0; + let wall: AuthorityWall<4> = AuthorityWall::new(); + explore( + wall, + Vec::new(), + DEPTH, + &DOMAINS, + &AUTHS, + &NEEDS, + &mut budget, + &mut explored, + ); + // Confirm we actually explored a large state space, not a trivial one. + assert!(explored > 50_000, "explored too few states: {explored}"); + std::eprintln!( + "bounded-exhaustive: {explored} reachable states verified to depth {DEPTH}, no invariant violated" + ); + } + + // Regression for the two orchestrator-policy bugs an adversarial audit found + // in `kernel::security::SecurityState`: a revoke that did not stick, and a + // `uid` that was not re-checked for a known domain (pid-reuse escalation). + // This mirrors the *fixed* policy over the REAL `AuthorityWall`; keep it in + // sync with `src/security/mod.rs`. + #[test] + fn orchestrator_gate_is_sound_under_revoke_and_uid_recheck() { + use std::collections::BTreeMap; + type Map = BTreeMap>; + + fn grant(w: &mut AuthorityWall<8>, m: &mut Map, d: DomainId, a: Authority) { + if let Some(Some(old)) = m.insert(d, None) { + w.revoke(old); + } + if let Ok(t) = w.issue(d, a) { + m.insert(d, Some(t)); + } + } + fn authorizes(w: &AuthorityWall<8>, m: &Map, d: DomainId, need: Authority) -> bool { + matches!(m.get(&d), Some(Some(t)) if w.authorizes(*t, d, need)) + } + fn revoke(w: &mut AuthorityWall<8>, m: &mut Map, d: DomainId) { + match m.get_mut(&d) { + Some(slot) => { + if let Some(t) = slot.take() { + w.revoke(t); + } + } + None => { + m.insert(d, None); + } + } + } + fn forget(w: &mut AuthorityWall<8>, m: &mut Map, d: DomainId) { + if let Some(Some(t)) = m.remove(&d) { + w.revoke(t); + } + } + fn gate(w: &mut AuthorityWall<8>, m: &mut Map, d: DomainId, uid: u32) -> bool { + if uid != 0 { + return false; + } + match m.get(&d) { + None => grant(w, m, d, Authority::CREATE_SCHEME), + Some(None) => return false, + Some(Some(_)) => {} + } + authorizes(w, m, d, Authority::CREATE_SCHEME) + } + + let mut w: AuthorityWall<8> = AuthorityWall::new(); + let mut m: Map = BTreeMap::new(); + let p = DomainId(7); + + // First contact at uid 0 -> granted (behaviour preserved). + assert!(gate(&mut w, &mut m, p, 0)); + // BUG B fixed: the SAME known domain at uid != 0 is denied (uid re-checked). + assert!(!gate(&mut w, &mut m, p, 1000)); + // BUG A fixed: an operator revoke STICKS — uid 0 is still denied afterward. + revoke(&mut w, &mut m, p); + assert!(!gate(&mut w, &mut m, p, 0)); + // A dead pid is forgotten -> a reuse at uid 0 starts fresh (granted again). + forget(&mut w, &mut m, p); + assert!(gate(&mut w, &mut m, p, 0)); + // A never-seen domain at uid != 0 is denied. + assert!(!gate(&mut w, &mut m, DomainId(9), 1000)); + } +} diff --git a/driver-primitives/src/handle.rs b/driver-primitives/src/handle.rs index af0e4c62..99ee9fe8 100644 --- a/driver-primitives/src/handle.rs +++ b/driver-primitives/src/handle.rs @@ -27,12 +27,14 @@ impl Handle { } } +#[derive(Clone)] struct Slot { generation: u32, value: Option, } /// A fixed-capacity table mapping opaque [`Handle`]s to values of type `T`. +#[derive(Clone)] pub struct HandleTable { slots: [Slot; N], /// Stack of free slot indices. diff --git a/driver-primitives/src/lib.rs b/driver-primitives/src/lib.rs index af2a0fbb..5d0f4b47 100644 --- a/driver-primitives/src/lib.rs +++ b/driver-primitives/src/lib.rs @@ -56,6 +56,7 @@ //! no locks — synchronization and the OS-facing glue (MMIO/DMA mapping, IRQ //! delivery) belong to the orchestrator that wraps them. +pub mod authority; pub mod capability; pub mod display; pub mod dma; @@ -64,6 +65,7 @@ pub mod fairqueue; pub mod feature; pub mod fence; pub mod handle; +pub mod mincut; pub mod mmio; pub mod power; pub mod reactor; @@ -72,6 +74,7 @@ pub mod runtime; pub mod txn; pub mod virtio; +pub use authority::{AtCapacity, Authority, AuthorityWall, CapToken, DelegateError, DomainId}; pub use capability::{CapabilityRing, Grant}; pub use display::{Display, Mode}; pub use dma::{Descriptor, DmaRegion}; @@ -80,6 +83,7 @@ pub use fairqueue::FairQueue; pub use feature::Features; pub use fence::{Fence, SeqCounter}; pub use handle::{Handle, HandleTable}; +pub use mincut::MinCutGraph; pub use mmio::{Bank, Reg}; pub use power::{Governor, PowerBudget, PowerManager, SensorEvent, Sensors}; pub use reactor::{Completion, Reactor, SubmissionId, Ticket}; diff --git a/driver-primitives/src/mincut.rs b/driver-primitives/src/mincut.rs new file mode 100644 index 00000000..93e8bbe0 --- /dev/null +++ b/driver-primitives/src/mincut.rs @@ -0,0 +1,234 @@ +//! Global minimum cut (Stoer–Wagner) — a graph-partition primitive. +//! +//! The cheapest way to split a weighted graph into two non-empty parts: the set +//! of edges whose total weight is smallest among all such splits. It is the +//! cross-domain optimization core that bridges into security (the *minimal* set +//! of authority edges to cut to isolate a compromised domain — quarantine with +//! the least collateral), resource placement (where to cut a dataflow across +//! GPUs so the least data crosses), and clustering. +//! +//! Like every primitive here it is **dumb mechanism, zero policy**: it computes a +//! cut weight (and, on request, which side each vertex lands on) over a +//! fixed-capacity weighted undirected graph. *What* the vertices and weights mean +//! — domains and delegated authority, kernels and buffer bytes — is the +//! orchestrator's composition. +//! +//! `no_std`, allocation-free (an `N×N` weight matrix via const generics), no +//! locks. Stoer–Wagner runs in `O(V³)`; keep `N` modest (a control-plane +//! analysis, never a hot path). + +/// A weighted undirected graph of up to `N` vertices, for global min-cut. +#[derive(Clone)] +pub struct MinCutGraph { + w: [[u64; N]; N], + n: usize, +} + +impl MinCutGraph { + /// A graph with `n` vertices (`0..n`) and no edges. Panics in debug if `n > N`. + pub fn new(n: usize) -> Self { + debug_assert!(n <= N); + Self { + w: [[0; N]; N], + n: if n <= N { n } else { N }, + } + } + + /// Number of vertices. + pub const fn vertices(&self) -> usize { + self.n + } + + /// Add `weight` to the undirected edge `{a, b}` (parallel edges accumulate). + /// Self-loops and out-of-range endpoints are ignored. + pub fn add_edge(&mut self, a: usize, b: usize, weight: u64) { + if a == b || a >= self.n || b >= self.n { + return; + } + self.w[a][b] = self.w[a][b].saturating_add(weight); + self.w[b][a] = self.w[b][a].saturating_add(weight); + } + + /// The weight of the global minimum cut. Returns 0 for a graph with fewer than + /// two vertices (nothing to separate). + pub fn min_cut(&self) -> u64 { + self.min_cut_inner(None) + } + + /// The global minimum cut, also recording into `side` which partition each + /// vertex lands on (`true` = the lighter "cut-off" side). `side` must be at + /// least `vertices()` long; extra entries are left untouched. + pub fn min_cut_partition(&self, side: &mut [bool]) -> u64 { + for s in side.iter_mut() { + *s = false; + } + self.min_cut_inner(Some(side)) + } + + fn min_cut_inner(&self, mut side: Option<&mut [bool]>) -> u64 { + let n = self.n; + if n < 2 { + return 0; + } + + // Working copy: merges accumulate weights into a surviving vertex. + let mut w = self.w; + // `group[v]` lists the original vertices merged into surviving vertex `v`. + // Tracked as a parent-less membership: members[v] is a bitset would need + // alloc; instead keep a per-vertex "merged-away" flag and, when partition + // output is wanted, the set of originals folded into the phase's last + // vertex via `co[v]` chains. + let mut merged = [false; N]; + // co[v] = next original vertex in v's merged chain, or usize::MAX. + let mut co_next = [usize::MAX; N]; + // co_head[v] = first original in v's chain (== v initially). + let co_head: [usize; N] = core::array::from_fn(|i| i); + + let mut best = u64::MAX; + let mut active = n; + + while active > 1 { + let mut in_a = [false; N]; + let mut conn = [0u64; N]; + let mut prev = usize::MAX; + let mut last = usize::MAX; + let mut cut_of_phase = 0u64; + + for i in 0..active { + // Most tightly connected vertex not yet in A. + let mut sel = usize::MAX; + for v in 0..n { + if !merged[v] && !in_a[v] && (sel == usize::MAX || conn[v] > conn[sel]) { + sel = v; + } + } + if sel == usize::MAX { + break; + } + if i == active - 1 { + cut_of_phase = conn[sel]; + } + in_a[sel] = true; + prev = last; + last = sel; + for v in 0..n { + if !merged[v] && !in_a[v] { + conn[v] = conn[v].saturating_add(w[sel][v]); + } + } + } + + if cut_of_phase < best { + best = cut_of_phase; + if let Some(side) = side.as_deref_mut() { + // The cut-of-the-phase isolates `last` (and everything merged + // into it) from the rest. Record that side. + for s in side.iter_mut().take(n) { + *s = false; + } + let mut cur = co_head[last]; + while cur != usize::MAX { + if cur < side.len() { + side[cur] = true; + } + cur = co_next[cur]; + } + } + } + + // Merge `last` into `prev`. + if prev != usize::MAX && last != usize::MAX { + merged[last] = true; + // Indexes row `prev`/`last` and column `prev`/`last` of `w` by the + // same `v`; the matrix layout makes an iterator no clearer. + #[allow(clippy::needless_range_loop)] + for v in 0..n { + w[prev][v] = w[prev][v].saturating_add(w[last][v]); + w[v][prev] = w[v][prev].saturating_add(w[v][last]); + } + // Splice last's original-vertex chain onto prev's. + let mut tail = co_head[prev]; + while co_next[tail] != usize::MAX { + tail = co_next[tail]; + } + co_next[tail] = co_head[last]; + active -= 1; + } else { + break; + } + } + + if best == u64::MAX { + 0 + } else { + best + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_edge_cut_is_its_weight() { + let mut g: MinCutGraph<2> = MinCutGraph::new(2); + g.add_edge(0, 1, 7); + assert_eq!(g.min_cut(), 7); + } + + #[test] + fn disconnected_graph_has_zero_cut() { + let mut g: MinCutGraph<4> = MinCutGraph::new(4); + g.add_edge(0, 1, 5); + // vertices 2,3 are isolated -> cutting them off costs nothing. + assert_eq!(g.min_cut(), 0); + } + + #[test] + fn fewer_than_two_vertices_is_zero() { + assert_eq!(MinCutGraph::<4>::new(1).min_cut(), 0); + assert_eq!(MinCutGraph::<4>::new(0).min_cut(), 0); + } + + #[test] + fn classic_stoer_wagner_example_is_four() { + // The 8-vertex graph from Stoer & Wagner (1997); global min cut = 4. + let mut g: MinCutGraph<8> = MinCutGraph::new(8); + for &(a, b, c) in &[ + (0, 1, 2), + (0, 4, 3), + (1, 2, 3), + (1, 4, 2), + (1, 5, 2), + (2, 3, 4), + (2, 6, 2), + (3, 6, 2), + (3, 7, 2), + (4, 5, 3), + (5, 6, 1), + (6, 7, 3), + ] { + g.add_edge(a, b, c); + } + assert_eq!(g.min_cut(), 4); + } + + #[test] + fn partition_isolates_a_cheaply_cut_vertex() { + // A triangle {0,1,2} densely tied, vertex 3 hangs off 2 by a single light + // edge — the min cut (weight 1) must put 3 alone on one side. + let mut g: MinCutGraph<4> = MinCutGraph::new(4); + g.add_edge(0, 1, 5); + g.add_edge(1, 2, 5); + g.add_edge(0, 2, 5); + g.add_edge(2, 3, 1); + let mut side = [false; 4]; + assert_eq!(g.min_cut_partition(&mut side), 1); + // Exactly one of {3} / {0,1,2} is the cut-off side; vertex 3 is alone. + let cut_off = side[3]; + assert_ne!(side[0], cut_off); + assert_ne!(side[1], cut_off); + assert_ne!(side[2], cut_off); + } +} diff --git a/src/main.rs b/src/main.rs index 41577273..bcaaf1ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,9 @@ mod profiling; /// Schemes, filesystem handlers mod scheme; +/// Capability-based security wall (orchestrator over `driver_primitives`) +mod security; + /// Early init mod startup; diff --git a/src/scheme/mod.rs b/src/scheme/mod.rs index 4dc63b3a..c40abe93 100644 --- a/src/scheme/mod.rs +++ b/src/scheme/mod.rs @@ -320,9 +320,13 @@ impl KernelScheme for SchemeList { return Err(Error::new(EINVAL)); } - if caller.uid != 0 { + // Scheme creation is authorized through the capability wall: a uid-0 + // caller is granted CREATE_SCHEME on first use (the legacy policy), but + // the authority is now recorded and revocable — a revoked domain is + // refused even at uid 0. Identity is structural (the caller's pid). + if !crate::security::authorize_scheme_create(caller.pid, caller.uid) { return Err(Error::new(EACCES)); - }; + } let context = Arc::downgrade(&context::current()); diff --git a/src/security/mod.rs b/src/security/mod.rs new file mode 100644 index 00000000..b109d1f2 --- /dev/null +++ b/src/security/mod.rs @@ -0,0 +1,167 @@ +//! Kernel security wall — the *orchestrator* side of [`AuthorityWall`]. +//! +//! `driver_primitives` provides the dumb mechanism: an immutable, +//! capability-to-construct authority table. The kernel is the **orchestrator** — +//! it owns the single wall instance, decides which domain receives which +//! authority, and consults the wall at the seams where a cross-domain operation +//! must be authorized. +//! +//! Doctrine held here: +//! +//! - Authority is an **immutable capability** *held* as a [`CapToken`]; the +//! bearer cannot widen or edit it. +//! - **Authorization is possession.** A domain's authority is whatever live +//! capability the orchestrator minted for it; a forged or revoked token is +//! rejected structurally by the table's generation check. +//! - **Revocation is destruction** (a generation bump), not a downgrade-in-place. +//! +//! Identity is **structural**: a domain is named by the kernel from the acting +//! context (its process id), never asserted by the caller. +//! +//! ## Enforcement: scheme creation +//! +//! [`authorize_scheme_create`] is the live gate for `SchemeList::kdup` +//! (`scheme::mod`). It is **behaviour-preserving** — a `uid == 0` caller is +//! granted `CREATE_SCHEME` on first use, exactly the legacy policy — but the +//! authority is now a *recorded, revocable capability*: once a domain is known to +//! the wall, a [`revoke`](AuthorityWall::revoke) sticks, so a compromised root +//! domain's scheme-creation power can be withdrawn at runtime, which the bare +//! `uid == 0` check could never do. [`on_context_exit`] destroys a dying domain's +//! capability so a reused pid starts from a clean slate. + +use alloc::collections::BTreeMap; + +use driver_primitives::{Authority, AuthorityWall, CapToken, DomainId}; +use spin::{Mutex, Once}; + +/// Maximum number of concurrent live authority capabilities, kernel-wide. +/// +/// Kept small on purpose: the kit's `HandleTable` is a fixed-capacity array, and +/// the wall is constructed during early boot on the kernel stack — a large array +/// here overflows that stack (a double fault in `kmain`). A microkernel has only +/// a modest number of *concurrently privileged* domains, and `on_context_exit` +/// frees a domain's slot as soon as it dies, so live usage stays well under this. +const WALL_CAPACITY: usize = 256; + +/// The bootstrap/root domain. +const ROOT_DOMAIN: DomainId = DomainId(0); + +/// The orchestrator's mutable security state: the capability wall (mechanism) +/// plus a domain index. A domain maps to one of three states, and the three are +/// kept distinct — conflating them is how the gate was unsound before: +/// - `Some(token)` — granted; holds the named capability; +/// - `None` — a **tombstone**: authority was explicitly revoked and must +/// not be re-granted while the domain stays alive; +/// - absent — never seen (first contact). +struct SecurityState { + wall: AuthorityWall, + domains: BTreeMap>, +} + +impl SecurityState { + fn new() -> Self { + Self { + wall: AuthorityWall::new(), + domains: BTreeMap::new(), + } + } + + /// Grant `domain` exactly `authority`, revoking any capability it previously + /// held and clearing any tombstone. + fn grant(&mut self, domain: DomainId, authority: Authority) { + if let Some(Some(old)) = self.domains.insert(domain, None) { + self.wall.revoke(old); + } + if let Ok(token) = self.wall.issue(domain, authority) { + self.domains.insert(domain, Some(token)); + } + } + + fn authorizes(&self, domain: DomainId, needed: Authority) -> bool { + match self.domains.get(&domain) { + Some(Some(token)) => self.wall.authorizes(*token, domain, needed), + _ => false, + } + } + + /// Operator withdrawal of a **live** domain's authority. Leaves a tombstone so + /// the withdrawal *sticks*: a later gate call for the same live domain is + /// denied, not silently re-granted. + fn revoke(&mut self, domain: DomainId) { + match self.domains.get_mut(&domain) { + Some(slot) => { + if let Some(token) = slot.take() { + self.wall.revoke(token); + } + } + None => { + self.domains.insert(domain, None); + } + } + } + + /// Forget a **dead** domain entirely (its pid is gone), so a process that later + /// reuses the pid starts from first contact rather than inheriting authority or + /// a tombstone. + fn forget(&mut self, domain: DomainId) { + if let Some(Some(token)) = self.domains.remove(&domain) { + self.wall.revoke(token); + } + } + + /// The scheme-creation gate. The legacy policy — only `uid == 0` may create a + /// scheme — is re-checked on **every** call, so a pid reused by a non-root + /// process cannot inherit a prior grant. First contact at uid 0 registers the + /// capability; a tombstoned (revoked) domain stays denied even at uid 0. + fn authorize_scheme_create(&mut self, domain: DomainId, uid: u32) -> bool { + if uid != 0 { + return false; + } + match self.domains.get(&domain) { + None => self.grant(domain, Authority::CREATE_SCHEME), // first contact + Some(None) => return false, // tombstoned: stays revoked + Some(Some(_)) => {} // already granted + } + self.authorizes(domain, Authority::CREATE_SCHEME) + } +} + +/// The single kernel security state, initialized once at boot by [`init`]. +static STATE: Once> = Once::new(); + +/// Initialize the security wall and grant the root domain its full authority. +/// Called once from `kmain`, before the bootstrap context is spawned. +pub fn init() { + let state = STATE.call_once(|| Mutex::new(SecurityState::new())); + state.lock().grant(ROOT_DOMAIN, Authority::ALL); +} + +/// Authorize scheme creation for the calling context (named structurally by +/// `pid`). The legacy `uid == 0` policy is preserved and re-checked every call, +/// but mediated through the revocable capability wall. Before the wall is +/// initialized the legacy policy applies directly. +#[must_use] +pub fn authorize_scheme_create(pid: usize, uid: u32) -> bool { + let domain = DomainId(pid as u64); + match STATE.get() { + Some(state) => state.lock().authorize_scheme_create(domain, uid), + None => uid == 0, + } +} + +/// Withdraw a live domain's authority and tombstone it so the withdrawal sticks. +/// The control-plane executor for a quarantine decision. +#[allow(dead_code)] // operator/quarantine entry point (wired incrementally) +pub fn revoke_domain(pid: usize) { + if let Some(state) = STATE.get() { + state.lock().revoke(DomainId(pid as u64)); + } +} + +/// Forget a dying context's domain so its authority cannot be inherited by a later +/// reuse of the same pid. Called from the context exit path. +pub fn on_context_exit(pid: usize) { + if let Some(state) = STATE.get() { + state.lock().forget(DomainId(pid as u64)); + } +} diff --git a/src/startup/mod.rs b/src/startup/mod.rs index 960eecde..04685ea0 100644 --- a/src/startup/mod.rs +++ b/src/startup/mod.rs @@ -163,6 +163,10 @@ pub(crate) fn kmain(bootstrap: Bootstrap) -> ! { //Initialize global schemes, such as `acpi:`. scheme::init_globals(); + // Initialize the capability security wall and mint the root domain's + // full-authority capability (held by the bootstrap context). + crate::security::init(); + debug!("BSP: {} CPUs", crate::cpu_count()); debug!("Env: {:?}", ::core::str::from_utf8(bootstrap.env)); diff --git a/src/syscall/process.rs b/src/syscall/process.rs index 123fe817..712e42eb 100644 --- a/src/syscall/process.rs +++ b/src/syscall/process.rs @@ -50,12 +50,15 @@ pub fn exit_this_context(excp: Option, token: &mut CleanLock } } // TODO: Should status == Status::HardBlocked be handled differently? - let owner = { + let (owner, dead_pid) = { let mut guard = context_lock.write(token.token()); guard.status = context::Status::Dead { excp }; - guard.owner_proc_id + (guard.owner_proc_id, guard.pid) }; + // Destroy the dying domain's scheme-creation capability so a reused pid does + // not inherit it. + crate::security::on_context_exit(dead_pid); if let Some(owner) = owner { event::trigger( GlobalSchemes::Proc.scheme_id(),