diff --git a/CHANGELOG.md b/CHANGELOG.md index 6908782..41d41d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,46 @@ Release codenames follow the six Noongar seasons — Birak, Bunuru, Djeran, Maku ### Added +- **The capability table itself — RFC-0003's second increment.** `setonix-capability` grows the owned + `Capability` value and the flat per-process `CapabilityTable` (§4, Option B): the structure every + future syscall resolves against. + - `Capability` is **owned and deliberately neither `Clone` nor `Copy`** (§6): transfer between + tables is a Rust move, and the sole duplication is `derive` — explicit, rights-checked, + subset-only (O-2). A `compile_fail` doctest (pinned to E0277, instantiated with a real + `ObjectRef` type) guards the absence of `Clone` on every test run, rather than asserting it in + prose; the type is also `#[must_use]`, since dropping a capability is a close and must never + happen by accident. Minting reads the generation from the object itself, so a capability can + never be back-dated to a generation its object no longer occupies. + - `CapabilityTable` — a fixed-capacity flat array (the kernel has no allocator; bounded by + construction), free-list recycling, O(1) everything. Resolution is one index plus two generation + checks: slot against handle (the ABA defence, O-1) and object against capability (destruction + revocation, O-3). A slot's generation is bumped **when it is vacated**, so a closed handle dies + at the instant of closing, not merely when its slot is reused; a slot whose generation cannot + advance is **retired** outright — capacity is the price of failing closed. All three of the RFC + amendment's load-bearing invariants are now *stated where they bind*: a resolve yields a borrow, + never a copy, and the caller holds that borrow across its whole check→act window — the property + any future multi-core synchronisation story must preserve, documented on `resolve` and in the + `ObjectRef` contract rather than assumed. + - The trait bounds carry the design: `insert`/`remove` are object-blind slot mechanics (the + plumbing a transfer is built from), while `resolve`/`derive` require the new `ObjectRef` trait — + one method, *what generation is the object at now* — which is all the table ever asks of a + kernel object. The kernel crate implements it when kernel objects exist; a test double implements + it today, which is what keeps the whole scheme host-testable. + - A full table hands the capability **back** in the error rather than dropping it: destroying + in-flight authority because a receiver had no room would turn a resource limit into silent + revocation. + - Twenty-seven new host tests, hardened by an adversarial multi-lens review with mutation testing + (every surviving mutant found got a test that kills it). Among them: an exhaustive forged-handle + sweep (only the exact minted handle resolves — for both `resolve` *and* `remove`, so a dangling + handle can neither use nor steal a reused slot's occupant); object destruction making parent and + derived child inert with no list of holders, while a *removed* parent leaves its derived sibling + untouched (the flat table has no parent link — pinned so RFC-0003a cannot regress it silently); + generation-exhaustion retirement alone and amid live neighbours; free-list LIFO order; + transfer-as-move between two tables; the two defensive fail-closed branches driven by + deliberately corrupted private state; and an 8192-operation churn test against a shadow model — + inserts, subset-random derivations, object destructions, object-blind cleanup and removals + interleaved — in which no dead handle ever resolves or removes anything. The lifecycle is also a + running doctest. - **The capability table begins — RFC-0003 turns into code, first increment.** A new `no_std` workspace crate, `setonix-capability`, holding the pure, architecture-independent logic of the capability spine so it can be **host-unit-tested** — a bare-metal target has no test harness, so the diff --git a/capability/src/capability.rs b/capability/src/capability.rs new file mode 100644 index 0000000..6cb04ba --- /dev/null +++ b/capability/src/capability.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The capability itself — an owned, unclonable grant of authority. + +use crate::{CapabilityError, Generation, ObjectRef, Rights}; +use core::fmt; + +/// A capability: a kernel-held reference to a kernel object, together with the +/// rights this reference carries and the object generation it was minted at +/// (RFC-0003 §3). +/// +/// The type is **owned and deliberately neither `Clone` nor `Copy`** (§6). A +/// capability is in exactly one table, or in flight in exactly one message — +/// moving it out of one table and into another is a Rust move, and the borrow +/// checker forbids the value existing in two places. The one way to get a +/// second capability to an object is [`derive`](Self::derive): an explicit, +/// rights-checked operation, never an implicit copy. +/// +/// Per the RFC-0003 prior-art amendment, that compile-time guarantee covers +/// the kernel's *internal* handling. The userspace-observable cross-process +/// transfer is a runtime table operation; the generation check is what secures +/// it. +/// +/// ```compile_fail,E0277 +/// // A capability cannot be cloned — duplication is `derive`. The object +/// // type implements `ObjectRef` so this guards the instantiation the +/// // kernel will actually use: any `Clone` impl on `Capability`, however +/// // bounded, would make this compile and the test fail. +/// use setonix_capability::{Capability, Generation, ObjectRef}; +/// +/// #[derive(Clone)] +/// struct Object; +/// impl ObjectRef for Object { +/// fn current_generation(&self) -> Generation { +/// Generation::FIRST +/// } +/// } +/// +/// fn requires_clone() {} +/// requires_clone::>(); +/// ``` +#[must_use = "dropping a capability closes it — move it into a table, or drop it deliberately"] +pub struct Capability { + object: O, + rights: Rights, + generation: Generation, +} + +impl Capability { + /// Mint a capability to `object`, carrying `rights`, at the object's + /// **current** generation. + /// + /// Reading the generation from the object rather than taking it as a + /// parameter means a mint can never be back-dated: there is no way to + /// construct a capability at a generation the object no longer occupies. + /// The complementary discipline is the kernel's — mint only at object + /// creation, or from a grant path that has established the object is + /// live, because a mint always grants authority to the object's *current* + /// incarnation. Everything downstream of the first grant should go + /// through [`derive`](Self::derive), which refuses a stale parent. + pub fn mint(object: O, rights: Rights) -> Self { + let generation = object.current_generation(); + Self { + object, + rights, + generation, + } + } + + /// Derive an attenuated child capability — the only duplication there is. + /// + /// The child references the same object (cloning the counted reference, + /// which duplicates no authority), carries exactly `requested`, and is + /// minted at the same generation as its live parent. + /// + /// # Errors + /// + /// Checked in this order, each failing closed with `self` untouched: + /// + /// - [`CapabilityError::StaleGeneration`] — `self` no longer matches its + /// object's generation. A stale capability is inert for *every* + /// operation, derivation included, so this is checked before anything + /// else. + /// - [`CapabilityError::NotDuplicable`] — `self` lacks + /// [`Rights::DUPLICATE`]: a leaf, from which nothing may be derived + /// (RFC-0003 §5). + /// - [`CapabilityError::RightsNotSubset`] — `requested` asks for a right + /// `self` does not hold, refused by [`Rights::diminish`]. O-2: no + /// derivation chain ever widens. + pub fn derive(&self, requested: Rights) -> Result { + if self.object.current_generation() != self.generation { + return Err(CapabilityError::StaleGeneration); + } + if !self.rights.contains(Rights::DUPLICATE) { + return Err(CapabilityError::NotDuplicable); + } + let rights = self + .rights + .diminish(requested) + .ok_or(CapabilityError::RightsNotSubset)?; + Ok(Self { + object: self.object.clone(), + rights, + generation: self.generation, + }) + } +} + +impl Capability { + /// The rights this capability carries. + #[must_use] + pub const fn rights(&self) -> Rights { + self.rights + } + + /// The object generation this capability was minted at. Compared against + /// the object's current generation on every resolve; destroying the + /// object bumps its generation and leaves this value behind, which is + /// what makes the capability inert (the destruction half of O-3). + #[must_use] + pub const fn generation(&self) -> Generation { + self.generation + } + + /// The referenced object. + #[must_use] + pub const fn object(&self) -> &O { + &self.object + } +} + +impl fmt::Debug for Capability { + /// The object reference is deliberately omitted: what a kernel object + /// looks like inside is not this crate's to print, and diagnostics must + /// not become a side channel. Omitting it also spares `O` a `Debug` + /// bound. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Capability") + .field("rights", &self.rights) + .field("generation", &self.generation) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)] +mod tests { + use crate::test_support::TestObject; + use crate::{Capability, CapabilityError, Generation, Rights}; + + #[test] + fn mint_records_rights_and_the_objects_current_generation() { + let object = TestObject::new(1); + // Advance the object to a second incarnation first, to prove the mint + // reads the current generation rather than assuming the first. + object.destroy(); + let generation_two = Generation::FIRST.next().expect("generation 2 exists"); + let capability = Capability::mint(object, Rights::READ); + assert_eq!(capability.rights(), Rights::READ); + assert_eq!(capability.generation(), generation_two); + assert_eq!(capability.object().id(), 1); + } + + #[test] + fn derive_attenuates_and_references_the_same_object() { + let parent = Capability::mint(TestObject::new(7), Rights::ALL); + let child = parent + .derive(Rights::READ) + .expect("READ is a subset of ALL"); + assert_eq!(child.rights(), Rights::READ); + assert_eq!(child.generation(), parent.generation()); + assert_eq!(child.object().id(), 7); + // The parent is untouched by the derivation. + assert_eq!(parent.rights(), Rights::ALL); + } + + #[test] + fn derive_refuses_widening() { + let parent = Capability::mint(TestObject::new(1), Rights::DUPLICATE.union(Rights::READ)); + // A right the parent lacks, alone or mixed with rights it holds — + // either way the request is not a subset (O-2). + assert_eq!( + parent.derive(Rights::WRITE).err(), + Some(CapabilityError::RightsNotSubset) + ); + assert_eq!( + parent.derive(Rights::READ.union(Rights::WRITE)).err(), + Some(CapabilityError::RightsNotSubset) + ); + } + + #[test] + fn derive_from_a_leaf_fails_even_for_a_strict_subset() { + // No DUPLICATE: a leaf. Nothing may be derived from it, not even + // nothing at all. + let leaf = Capability::mint(TestObject::new(1), Rights::READ); + assert_eq!( + leaf.derive(Rights::READ).err(), + Some(CapabilityError::NotDuplicable) + ); + assert_eq!( + leaf.derive(Rights::NONE).err(), + Some(CapabilityError::NotDuplicable) + ); + // A right the leaf also lacks still reports NotDuplicable — the + // checks run in the documented order, and this is the one input that + // tells the two rights checks apart. + assert_eq!( + leaf.derive(Rights::WRITE).err(), + Some(CapabilityError::NotDuplicable) + ); + } + + #[test] + fn derive_from_a_stale_capability_fails_before_any_rights_check() { + let object = TestObject::new(1); + let parent = Capability::mint(object.clone(), Rights::ALL); + object.destroy(); + // Staleness wins over every other verdict: a dead capability is inert, + // not merely under-privileged. + assert_eq!( + parent.derive(Rights::READ).err(), + Some(CapabilityError::StaleGeneration) + ); + // Even a request that would also fail the rights checks reports the + // staleness, deliberately: the checks run in the documented order. + let stale_leaf = Capability::mint(object.clone(), Rights::NONE); + object.destroy(); + assert_eq!( + stale_leaf.derive(Rights::WRITE).err(), + Some(CapabilityError::StaleGeneration) + ); + } + + #[test] + fn debug_omits_the_object() { + let capability = Capability::mint(TestObject::new(1), Rights::READ); + let text = format!("{capability:?}"); + assert!(text.contains("Rights(READ)")); + assert!( + !text.contains("TestObject"), + "the object must not leak through Debug: {text}" + ); + } +} diff --git a/capability/src/error.rs b/capability/src/error.rs index 5433417..62e3c90 100644 --- a/capability/src/error.rs +++ b/capability/src/error.rs @@ -22,7 +22,14 @@ pub enum CapabilityError { NotDuplicable, /// The table has no free slot to mint into. TableFull, - /// A slot's generation counter is exhausted; the slot is retired rather than - /// wrapped (the fail-closed boundary — see [`Generation`](crate::Generation)). + /// An object's generation counter is exhausted, so the object must be + /// retired: kept inert, its identity never reused, nothing minted to it + /// again (the fail-closed boundary — see the generation contract on + /// [`ObjectRef`](crate::ObjectRef)). Reserved vocabulary for the kernel's + /// object-destruction path: no *table* operation returns it. Slot-side + /// exhaustion is handled silently by retiring the slot — + /// [`remove`](crate::CapabilityTable::remove) still succeeds, resolve + /// reports [`StaleGeneration`](Self::StaleGeneration) and insert reports + /// [`TableFull`](Self::TableFull). GenerationExhausted, } diff --git a/capability/src/generation.rs b/capability/src/generation.rs index 735472f..5b50bf7 100644 --- a/capability/src/generation.rs +++ b/capability/src/generation.rs @@ -44,6 +44,17 @@ impl Generation { } } +#[cfg(test)] +impl Generation { + /// The last representable generation — test-only, so the fail-closed + /// exhaustion boundary can be reached without 2^64 bumps. Kernel builds + /// never see this constructor; outside tests a generation is only ever + /// [`FIRST`](Self::FIRST) or the successor of an existing one. + pub(crate) const fn last() -> Self { + Self(u64::MAX) + } +} + #[cfg(test)] #[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)] mod tests { diff --git a/capability/src/lib.rs b/capability/src/lib.rs index 83024b6..bc49c7c 100644 --- a/capability/src/lib.rs +++ b/capability/src/lib.rs @@ -8,34 +8,58 @@ //! so it *can* be host-tested — a bare-metal target has no test harness — and //! so the security spine can be exercised exhaustively away from the hardware. //! -//! This first increment is the value types the table is built from. The owned, -//! no-`Clone` capability and the flat per-process table follow, one reviewable -//! step at a time (§5.3). -//! //! What the design guarantees, and where it lives: //! //! - **Unforgeability (O-1).** Userspace holds a [`Handle`] — a table index plus -//! a [`Generation`] — never the capability itself, which is kernel memory. A -//! stale handle fails closed against the generation rather than aliasing a -//! reused slot. -//! - **Non-widenability (O-2).** [`Rights`] only ever *diminish*: no operation -//! anywhere adds a right, and [`Rights::diminish`] is the sole way to change a -//! held capability's rights. +//! a [`Generation`] — never the capability itself, which is kernel memory. +//! [`CapabilityTable::resolve`] is the single gate through which a handle is +//! exercised against its object, re-checked in full on every call; a stale +//! handle fails closed against the generation rather than aliasing a reused +//! slot. ([`CapabilityTable::remove`] relocates authority for transfer or +//! close, after the same stale-handle check — but every exercise of that +//! authority still funnels through `resolve`, whose borrow the caller holds +//! across the whole check→act window.) +//! - **Non-widenability (O-2).** [`Rights`] only ever *diminish*: derivation +//! ([`Capability::derive`], [`CapabilityTable::derive`]) is subset-only, and +//! no operation anywhere adds a right. +//! - **Revocability, the destruction half (O-3).** Destroying an object bumps +//! its generation ([`ObjectRef::current_generation`]); every capability +//! minted before that instant fails closed at its next resolve, with no list +//! of holders required. Selective revocation is RFC-0003a's question, still +//! open — until it lands, this is the only revocation there is. +//! - **Transfer is a move (§6).** A [`Capability`] is owned and neither `Clone` +//! nor `Copy`: it leaves one table by [`CapabilityTable::remove`] and enters +//! another by [`CapabilityTable::insert`] as a Rust move, never live in two +//! places at once. //! //! Per the RFC-0003 prior-art amendment, the compile-time guarantees here cover //! the kernel's *internal* handling; the userspace-observable cross-process //! transfer is a runtime table operation the generation scheme secures — the //! borrow checker cannot span protection domains, so this crate does not pretend //! it does. +//! +//! Still to come, each its own reviewable increment (§5.3): wiring into the +//! kernel's syscall surface once there are kernel objects to reference, and +//! selective revocation once RFC-0003a decides it. #![cfg_attr(not(test), no_std)] +mod capability; mod error; mod generation; mod handle; +mod object; mod rights; +mod table; + +#[cfg(test)] +#[allow(clippy::expect_used)] +pub(crate) mod test_support; +pub use capability::Capability; pub use error::CapabilityError; pub use generation::Generation; pub use handle::Handle; +pub use object::ObjectRef; pub use rights::Rights; +pub use table::CapabilityTable; diff --git a/capability/src/object.rs b/capability/src/object.rs new file mode 100644 index 0000000..3dac628 --- /dev/null +++ b/capability/src/object.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The object side of the contract — what the table requires of a kernel +//! object reference. + +use crate::Generation; + +/// A counted reference to a kernel object, as the capability table sees one. +/// +/// The table is deliberately generic over the reference type. It moves, clones +/// and drops references and asks exactly one question — *what generation is the +/// referenced object at now?* Everything else about kernel objects (what they +/// are, how they are allocated, how their reference counts work) belongs to the +/// kernel crate, which implements this trait for its object references once +/// those exist. Until then a test double implements it on the host, which is +/// how the whole scheme stays exhaustively testable away from the hardware. +/// +/// `Clone` is a supertrait because a counted reference is duplicable *as a +/// reference*: cloning it copies a pointer and takes a reference count, and +/// duplicates no authority. Authority lives in +/// [`Capability`](crate::Capability), which is deliberately not `Clone` — its +/// only duplication is [`derive`](crate::Capability::derive), explicit and +/// rights-checked. +/// +/// # The generation contract +/// +/// An implementation must uphold what RFC-0003 §7–§8 rely on: +/// +/// - The returned generation only ever advances: successive calls never yield +/// an earlier value, and a generation, once left, is never occupied again. +/// - Destroying the object advances its generation past every capability +/// minted for it, which is what makes those capabilities inert (the +/// destruction half of O-3): +/// [`CapabilityTable::resolve`](crate::CapabilityTable::resolve) checks the +/// minted generation against this one on every call. +/// - If the generation cannot advance — [`Generation::next`] returns [`None`], +/// the fail-closed boundary — the object must be retired: kept inert, its +/// identity never reused, no new capability ever minted to it. The error +/// vocabulary for that path is +/// [`CapabilityError::GenerationExhausted`](crate::CapabilityError::GenerationExhausted). +/// - A cloned reference confers no authority. Acting on the object is +/// authorised only by a live capability, resolved and *held* — as a borrow — +/// across the caller's whole check→act window +/// ([`CapabilityTable::resolve`](crate::CapabilityTable::resolve) states +/// why). Extracting the reference from a resolved capability and acting +/// after the borrow is gone would re-open the race with revocation the +/// generation check closes: `Clone` exists for table bookkeeping, not for +/// acting outside a resolve. +pub trait ObjectRef: Clone { + /// The referenced object's current generation. + fn current_generation(&self) -> Generation; +} diff --git a/capability/src/table.rs b/capability/src/table.rs new file mode 100644 index 0000000..011aa89 --- /dev/null +++ b/capability/src/table.rs @@ -0,0 +1,1173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The flat per-process capability table — RFC-0003 §4, Option B. + +use crate::{Capability, CapabilityError, Generation, Handle, ObjectRef, Rights}; +use core::fmt; +use core::mem; + +/// One slot of the table: its generation, and what currently occupies it. +struct Slot { + /// For an occupied slot: the generation its handle was minted at. For a + /// free slot: the generation the *next* mint will use — bumped at close + /// time, so a closed handle dies at the instant of closing, not merely + /// when its slot is reused. + generation: Generation, + state: SlotState, +} + +/// What a slot currently holds. +enum SlotState { + /// Empty. A recycled slot additionally links the free list; a never-used + /// slot carries `None` and is reached through the high-water mark instead. + Free { next_free: Option }, + /// Holds a live capability. + Occupied(Capability), + /// The slot's generation space is exhausted: never offered again, so no + /// stale handle can ever match a wrapped counter (the fail-closed + /// boundary — see [`Generation::next`]). + Retired, +} + +/// A process's capability table: the flat array mapping [`Handle`]s to the +/// [`Capability`]s the process holds (RFC-0003 §4, Option B). +/// +/// One table per process, `N` slots, no allocation and no tree: resolution is +/// an array index plus two generation checks, O(1) by construction. The +/// capacity is a compile-time bound because the kernel has no allocator, and +/// because pillar 2 prefers bounded structures a reviewer can hold in their +/// head; a full table fails closed with [`CapabilityError::TableFull`]. +/// +/// Slot reuse is guarded by a per-slot [`Generation`], bumped when a slot is +/// vacated — so a closed handle is dead from the moment of closing, and a +/// reused slot never honours the previous occupant's handle (the ABA defence, +/// O-1). A slot whose generation cannot advance is retired outright: unusable +/// forever, at the cost of one slot of capacity. +/// +/// The division of labour is deliberately visible in the trait bounds: +/// [`insert`](Self::insert) and [`remove`](Self::remove) are pure slot +/// mechanics — the plumbing a transfer is built from — and know nothing about +/// objects; [`resolve`](Self::resolve) and [`derive`](Self::derive) are the +/// authority checks, and require [`ObjectRef`]. Rights enforcement on +/// *invocation* (does this capability permit `WRITE`?) belongs to the caller +/// that resolves, per RFC-0003 §9 — the table answers what a handle names, +/// not what a syscall may do with it. +/// +/// # Example — the life of a capability +/// +/// ``` +/// use core::cell::Cell; +/// use std::rc::Rc; +/// use setonix_capability::{ +/// Capability, CapabilityError, CapabilityTable, Generation, ObjectRef, Rights, +/// }; +/// +/// // A stand-in kernel object: a counted reference to a shared generation. +/// #[derive(Clone)] +/// struct Endpoint(Rc>); +/// +/// impl ObjectRef for Endpoint { +/// fn current_generation(&self) -> Generation { +/// self.0.get() +/// } +/// } +/// +/// let generation = Rc::new(Cell::new(Generation::FIRST)); +/// let mut table: CapabilityTable = CapabilityTable::new(); +/// +/// // Mint at the object's current generation; the holder gets back a handle. +/// let parent = table +/// .insert(Capability::mint(Endpoint(Rc::clone(&generation)), Rights::ALL)) +/// .expect("the table is empty"); +/// +/// // Derivation attenuates: the child holds a subset, never more (O-2). +/// let child = table +/// .derive(parent, Rights::DUPLICATE.union(Rights::READ)) +/// .expect("a subset of ALL"); +/// assert_eq!( +/// table.derive(child, Rights::WRITE), +/// Err(CapabilityError::RightsNotSubset), +/// ); +/// +/// // Destroying the object bumps its generation: every capability to it, +/// // parent and child alike, is inert at its next resolve (O-3). +/// generation.set(Generation::FIRST.next().expect("generation 2 exists")); +/// assert_eq!(table.resolve(parent).err(), Some(CapabilityError::StaleGeneration)); +/// assert_eq!(table.resolve(child).err(), Some(CapabilityError::StaleGeneration)); +/// ``` +pub struct CapabilityTable { + slots: [Slot; N], + /// Head of the free list threaded through recycled slots. + free_head: Option, + /// The high-water mark: the first never-used slot. Slots at or above it + /// are pristine and reached here, not through the free list. + next_unused: usize, + /// The number of occupied slots. + live: usize, +} + +impl CapabilityTable { + /// The number of slots, fixed at compile time. + pub const CAPACITY: usize = N; + + /// An empty table: every slot pristine at [`Generation::FIRST`]. + #[must_use] + pub const fn new() -> Self { + Self { + slots: [const { + Slot { + generation: Generation::FIRST, + state: SlotState::Free { next_free: None }, + } + }; N], + free_head: None, + next_unused: 0, + live: 0, + } + } + + /// The number of capabilities currently held. + #[must_use] + pub const fn len(&self) -> usize { + self.live + } + + /// Whether the table holds no capabilities at all. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.live == 0 + } + + /// Store `capability`, minting the handle that names it. + /// + /// Prefers a recycled slot (most recently freed first) and opens a + /// never-used one otherwise. + /// + /// # Errors + /// + /// On a full table the capability is handed **back** beside + /// [`CapabilityError::TableFull`] rather than dropped: destroying + /// in-flight authority because the receiver had no room would turn a + /// resource limit into silent revocation. The caller — ultimately the IPC + /// path — decides what a failed delivery means. + pub fn insert( + &mut self, + capability: Capability, + ) -> Result)> { + let index = if let Some(recycled) = self.take_recycled() { + recycled + } else if self.next_unused < N { + match u32::try_from(self.next_unused) { + Ok(index) => { + self.next_unused += 1; + index + } + // A handle is deliberately small: slot numbers beyond + // `u32::MAX` are unmintable, so a table that large exhausts + // at the handle-width boundary. Fail closed as capacity. + Err(_) => return Err((CapabilityError::TableFull, capability)), + } + } else { + return Err((CapabilityError::TableFull, capability)); + }; + + let Ok(slot_index) = usize::try_from(index) else { + // Unreachable on any supported platform (`u32` fits in `usize`); + // fail closed rather than panic. + return Err((CapabilityError::TableFull, capability)); + }; + match self.slots.get_mut(slot_index) { + Some(slot) if matches!(slot.state, SlotState::Free { .. }) => { + slot.state = SlotState::Occupied(capability); + // Occupancy is bounded by N, so this cannot overflow. + self.live += 1; + Ok(Handle::new(index, slot.generation)) + } + // Unreachable: both index sources yield an in-bounds, vacant + // slot. Refuse to overwrite regardless — silently dropping a live + // occupant would be silent revocation — and fail closed. + _ => Err((CapabilityError::TableFull, capability)), + } + } + + /// Take the capability `handle` names out of the table, by value. + /// + /// This is close and transfer-out in one operation: the caller receives + /// the owned capability and the slot is vacated, its generation bumped so + /// the handle — and any copy of it userspace kept — is dead from this + /// instant, not merely from the slot's next reuse. Dropping the returned + /// value is a close; moving it into another table is a transfer. + /// + /// Deliberately object-blind: a capability whose object is long destroyed + /// can still be removed, because cleanup must always be possible. Whether + /// the removal *means* anything — a transfer wants `TRANSFER` rights and + /// a live object — is the invocation path's question, answered against + /// [`resolve`](Self::resolve) before anything moves. + /// + /// # Errors + /// + /// - [`CapabilityError::OutOfBounds`] — the index lies outside the table. + /// - [`CapabilityError::Empty`] — the slot holds nothing. + /// - [`CapabilityError::StaleGeneration`] — the slot is occupied at a + /// different generation than the handle's, or is retired. + pub fn remove(&mut self, handle: Handle) -> Result, CapabilityError> { + let slot_index = + usize::try_from(handle.index()).map_err(|_| CapabilityError::OutOfBounds)?; + let slot = self + .slots + .get_mut(slot_index) + .ok_or(CapabilityError::OutOfBounds)?; + + // Take the state out to gain ownership of what it holds. Every path + // below installs the slot's true successor state; `Retired` stands in + // meanwhile so that no path can leave the slot claiming a capability + // it no longer holds. + let state = mem::replace(&mut slot.state, SlotState::Retired); + match state { + SlotState::Occupied(capability) if slot.generation == handle.generation() => { + // If the generation cannot advance the slot stays `Retired` — + // never freed, never reused — rather than wrapping to a value + // a stale handle could match. + if let Some(next) = slot.generation.next() { + slot.generation = next; + slot.state = SlotState::Free { + next_free: self.free_head, + }; + self.free_head = Some(handle.index()); + } + // An occupied slot implies at least one live capability, so + // this cannot underflow. + self.live -= 1; + Ok(capability) + } + other => { + let error = match &other { + SlotState::Free { .. } => CapabilityError::Empty, + // Occupied at some other generation, or retired: either + // way the handle is stale. + SlotState::Occupied(_) | SlotState::Retired => CapabilityError::StaleGeneration, + }; + slot.state = other; + Err(error) + } + } + } + + /// Pop the most recently freed slot off the free list, if any. + fn take_recycled(&mut self) -> Option { + let index = self.free_head?; + let slot_index = usize::try_from(index).ok()?; + let slot = self.slots.get(slot_index)?; + if let SlotState::Free { next_free } = &slot.state { + self.free_head = *next_free; + Some(index) + } else { + // A free-list entry that is not free would be a logic error in + // this module; fail closed by offering no recycled slot rather + // than panicking. The never-used region still serves inserts. + None + } + } +} + +impl CapabilityTable { + /// Resolve `handle` to the capability it names — the single gate through + /// which a handle is exercised against its object, re-checked in full on + /// every call. ([`remove`](Self::remove) relocates authority for transfer + /// or close, after the same stale-handle check; but every *exercise* of + /// that authority funnels through here.) + /// + /// Two of the RFC-0003 amendment's load-bearing invariants live on this + /// method. A successful resolve yields a borrow, never a copy: + /// capabilities are not cached outside the table, which is why a + /// generation bump is a complete revocation. And the caller must hold + /// that borrow across its whole check→act window: the borrow is what + /// keeps revocation out until the authorised action completes, so acting + /// after letting it go — say, by cloning the object reference out of the + /// capability and dropping the borrow — would re-open the race with + /// revocation that the generation check just closed. Today the borrow + /// checker enforces this on a single thread; any future multi-core + /// synchronisation story (RFC-0003 §14) must preserve exactly this + /// property, as a correctness dependency of the generation scheme rather + /// than a later addition. + /// + /// # Errors + /// + /// Checked in this order, each failing closed: + /// + /// - [`CapabilityError::OutOfBounds`] — the index lies outside the table. + /// - [`CapabilityError::Empty`] — the slot holds nothing. (A freed slot's + /// generation was already bumped, so a closed handle also lands here + /// until the slot is reused; the state check simply answers first.) + /// - [`CapabilityError::StaleGeneration`] — the slot is retired, or + /// occupied at a different generation than the handle's (a stale + /// handle, O-1); or the capability's minted generation no longer + /// matches the object's — the object was destroyed, and the capability + /// is inert (the destruction half of O-3). + pub fn resolve(&self, handle: Handle) -> Result<&Capability, CapabilityError> { + let slot_index = + usize::try_from(handle.index()).map_err(|_| CapabilityError::OutOfBounds)?; + let slot = self + .slots + .get(slot_index) + .ok_or(CapabilityError::OutOfBounds)?; + match &slot.state { + SlotState::Free { .. } => Err(CapabilityError::Empty), + SlotState::Retired => Err(CapabilityError::StaleGeneration), + SlotState::Occupied(capability) => { + if slot.generation != handle.generation() { + return Err(CapabilityError::StaleGeneration); + } + if capability.generation() != capability.object().current_generation() { + return Err(CapabilityError::StaleGeneration); + } + Ok(capability) + } + } + } + + /// Mint an attenuated sibling: resolve `parent`, run the derivation + /// checks ([`Capability::derive`] — live, `DUPLICATE`, subset-only), and + /// store the child in this same table. + /// + /// # Errors + /// + /// Everything [`resolve`](Self::resolve) and [`Capability::derive`] can + /// return, plus [`CapabilityError::TableFull`] if there is no slot for + /// the child. On any failure the table is unchanged and the parent + /// untouched; a child that could not be stored is dropped, which loses + /// nothing — it was never granted. + pub fn derive(&mut self, parent: Handle, requested: Rights) -> Result { + let child = self.resolve(parent)?.derive(requested)?; + self.insert(child).map_err(|(error, _never_granted)| error) + } +} + +impl Default for CapabilityTable { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for CapabilityTable { + /// A summary, not a listing: what the table's capabilities reference is + /// not this crate's to print. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CapabilityTable") + .field("live", &self.live) + .field("capacity", &N) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)] +mod tests { + use crate::test_support::TestObject; + use crate::{Capability, CapabilityError, CapabilityTable, Generation, Handle, Rights}; + + /// Mint a full-rights capability to a fresh test object. + fn full(id: u32) -> Capability { + Capability::mint(TestObject::new(id), Rights::ALL) + } + + #[test] + fn insert_then_resolve_returns_the_capability() { + let mut table: CapabilityTable = CapabilityTable::new(); + let handle = table.insert(full(1)).expect("the table is empty"); + let capability = table.resolve(handle).expect("a live handle resolves"); + assert_eq!(capability.rights(), Rights::ALL); + assert_eq!(capability.object().id(), 1); + } + + #[test] + fn resolve_fails_closed_on_every_shape_of_bad_handle() { + let mut table: CapabilityTable = CapabilityTable::new(); + let real = table.insert(full(1)).expect("the table is empty"); + // Beyond the table: out of bounds. + assert_eq!( + table.resolve(Handle::new(2, Generation::FIRST)).err(), + Some(CapabilityError::OutOfBounds) + ); + assert_eq!( + table + .resolve(Handle::new(u32::MAX, Generation::FIRST)) + .err(), + Some(CapabilityError::OutOfBounds) + ); + // In bounds, never allocated: empty. + assert_eq!( + table.resolve(Handle::new(1, Generation::FIRST)).err(), + Some(CapabilityError::Empty) + ); + // The right slot at the wrong generation: stale. + let wrong_generation = Handle::new( + real.index(), + real.generation().next().expect("a next generation exists"), + ); + assert_eq!( + table.resolve(wrong_generation).err(), + Some(CapabilityError::StaleGeneration) + ); + } + + #[test] + fn only_the_exact_minted_handle_resolves() { + // A small exhaustive sweep over forged (index, generation) pairs: + // exactly one handle in the space resolves, and it is the minted one + // (O-1 exercised as a search, not an anecdote). + let mut table: CapabilityTable = CapabilityTable::new(); + let real = table.insert(full(1)).expect("the table is empty"); + let mut generation = Generation::FIRST; + for _ in 0..4 { + for index in 0..6 { + let forged = Handle::new(index, generation); + if forged == real { + assert!(table.resolve(forged).is_ok()); + } else { + assert!( + table.resolve(forged).is_err(), + "forged handle {forged:?} must not resolve" + ); + } + } + generation = generation.next().expect("small generations exist"); + } + } + + #[test] + fn remove_returns_the_owned_capability_and_the_handle_dies_immediately() { + let mut table: CapabilityTable = CapabilityTable::new(); + let handle = table.insert(full(3)).expect("the table is empty"); + let capability = table.remove(handle).expect("a live handle removes"); + assert_eq!(capability.object().id(), 3); + assert_eq!(table.len(), 0); + // Dead at the instant of closing — before any reuse. The slot is + // empty *and* its generation already bumped; the state check answers + // first, hence `Empty` (the precedence is deliberate and documented + // on `resolve`). + assert_eq!(table.resolve(handle).err(), Some(CapabilityError::Empty)); + // A second remove finds nothing either. + assert_eq!(table.remove(handle).err(), Some(CapabilityError::Empty)); + } + + #[test] + fn a_reused_slot_never_honours_the_previous_occupants_handle() { + // The ABA test: close, reuse, and the stale handle must fail rather + // than alias the new occupant (RFC-0003 §8). + let mut table: CapabilityTable = CapabilityTable::new(); + let first = table.insert(full(1)).expect("the table is empty"); + drop(table.remove(first).expect("a live handle removes")); + let second = table.insert(full(2)).expect("a recycled slot is free"); + // Same slot, new generation. + assert_eq!(second.index(), first.index()); + assert_ne!(second.generation(), first.generation()); + // The stale handle fails closed; the new one names the new occupant. + assert_eq!( + table.resolve(first).err(), + Some(CapabilityError::StaleGeneration) + ); + assert_eq!( + table + .resolve(second) + .expect("a live handle resolves") + .object() + .id(), + 2 + ); + } + + #[test] + fn a_full_table_hands_the_capability_back_rather_than_destroying_it() { + let mut table: CapabilityTable = CapabilityTable::new(); + let _first = table.insert(full(1)).expect("slot 0 is free"); + let second = table.insert(full(2)).expect("slot 1 is free"); + let (error, returned) = table + .insert(Capability::mint(TestObject::new(3), Rights::READ)) + .expect_err("the table is full"); + assert_eq!(error, CapabilityError::TableFull); + // The capability comes back intact, not destroyed. + assert_eq!(returned.object().id(), 3); + assert_eq!(returned.rights(), Rights::READ); + // Freeing a slot restores capacity, and the freed slot is the one + // recycled. + drop(table.remove(second).expect("a live handle removes")); + let again = table.insert(returned).expect("a slot was recycled"); + assert_eq!(again.index(), second.index()); + assert_ne!(again.generation(), second.generation()); + } + + #[test] + fn derive_mints_an_attenuated_sibling_in_the_same_table() { + let mut table: CapabilityTable = CapabilityTable::new(); + let parent = table.insert(full(5)).expect("the table is empty"); + let child = table + .derive(parent, Rights::READ) + .expect("READ is a subset of ALL"); + assert_ne!(child.index(), parent.index()); + let resolved = table.resolve(child).expect("the child is live"); + assert_eq!(resolved.rights(), Rights::READ); + assert_eq!(resolved.object().id(), 5); + // The parent is untouched by the derivation. + assert_eq!( + table.resolve(parent).expect("the parent is live").rights(), + Rights::ALL + ); + assert_eq!(table.len(), 2); + } + + #[test] + fn derive_refuses_widening_and_underprivileged_parents() { + let mut table: CapabilityTable = CapabilityTable::new(); + let limited = table + .insert(Capability::mint( + TestObject::new(1), + Rights::DUPLICATE.union(Rights::READ), + )) + .expect("the table is empty"); + // Widening: the child asks for a right the parent lacks (O-2). + assert_eq!( + table.derive(limited, Rights::WRITE), + Err(CapabilityError::RightsNotSubset) + ); + assert_eq!( + table.derive(limited, Rights::READ.union(Rights::WRITE)), + Err(CapabilityError::RightsNotSubset) + ); + // A leaf — no DUPLICATE — yields nothing, not even a strict subset. + let leaf = table + .derive(limited, Rights::READ) + .expect("READ is a subset"); + assert_eq!( + table.derive(leaf, Rights::READ), + Err(CapabilityError::NotDuplicable) + ); + assert_eq!( + table.derive(leaf, Rights::NONE), + Err(CapabilityError::NotDuplicable) + ); + // The failures minted nothing. + assert_eq!(table.len(), 2); + } + + #[test] + fn derive_into_a_full_table_fails_and_the_parent_survives() { + let mut table: CapabilityTable = CapabilityTable::new(); + let parent = table.insert(full(1)).expect("the table is empty"); + assert_eq!( + table.derive(parent, Rights::READ), + Err(CapabilityError::TableFull) + ); + assert_eq!( + table + .resolve(parent) + .expect("the parent is still live") + .rights(), + Rights::ALL + ); + } + + #[test] + fn destroying_the_object_makes_every_capability_to_it_inert() { + let mut table: CapabilityTable = CapabilityTable::new(); + let object = TestObject::new(3); + let parent = table + .insert(Capability::mint(object.clone(), Rights::ALL)) + .expect("the table is empty"); + let child = table + .derive(parent, Rights::READ) + .expect("READ is a subset of ALL"); + object.destroy(); + // No list of holders required: parent and child alike fail at their + // next resolve (the destruction half of O-3). + assert_eq!( + table.resolve(parent).err(), + Some(CapabilityError::StaleGeneration) + ); + assert_eq!( + table.resolve(child).err(), + Some(CapabilityError::StaleGeneration) + ); + // Slot-level cleanup still works — a corpse can be removed... + let corpse = table + .remove(parent) + .expect("slot bookkeeping outlives the object"); + assert_eq!(corpse.rights(), Rights::ALL); + // ...but deriving from one fails closed. + assert_eq!( + table.derive(child, Rights::READ), + Err(CapabilityError::StaleGeneration) + ); + } + + #[test] + fn transfer_moves_a_capability_between_tables() { + let mut sender: CapabilityTable = CapabilityTable::new(); + let mut receiver: CapabilityTable = CapabilityTable::new(); + let outgoing = sender.insert(full(9)).expect("the sender table is empty"); + // The move (RFC-0003 §6): out of one table, into the other. At no + // instant is the capability in both — `remove` returns it by value, + // `insert` consumes it. + let in_flight = sender.remove(outgoing).expect("a live handle removes"); + let delivered = receiver.insert(in_flight).expect("the receiver has room"); + assert_eq!(sender.len(), 0); + assert_eq!(receiver.len(), 1); + assert!(sender.resolve(outgoing).is_err()); + assert_eq!( + receiver + .resolve(delivered) + .expect("the delivered handle resolves") + .object() + .id(), + 9 + ); + } + + #[test] + fn generation_exhaustion_retires_the_slot_rather_than_wrapping() { + let mut table: CapabilityTable = CapabilityTable::new(); + // Age slot 0 to the last representable generation, as if it had been + // recycled 2^64 - 1 times. Tests may reach into the private slot + // array; nothing outside this module can. + table.slots.get_mut(0).expect("slot 0 exists").generation = Generation::last(); + let handle = table.insert(full(7)).expect("the table is empty"); + assert_eq!(handle.generation(), Generation::last()); + assert!(table.resolve(handle).is_ok()); + // Closing cannot bump the generation, so the slot retires instead of + // wrapping to a value some stale handle could match. + drop(table.remove(handle).expect("a live handle removes")); + assert_eq!(table.len(), 0); + // The handle is dead... + assert_eq!( + table.resolve(handle).err(), + Some(CapabilityError::StaleGeneration) + ); + // ...and the slot is never offered again: with its only slot retired, + // the table is permanently full. Capacity is the price of failing + // closed. + assert!(matches!( + table.insert(full(8)), + Err((CapabilityError::TableFull, _)) + )); + } + + #[test] + fn bookkeeping_reports_occupancy() { + let mut table: CapabilityTable = CapabilityTable::default(); + assert!(table.is_empty()); + assert_eq!(CapabilityTable::::CAPACITY, 3); + let handle = table.insert(full(1)).expect("the table is empty"); + assert_eq!(table.len(), 1); + assert!(!table.is_empty()); + drop(table.remove(handle).expect("a live handle removes")); + assert!(table.is_empty()); + // Debug summarises without printing objects. + let text = format!("{table:?}"); + assert!(text.contains("live")); + assert!(!text.contains("TestObject")); + } + + #[test] + fn remove_honours_only_the_current_occupants_handle() { + // The ABA case at the remove gate (RFC-0003 §8): a dangling handle to + // a reused slot must not steal or destroy the slot's current + // occupant. + let mut table: CapabilityTable = CapabilityTable::new(); + let first = table.insert(full(1)).expect("the table is empty"); + drop(table.remove(first).expect("a live handle removes")); + let second = table.insert(full(2)).expect("the slot recycles"); + assert_eq!(second.index(), first.index()); + // The stale handle removes nothing... + assert_eq!( + table.remove(first).err(), + Some(CapabilityError::StaleGeneration) + ); + // ...and disturbed nothing: the occupant is still there, still + // resolves, and still answers to its own handle. + assert_eq!(table.len(), 1); + assert_eq!( + table + .resolve(second) + .expect("the occupant survives the failed remove") + .object() + .id(), + 2 + ); + let occupant = table.remove(second).expect("the real handle still removes"); + assert_eq!(occupant.object().id(), 2); + } + + #[test] + fn remove_fails_closed_on_out_of_bounds_handles() { + let mut table: CapabilityTable = CapabilityTable::new(); + let _live = table.insert(full(1)).expect("the table is empty"); + // The first index past the table, and the far end of handle space. + assert_eq!( + table.remove(Handle::new(2, Generation::FIRST)).err(), + Some(CapabilityError::OutOfBounds) + ); + assert_eq!( + table.remove(Handle::new(u32::MAX, Generation::FIRST)).err(), + Some(CapabilityError::OutOfBounds) + ); + // The failures removed nothing. + assert_eq!(table.len(), 1); + } + + #[test] + fn a_retired_slot_coexists_with_usable_slots() { + // Retirement in company: the N=1 exhaustion test proves the fail-closed + // boundary, this one proves the price is one slot, not the table. The + // ordering matters — a slot is freed normally *before* the retirement, + // so the free list is shown to survive it. + let mut table: CapabilityTable = CapabilityTable::new(); + // Age slot 1 to the boundary before it is ever offered. + table.slots.get_mut(1).expect("slot 1 exists").generation = Generation::last(); + let first = table.insert(full(0)).expect("slot 0 is free"); + let aged = table.insert(full(1)).expect("slot 1 is free"); + let neighbour = table.insert(full(2)).expect("slot 2 is free"); + // A normally freed slot joins the free list first... + drop(table.remove(first).expect("a live handle removes")); + // ...then closing the aged slot retires it: no bump is possible. + drop(table.remove(aged).expect("a live handle removes")); + // Removing through the retired slot's handle is stale, not empty — + // the `Retired` arm of remove's error match. + assert_eq!( + table.remove(aged).err(), + Some(CapabilityError::StaleGeneration) + ); + // The neighbour is untouched by the retirement. + assert_eq!( + table + .resolve(neighbour) + .expect("the neighbour is live") + .object() + .id(), + 2 + ); + // The free list survived the retirement: slot 0 is still reachable. + let reused = table.insert(full(3)).expect("slot 0 was recycled"); + assert_eq!(reused.index(), first.index()); + // And the table is permanently one slot smaller: full at N - 1. + assert!(matches!( + table.insert(full(4)), + Err((CapabilityError::TableFull, _)) + )); + assert_eq!(table.len(), 2); + } + + #[test] + fn recycled_slots_are_reused_most_recently_freed_first() { + // The free list is LIFO (documented on `insert`): the most recently + // freed slot is handed out first, and the chain is followed to its + // end before a pristine slot is opened. + let mut table: CapabilityTable = CapabilityTable::new(); + let a = table.insert(full(1)).expect("slot 0 is free"); + let _b = table.insert(full(2)).expect("slot 1 is free"); + let c = table.insert(full(3)).expect("slot 2 is free"); + drop(table.remove(a).expect("a live handle removes")); + drop(table.remove(c).expect("a live handle removes")); + // The free list is now c -> a -> None: c was freed last, so it is + // reused first, then a, and only then the pristine slot 3. + let first = table.insert(full(4)).expect("two slots are recycled"); + assert_eq!(first.index(), c.index()); + let second = table.insert(full(5)).expect("one slot is recycled"); + assert_eq!(second.index(), a.index()); + let third = table.insert(full(6)).expect("slot 3 is pristine"); + assert_eq!(third.index(), 3); + } + + #[test] + fn a_removed_parent_leaves_its_derived_sibling_untouched() { + // Flat table, no parent link: closing the parent is not revocation — + // the child is a sibling, not a dependant. Selective revocation is + // RFC-0003a's question, and this test pins that remove does not + // pre-empt it. + let mut table: CapabilityTable = CapabilityTable::new(); + let parent = table.insert(full(1)).expect("the table is empty"); + let child = table + .derive(parent, Rights::DUPLICATE.union(Rights::READ)) + .expect("a subset of ALL"); + drop(table.remove(parent).expect("a live handle removes")); + let resolved = table.resolve(child).expect("the child outlives the parent"); + assert_eq!(resolved.rights(), Rights::DUPLICATE.union(Rights::READ)); + assert_eq!(resolved.object().id(), 1); + // Reusing the parent's slot changes nothing for the child, and the + // old parent handle stays dead. + let newcomer = table.insert(full(2)).expect("the parent's slot recycled"); + assert_eq!(newcomer.index(), parent.index()); + assert!(table.resolve(parent).is_err()); + assert_eq!( + table + .resolve(child) + .expect("the child is still live") + .object() + .id(), + 1 + ); + // The chain deepens monotonically (RFC-0003 §5): a DUPLICATE-carrying + // child yields a grandchild, narrowed at each level. + let grandchild = table + .derive(child, Rights::READ) + .expect("READ is a subset of DUPLICATE|READ"); + assert_eq!( + table + .resolve(grandchild) + .expect("the grandchild is live") + .rights(), + Rights::READ + ); + } + + #[test] + fn a_corrupted_free_list_head_fails_closed_rather_than_panicking() { + // The defensive arm in `take_recycled`: a free-list head pointing at + // a non-free slot must offer nothing — not panic, not hand out the + // occupied slot. Unreachable through the public API; tests may + // corrupt the private state directly, as the retirement tests age + // slots directly. + let mut table: CapabilityTable = CapabilityTable::new(); + let first = table.insert(full(1)).expect("the table is empty"); + table.free_head = Some(first.index()); + let second = table + .insert(full(2)) + .expect("the pristine region still serves inserts"); + assert_ne!(second.index(), first.index()); + assert_eq!( + table + .resolve(first) + .expect("the occupant is untouched") + .object() + .id(), + 1 + ); + assert_eq!( + table + .resolve(second) + .expect("the newcomer resolves") + .object() + .id(), + 2 + ); + assert_eq!(table.len(), 2); + } + + #[test] + fn insert_refuses_to_overwrite_an_occupied_slot() { + // The defensive arm in `insert`: even with the high-water mark + // rewound onto an occupied slot (unreachable without a logic error), + // insert hands the capability back rather than silently destroying + // the occupant — silent revocation being the failure mode the error + // shape exists to prevent. + let mut table: CapabilityTable = CapabilityTable::new(); + let first = table.insert(full(1)).expect("the table is empty"); + table.next_unused = 0; + let (error, returned) = table.insert(full(2)).expect_err("the slot is occupied"); + assert_eq!(error, CapabilityError::TableFull); + assert_eq!(returned.object().id(), 2); + assert_eq!( + table + .resolve(first) + .expect("the occupant survives") + .object() + .id(), + 1 + ); + assert_eq!(table.len(), 1); + } + + /// A deterministic xorshift64 generator — no dependencies, fixed seed, + /// reproducible failures. + struct XorShift(u64); + + impl XorShift { + fn draw(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + } + + /// A uniform-enough index below `upper`, which must be non-zero. + fn pick(rng: &mut XorShift, upper: usize) -> usize { + let bound = u64::try_from(upper).expect("usize fits in u64 on supported platforms"); + usize::try_from(rng.draw() % bound).expect("a value below `upper` fits in usize") + } + + /// A rights set built from five random bits — every combination of the + /// defined rights is reachable, so derivation requests span the space. + fn random_rights(rng: &mut XorShift) -> Rights { + let bits = rng.draw(); + let mut rights = Rights::NONE; + for (position, right) in [ + Rights::DUPLICATE, + Rights::TRANSFER, + Rights::READ, + Rights::WRITE, + Rights::REVOKE, + ] + .into_iter() + .enumerate() + { + if bits & (1_u64 << position) != 0 { + rights = rights.union(right); + } + } + rights + } + + /// The churn table's capacity — small enough that the slots run full and + /// recycle constantly under thousands of operations. + const CHURN_CAPACITY: usize = 16; + + /// The churn property test's shadow model: the table under test plus the + /// model's view of it, one method per random operation — extracted from + /// the test so that each step stays short enough to review on its own. + struct Churn { + table: CapabilityTable, + rng: XorShift, + /// Live entries: handle, object id, a clone of the object reference + /// (so the model can destroy it), and the rights minted or derived. + live: Vec<(Handle, u32, TestObject, Rights)>, + /// Stale entries: still occupying a slot, but their object destroyed. + stale: Vec<(Handle, Rights)>, + /// Handles whose slots were vacated: they must never work again. + dead: Vec, + next_id: u32, + } + + impl Churn { + fn new() -> Self { + Self { + table: CapabilityTable::new(), + rng: XorShift(0x5e70_11f0_57ab_1e5d), + live: Vec::new(), + stale: Vec::new(), + dead: Vec::new(), + next_id: 0, + } + } + + /// Insert a fresh full-rights object. + fn insert_fresh(&mut self) { + let object = TestObject::new(self.next_id); + match self + .table + .insert(Capability::mint(object.clone(), Rights::ALL)) + { + Ok(handle) => { + self.live.push((handle, self.next_id, object, Rights::ALL)); + self.next_id += 1; + } + Err((CapabilityError::TableFull, _)) => { + assert_eq!( + self.live.len() + self.stale.len(), + CHURN_CAPACITY, + "full means exactly N occupied" + ); + } + Err((error, _)) => panic!("unexpected insert failure: {error:?}"), + } + } + + /// Derive a random rights request from a random live parent, checking + /// the verdict against the recorded parent rights. + fn derive_random(&mut self) { + if self.live.is_empty() { + return; + } + let (parent, id, object, parent_rights) = self + .live + .get(pick(&mut self.rng, self.live.len())) + .expect("the index is bounded") + .clone(); + let requested = random_rights(&mut self.rng); + match self.table.derive(parent, requested) { + Ok(child) => { + assert!(parent_rights.contains(Rights::DUPLICATE)); + assert!(requested.is_subset_of(parent_rights)); + assert_eq!( + self.table + .resolve(child) + .expect("the child is live") + .rights(), + requested + ); + self.live.push((child, id, object, requested)); + } + Err(CapabilityError::NotDuplicable) => { + assert!(!parent_rights.contains(Rights::DUPLICATE)); + } + Err(CapabilityError::RightsNotSubset) => { + assert!(parent_rights.contains(Rights::DUPLICATE)); + assert!(!requested.is_subset_of(parent_rights)); + } + Err(CapabilityError::TableFull) => { + assert!(parent_rights.contains(Rights::DUPLICATE)); + assert!(requested.is_subset_of(parent_rights)); + assert_eq!(self.live.len() + self.stale.len(), CHURN_CAPACITY); + } + Err(error) => panic!("unexpected derive failure: {error:?}"), + } + } + + /// Remove a random live capability. + fn remove_live(&mut self) { + if self.live.is_empty() { + return; + } + let victim = pick(&mut self.rng, self.live.len()); + let (handle, id, _object, rights) = self.live.swap_remove(victim); + let capability = self.table.remove(handle).expect("a live handle removes"); + assert_eq!(capability.object().id(), id); + assert_eq!(capability.rights(), rights); + self.dead.push(handle); + } + + /// Destroy a random live object: every capability sharing it — the + /// entry itself and any derived from it — turns stale in place, still + /// occupying its slot (the destruction half of O-3 needs no list of + /// holders). + fn destroy_object(&mut self) { + if self.live.is_empty() { + return; + } + let victim_id = self + .live + .get(pick(&mut self.rng, self.live.len())) + .expect("the index is bounded") + .1; + let mut index = 0; + let mut destroyed = false; + while index < self.live.len() { + if self.live.get(index).map(|entry| entry.1) == Some(victim_id) { + let (handle, _id, object, rights) = self.live.swap_remove(index); + if !destroyed { + object.destroy(); + destroyed = true; + } + self.stale.push((handle, rights)); + } else { + index += 1; + } + } + assert!(destroyed, "the victim was in the live set"); + } + + /// Cleanup: a stale entry is inert but must still remove + /// (object-blind), freeing its slot; a dead handle must not remove + /// anything, and its failed remove must disturb no one. + fn clean_up(&mut self) { + if !self.stale.is_empty() { + let victim = pick(&mut self.rng, self.stale.len()); + let (handle, rights) = self.stale.swap_remove(victim); + assert_eq!( + self.table.resolve(handle).err(), + Some(CapabilityError::StaleGeneration) + ); + let corpse = self + .table + .remove(handle) + .expect("object-blind cleanup always works"); + assert_eq!(corpse.rights(), rights); + self.dead.push(handle); + } + if !self.dead.is_empty() { + let handle = *self + .dead + .get(pick(&mut self.rng, self.dead.len())) + .expect("the index is bounded"); + assert!( + self.table.remove(handle).is_err(), + "a dead handle must not remove anything" + ); + if let Some((survivor, id, _object, _rights)) = self.live.first() { + assert_eq!( + self.table + .resolve(*survivor) + .expect("the failed remove disturbed nobody") + .object() + .id(), + *id + ); + } + } + } + + /// Resolve one of each: live, stale, dead. + fn resolve_each_kind(&mut self) { + if !self.live.is_empty() { + let (handle, id, _object, rights) = self + .live + .get(pick(&mut self.rng, self.live.len())) + .expect("the index is bounded") + .clone(); + let capability = self.table.resolve(handle).expect("a live handle resolves"); + assert_eq!(capability.object().id(), id); + assert_eq!(capability.rights(), rights); + } + if !self.stale.is_empty() { + let (handle, _rights) = *self + .stale + .get(pick(&mut self.rng, self.stale.len())) + .expect("the index is bounded"); + assert_eq!( + self.table.resolve(handle).err(), + Some(CapabilityError::StaleGeneration), + "a destroyed object's capability must be inert" + ); + } + if !self.dead.is_empty() { + let handle = *self + .dead + .get(pick(&mut self.rng, self.dead.len())) + .expect("the index is bounded"); + assert!( + self.table.resolve(handle).is_err(), + "a dead handle must never resolve" + ); + } + } + } + + #[test] + fn churn_never_resolves_a_dead_handle() { + // Property test against a shadow model: thousands of random inserts, + // subset-random derivations, object destructions, removals and + // resolves, checking after every step that live handles resolve to + // the right object with the right rights, that handles to destroyed + // objects are inert but still removable (object-blind cleanup), that + // closed handles never resolve and never remove, and that occupancy + // agrees with the model. All three generation-checked behaviours — + // slot reuse (O-1), derivation (O-2) and destruction (O-3) — at + // scale rather than as anecdotes. + let mut churn = Churn::new(); + for _ in 0..8192 { + match churn.rng.draw() % 8 { + // Weighted towards growth so the table regularly runs full. + 0..=2 => churn.insert_fresh(), + 3 => churn.derive_random(), + 4 => churn.remove_live(), + 5 => churn.destroy_object(), + 6 => churn.clean_up(), + _ => churn.resolve_each_kind(), + } + assert_eq!(churn.table.len(), churn.live.len() + churn.stale.len()); + } + + // And at the end: every closed handle is still dead, every survivor + // of a destroyed object still inert. + for handle in churn.dead { + assert!(churn.table.resolve(handle).is_err()); + } + for (handle, _rights) in churn.stale { + assert_eq!( + churn.table.resolve(handle).err(), + Some(CapabilityError::StaleGeneration) + ); + } + } +} diff --git a/capability/src/test_support.rs b/capability/src/test_support.rs new file mode 100644 index 0000000..15f42c9 --- /dev/null +++ b/capability/src/test_support.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Test doubles shared by the unit tests. Compiled only under `cfg(test)`; +//! kernel builds never see this module. + +use crate::{Generation, ObjectRef}; +use std::cell::Cell; +use std::rc::Rc; + +/// A counted reference to a pretend kernel object. +/// +/// Mirrors what the kernel's object references will provide: cloning +/// duplicates the *reference*, never authority — that is `derive`'s job — and +/// the shared generation cell lets a test destroy the object out from under +/// its capabilities, which is exactly the situation the generation check +/// exists for. +#[derive(Clone, Debug)] +pub(crate) struct TestObject { + /// Distinguishes objects in assertions. + id: u32, + /// The object's generation, shared across all clones of the reference. + generation: Rc>, +} + +impl TestObject { + pub(crate) fn new(id: u32) -> Self { + Self { + id, + generation: Rc::new(Cell::new(Generation::FIRST)), + } + } + + pub(crate) fn id(&self) -> u32 { + self.id + } + + /// Destroy the object: bump its generation, so that every capability + /// minted before this moment fails closed at its next resolve. + pub(crate) fn destroy(&self) { + let next = self + .generation + .get() + .next() + .expect("test generations never exhaust"); + self.generation.set(next); + } +} + +impl ObjectRef for TestObject { + fn current_generation(&self) -> Generation { + self.generation.get() + } +}