feat(kernel): derivation-tree revocation, region caps

- caps record parent (mint/delegate/ipc), CapRef w/ pid+slot gens
- Revoke = delete subtree, keep self; drop/death re-parents children
- CreateRegion/MapRegion; mapping lives only as long as its cap
- drop Permission::Revoke + object-wide revoke
- mint any cap (badge only ep/ntfn), process caps delegable
- kani: descendants == subtree; model-based fuzz vs spec
This commit is contained in:
2026-10-26 20:41:03 +01:00
parent 76f10ff1f1
commit 154599e3af
32 changed files with 1302 additions and 239 deletions

View File

@@ -0,0 +1,83 @@
//! The capability derivation relation, reduced to its essence: every
//! capability is a node with at most one parent. Kept free of kernel
//! types so the revocation rule can be checked on its own.
/// `result[i]` is true iff `root` is `i` or an ancestor of `i`.
///
/// `parents` must be acyclic, which holds because a capability's parent
/// always exists before it does.
pub fn descendants<const N: usize>(parents: &[Option<u16>; N], root: usize) -> [bool; N] {
let mut marked = [false; N];
if root >= N {
return marked;
}
marked[root] = true;
// Each pass extends the marked set by at least one generation of
// children; a chain in an N-node forest is at most N - 1 long.
for _ in 0..N {
let mut changed = false;
for i in 0..N {
if !marked[i]
&& let Some(p) = parents[i].map(usize::from)
&& p < N
&& marked[p]
{
marked[i] = true;
changed = true;
}
}
if !changed {
break;
}
}
marked
}
#[cfg(kani)]
mod proofs {
use super::descendants;
const N: usize = 4;
fn is_ancestor(parents: &[Option<u16>; N], root: usize, mut i: usize) -> bool {
for _ in 0..=N {
if i == root {
return true;
}
match parents[i].map(usize::from) {
Some(p) if p < N => i = p,
_ => return false,
}
}
false
}
/// On every acyclic forest of `N` capabilities, `descendants` marks
/// exactly the nodes `root` is an ancestor of: revocation removes the
/// whole subtree and nothing else.
#[kani::proof]
#[kani::unwind(6)]
fn marks_exactly_the_subtree() {
let mut parents = [None; N];
for slot in parents.iter_mut() {
if kani::any() {
*slot = Some(kani::any::<u16>() % N as u16);
}
}
// Only forests: following parents never revisits a node.
for start in 0..N {
let mut i = start;
let mut steps = 0;
while let Some(p) = parents[i] {
steps += 1;
kani::assume(steps <= N - 1);
i = p as usize;
}
}
let root = kani::any::<usize>() % N;
let marked = descendants(&parents, root);
for i in 0..N {
assert!(marked[i] == is_ancestor(&parents, root, i));
}
}
}

View File

@@ -0,0 +1,40 @@
use super::derivation::descendants;
#[test]
fn a_root_with_no_children_marks_only_itself() {
let parents = [None, None, None];
assert_eq!(descendants(&parents, 1), [false, true, false]);
}
#[test]
fn marks_children_and_grandchildren() {
// 0 -> 1 -> 2, 0 -> 3, 4 unrelated
let parents = [None, Some(0), Some(1), Some(0), None];
assert_eq!(descendants(&parents, 0), [true, true, true, true, false]);
}
#[test]
fn revoking_a_middle_node_spares_its_ancestors_and_siblings() {
// 0 -> 1 -> 2, 0 -> 3
let parents = [None, Some(0), Some(1), Some(0)];
assert_eq!(descendants(&parents, 1), [false, true, true, false]);
}
#[test]
fn deep_chains_are_followed_to_the_end() {
let parents = [None, Some(0), Some(1), Some(2), Some(3), Some(4)];
assert_eq!(descendants(&parents, 0), [true; 6]);
}
#[test]
fn chains_listed_out_of_order_are_still_found() {
// 3 -> 2 -> 1 -> 0 (children appear before their parents)
let parents = [Some(1), Some(2), Some(3), None];
assert_eq!(descendants(&parents, 3), [true, true, true, true]);
}
#[test]
fn an_out_of_range_root_marks_nothing() {
let parents = [None, Some(0)];
assert_eq!(descendants(&parents, 7), [false, false]);
}

View File

@@ -1,8 +1,16 @@
use types::{badge::Badge, bounded::CapSlot, generation::Generation, permission::PermissionSet};
use types::{
badge::Badge, bounded::CapSlot, generation::Generation, id::ProcessId,
permission::PermissionSet,
};
pub mod authority;
pub mod derivation;
use crate::{error::KernelError, resource::Resource};
#[cfg(test)]
#[path = "derivation_tests.rs"]
mod derivation_tests;
use crate::{error::KernelError, generations::SlotGenerations, resource::Resource};
#[cfg(test)]
#[path = "tests.rs"]
@@ -11,12 +19,26 @@ mod tests;
// Sized so that every representable `CapSlot` indexes a real slot.
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
/// Names one capability slot at one point in time. Both generations must
/// still match for the reference to resolve, so a reused pid or slot is
/// never mistaken for the capability that used to live there.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapRef {
pub process: ProcessId,
pub process_generation: Generation,
pub slot: CapSlot,
pub slot_generation: Generation,
}
#[derive(Clone)]
pub struct Capability {
resource: Resource,
permissions: PermissionSet,
generation: Generation,
badge: Badge,
/// The capability this one was derived from (Mint, Delegate, IPC
/// transfer); `None` for capabilities the kernel created directly.
parent: Option<CapRef>,
}
impl Capability {
@@ -26,9 +48,23 @@ impl Capability {
permissions,
generation,
badge: Badge::NONE,
parent: None,
}
}
pub fn with_parent(mut self, parent: Option<CapRef>) -> Self {
self.parent = parent;
self
}
pub fn parent(&self) -> Option<CapRef> {
self.parent
}
pub fn set_parent(&mut self, parent: Option<CapRef>) {
self.parent = parent;
}
/// The same capability with `badge` stamped on it.
pub fn with_badge(mut self, badge: Badge) -> Self {
self.badge = badge;
@@ -54,15 +90,36 @@ impl Capability {
pub struct CapabilityTable {
slots: [Option<Capability>; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations<MAX_CAPS_PER_PROCESS>,
}
impl CapabilityTable {
pub fn new() -> Self {
Self {
slots: [const { None }; MAX_CAPS_PER_PROCESS],
generations: SlotGenerations::new(),
}
}
/// Generation of an occupied slot.
pub fn slot_generation(&self, slot: CapSlot) -> Option<Generation> {
self.slots[slot.value() as usize].as_ref()?;
self.generations.current(slot.value() as usize)
}
pub fn get_mut(&mut self, slot: CapSlot) -> Result<&mut Capability, KernelError> {
self.slots[slot.value() as usize]
.as_mut()
.ok_or(KernelError::InvalidSlot(slot))
}
pub fn iter(&self) -> impl Iterator<Item = (CapSlot, &Capability)> {
self.slots.iter().enumerate().filter_map(|(i, c)| {
let cap = c.as_ref()?;
Some((CapSlot::new(i as u8)?, cap))
})
}
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
let idx = slot.value() as usize;
self.slots[idx]
@@ -71,10 +128,8 @@ impl CapabilityTable {
}
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> {
let idx = self
.slots
.iter()
.position(|s| s.is_none())
let idx = (0..MAX_CAPS_PER_PROCESS)
.find(|&i| self.slots[i].is_none() && self.generations.is_usable(i))
.ok_or(KernelError::NoFreeSlot)?;
self.slots[idx] = Some(cap);
@@ -87,6 +142,7 @@ impl CapabilityTable {
return Err(KernelError::InvalidSlot(slot));
}
self.slots[idx] = None;
self.generations.advance(idx);
Ok(())
}
}

View File

@@ -109,7 +109,6 @@ fn restrict_to_cannot_add_permissions() {
Permission::Write,
Permission::Execute,
Permission::Delegate,
Permission::Revoke,
]);
let result = read_only.restrict_to(&wants_everything);
@@ -118,7 +117,6 @@ fn restrict_to_cannot_add_permissions() {
assert!(!result.allows(Permission::Write));
assert!(!result.allows(Permission::Execute));
assert!(!result.allows(Permission::Delegate));
assert!(!result.allows(Permission::Revoke));
}
#[test]
@@ -138,7 +136,6 @@ fn empty_permission_set_allows_nothing() {
assert!(!empty.allows(Permission::Write));
assert!(!empty.allows(Permission::Execute));
assert!(!empty.allows(Permission::Delegate));
assert!(!empty.allows(Permission::Revoke));
}
#[test]
@@ -150,3 +147,45 @@ fn capability_preserves_resource_and_permissions() {
assert_eq!(cap.permissions(), &expected_permissions);
assert_eq!(cap.generation(), &Generation::initial());
}
#[test]
fn removing_a_cap_changes_its_slot_generation() {
let mut table = CapabilityTable::new();
let slot = table.insert(endpoint_cap(1, &[Permission::Read])).unwrap();
let before = table.slot_generation(slot).unwrap();
table.remove(slot).unwrap();
assert_eq!(
table.slot_generation(slot),
None,
"empty slots have no generation"
);
let again = table.insert(endpoint_cap(2, &[Permission::Read])).unwrap();
assert_eq!(again, slot);
assert!(!before.is_valid_against(&table.slot_generation(again).unwrap()));
}
#[test]
fn iter_lists_occupied_slots() {
let mut table = CapabilityTable::new();
let a = table.insert(endpoint_cap(1, &[])).unwrap();
let b = table.insert(endpoint_cap(2, &[])).unwrap();
table.remove(a).unwrap();
let slots: [Option<types::bounded::CapSlot>; 2] = {
let mut it = table.iter().map(|(s, _)| s);
[it.next(), it.next()]
};
assert_eq!(slots, [Some(b), None]);
}
#[test]
fn caps_record_their_parent() {
let parent = CapRef {
process: types::id::ProcessId::from_raw(3),
process_generation: Generation::initial(),
slot: types::bounded::CapSlot::new(4).unwrap(),
slot_generation: Generation::initial(),
};
let cap = endpoint_cap(1, &[]).with_parent(Some(parent));
assert_eq!(cap.parent(), Some(parent));
assert_eq!(endpoint_cap(1, &[]).parent(), None);
}

View File

@@ -111,13 +111,6 @@ impl EndpointTable {
self.generations.current(id.raw() as usize)
}
/// Invalidates every existing capability to `id`; the endpoint lives on.
pub fn revoke(&mut self, id: EndpointId) {
if self.get(id).is_some() {
self.generations.advance(id.raw() as usize);
}
}
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = EndpointId> + '_ {
self.endpoints
.iter()

View File

@@ -102,18 +102,6 @@ fn senders_and_receivers_are_independent() {
assert_eq!(ep.dequeue_receiver(), Some(thread(2)));
}
#[test]
fn revoke_bumps_generation_but_keeps_the_endpoint() {
let mut table = EndpointTable::new();
let id = table.create(owner()).unwrap();
let gen_before = table.generation(id).unwrap();
table.revoke(id);
let gen_after = table.generation(id).unwrap();
assert!(!gen_before.is_valid_against(&gen_after));
assert!(table.get(id).is_some());
}
#[test]
fn reused_slot_gets_a_new_generation() {
let mut table = EndpointTable::new();

View File

@@ -1,4 +1,4 @@
use crate::generations::SlotGenerations;
use crate::{capability::CapRef, generations::SlotGenerations};
use types::{
address::{PhysicalAddress, VirtualAddress},
bounded::PageCount,
@@ -35,6 +35,9 @@ pub struct RegionMapping {
pub process: ProcessId,
pub va: VirtualAddress,
pub perms: PagePerms,
/// The capability the mapping was made through. Deleting that
/// capability removes the mapping; `None` for kernel-made mappings.
pub via: Option<CapRef>,
}
/// Physically contiguous memory that may be mapped into several address
@@ -132,13 +135,6 @@ impl RegionTable {
self.generations.current(id.raw() as usize)
}
/// Invalidates every existing capability to `id`; the region lives on.
pub fn revoke(&mut self, id: RegionId) {
if self.get(id).is_some() {
self.generations.advance(id.raw() as usize);
}
}
fn free_slot(&self) -> Option<usize> {
(0..MAX_REGIONS).find(|&i| self.regions[i].is_none() && self.generations.is_usable(i))
}

View File

@@ -26,6 +26,7 @@ fn mapping(process: u16, addr: usize) -> RegionMapping {
process: pid(process),
va: va(addr),
perms: PagePerms::ReadWrite,
via: None,
}
}

View File

@@ -152,12 +152,6 @@ impl NotificationTable {
self.generations.current(id.raw() as usize)
}
pub fn revoke(&mut self, id: NotificationId) {
if self.get(id).is_some() {
self.generations.advance(id.raw() as usize);
}
}
pub fn destroy(&mut self, id: NotificationId) -> Option<Notification> {
let n = self.slots.get_mut(id.raw() as usize)?.take()?;
self.generations.advance(id.raw() as usize);

View File

@@ -58,9 +58,6 @@ fn delegation_cannot_add_permissions_not_in_source() {
if !source.allows(Permission::Delegate) {
assert!(!result.allows(Permission::Delegate));
}
if !source.allows(Permission::Revoke) {
assert!(!result.allows(Permission::Revoke));
}
}
#[kani::proof]

View File

@@ -2,6 +2,7 @@ use types::{
address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
badge::Badge,
bounded::{CapSlot, PageCount},
mapping::PagePerms,
permission::PermissionSet,
};
@@ -68,9 +69,21 @@ pub enum Syscall {
Kill {
process: CapSlot,
},
/// Deletes every capability derived from `slot`, keeping `slot`.
Revoke {
slot: CapSlot,
},
/// Allocates a shared-memory region charged to the caller and returns
/// a capability to it.
CreateRegion {
pages: PageCount,
},
/// Maps the whole region named by `region` at `va` in the caller.
MapRegion {
region: CapSlot,
va: VirtualAddress,
perms: PagePerms,
},
/// Removes a capability from the caller's own table.
DropCap {
slot: CapSlot,