Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<O>` 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<O, N>` — 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
Expand Down
245 changes: 245 additions & 0 deletions capability/src/capability.rs
Original file line number Diff line number Diff line change
@@ -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<T: Clone>() {}
/// requires_clone::<Capability<Object>>();
/// ```
#[must_use = "dropping a capability closes it — move it into a table, or drop it deliberately"]
pub struct Capability<O> {
object: O,
rights: Rights,
generation: Generation,
}

impl<O: ObjectRef> Capability<O> {
/// 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<Self, CapabilityError> {
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<O> Capability<O> {
/// 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<O> fmt::Debug for Capability<O> {
/// 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}"
);
}
}
11 changes: 9 additions & 2 deletions capability/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
11 changes: 11 additions & 0 deletions capability/src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 34 additions & 10 deletions capability/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading