From 154599e3afe0469b084015f158ce21fed46fab0c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 20:41:03 +0100 Subject: [PATCH] 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 --- crates/adapter-riscv/src/trap/mod.rs | 13 + crates/domain/src/capability/derivation.rs | 83 ++++ .../domain/src/capability/derivation_tests.rs | 40 ++ crates/domain/src/capability/mod.rs | 68 +++- crates/domain/src/capability/tests.rs | 45 ++- crates/domain/src/endpoint/mod.rs | 7 - crates/domain/src/endpoint/tests.rs | 12 - crates/domain/src/memory/mod.rs | 12 +- crates/domain/src/memory/tests.rs | 1 + crates/domain/src/notification/mod.rs | 6 - crates/domain/src/proofs/delegation.rs | 3 - crates/domain/src/syscall.rs | 13 + crates/kernel/src/captree.rs | 168 ++++++++ crates/kernel/src/lib.rs | 11 +- crates/kernel/src/memory.rs | 30 ++ crates/kernel/src/proofs/delegation.rs | 3 - crates/kernel/src/proofs/mediation.rs | 1 - crates/kernel/src/syscall/capability.rs | 24 +- crates/kernel/src/syscall/ipc.rs | 24 +- crates/kernel/src/syscall/lifecycle.rs | 5 +- crates/kernel/src/syscall/memory.rs | 40 ++ crates/kernel/src/syscall/mod.rs | 7 + crates/kernel/src/tests/capability.rs | 144 +------ crates/kernel/src/tests/captree_model.rs | 326 ++++++++++++++++ crates/kernel/src/tests/helpers.rs | 27 ++ crates/kernel/src/tests/ipc.rs | 5 +- crates/kernel/src/tests/mod.rs | 2 + crates/kernel/src/tests/revocation.rs | 365 ++++++++++++++++++ crates/libkos/src/syscall.rs | 27 ++ crates/types/src/permission.rs | 1 - crates/types/src/syscall_abi.rs | 7 + docs/ipc.md | 21 +- 32 files changed, 1302 insertions(+), 239 deletions(-) create mode 100644 crates/domain/src/capability/derivation.rs create mode 100644 crates/domain/src/capability/derivation_tests.rs create mode 100644 crates/kernel/src/captree.rs create mode 100644 crates/kernel/src/tests/captree_model.rs create mode 100644 crates/kernel/src/tests/revocation.rs diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 484f4c8..9bc1f06 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -241,6 +241,19 @@ fn decode_syscall(frame: &TrapFrame) -> Option { stack: StackPointer::from_raw(a(2)), }, SYS_CREATE_ENDPOINT => Syscall::CreateEndpoint, + SYS_CREATE_REGION => Syscall::CreateRegion { + pages: PageCount::new(u16::try_from(a(0)).ok()?)?, + }, + SYS_MAP_REGION => Syscall::MapRegion { + region: slot(0)?, + va: VirtualAddress::from_raw(a(1)), + perms: match a(2) { + PERMS_READ_ONLY => types::mapping::PagePerms::ReadOnly, + PERMS_READ_WRITE => types::mapping::PagePerms::ReadWrite, + PERMS_READ_EXECUTE => types::mapping::PagePerms::ReadExecute, + _ => return None, + }, + }, SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a(0) as u8 }, SYS_MAP_PAGE => Syscall::MapPage { process: slot(0)?, diff --git a/crates/domain/src/capability/derivation.rs b/crates/domain/src/capability/derivation.rs new file mode 100644 index 0000000..8c3be8f --- /dev/null +++ b/crates/domain/src/capability/derivation.rs @@ -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(parents: &[Option; 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; 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::() % 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::() % N; + let marked = descendants(&parents, root); + for i in 0..N { + assert!(marked[i] == is_ancestor(&parents, root, i)); + } + } +} diff --git a/crates/domain/src/capability/derivation_tests.rs b/crates/domain/src/capability/derivation_tests.rs new file mode 100644 index 0000000..970024a --- /dev/null +++ b/crates/domain/src/capability/derivation_tests.rs @@ -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]); +} diff --git a/crates/domain/src/capability/mod.rs b/crates/domain/src/capability/mod.rs index 6d204d9..b12432b 100644 --- a/crates/domain/src/capability/mod.rs +++ b/crates/domain/src/capability/mod.rs @@ -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, } impl Capability { @@ -26,9 +48,23 @@ impl Capability { permissions, generation, badge: Badge::NONE, + parent: None, } } + pub fn with_parent(mut self, parent: Option) -> Self { + self.parent = parent; + self + } + + pub fn parent(&self) -> Option { + self.parent + } + + pub fn set_parent(&mut self, parent: Option) { + 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; MAX_CAPS_PER_PROCESS], + generations: SlotGenerations, } 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 { + 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 { + 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 { - 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(()) } } diff --git a/crates/domain/src/capability/tests.rs b/crates/domain/src/capability/tests.rs index a234986..86cef66 100644 --- a/crates/domain/src/capability/tests.rs +++ b/crates/domain/src/capability/tests.rs @@ -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; 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); +} diff --git a/crates/domain/src/endpoint/mod.rs b/crates/domain/src/endpoint/mod.rs index 7dbe6cc..e9daa10 100644 --- a/crates/domain/src/endpoint/mod.rs +++ b/crates/domain/src/endpoint/mod.rs @@ -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 + '_ { self.endpoints .iter() diff --git a/crates/domain/src/endpoint/tests.rs b/crates/domain/src/endpoint/tests.rs index b2c99cf..8baa468 100644 --- a/crates/domain/src/endpoint/tests.rs +++ b/crates/domain/src/endpoint/tests.rs @@ -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(); diff --git a/crates/domain/src/memory/mod.rs b/crates/domain/src/memory/mod.rs index e0186d3..c2625ef 100644 --- a/crates/domain/src/memory/mod.rs +++ b/crates/domain/src/memory/mod.rs @@ -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, } /// 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 { (0..MAX_REGIONS).find(|&i| self.regions[i].is_none() && self.generations.is_usable(i)) } diff --git a/crates/domain/src/memory/tests.rs b/crates/domain/src/memory/tests.rs index b2855fa..f1ce34f 100644 --- a/crates/domain/src/memory/tests.rs +++ b/crates/domain/src/memory/tests.rs @@ -26,6 +26,7 @@ fn mapping(process: u16, addr: usize) -> RegionMapping { process: pid(process), va: va(addr), perms: PagePerms::ReadWrite, + via: None, } } diff --git a/crates/domain/src/notification/mod.rs b/crates/domain/src/notification/mod.rs index e76c52d..beb0a7e 100644 --- a/crates/domain/src/notification/mod.rs +++ b/crates/domain/src/notification/mod.rs @@ -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 { let n = self.slots.get_mut(id.raw() as usize)?.take()?; self.generations.advance(id.raw() as usize); diff --git a/crates/domain/src/proofs/delegation.rs b/crates/domain/src/proofs/delegation.rs index a3cc131..5dc2c6c 100644 --- a/crates/domain/src/proofs/delegation.rs +++ b/crates/domain/src/proofs/delegation.rs @@ -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] diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index 7dbd770..f33c164 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -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, diff --git a/crates/kernel/src/captree.rs b/crates/kernel/src/captree.rs new file mode 100644 index 0000000..7734de4 --- /dev/null +++ b/crates/kernel/src/captree.rs @@ -0,0 +1,168 @@ +//! The capability derivation tree. Every derived capability (Mint, +//! Delegate, IPC transfer) records its parent; `Revoke` deletes a subtree +//! and deleting a capability re-parents its children, so the tree can only +//! shrink by explicit deletion, never by a middleman stepping out. + +use domain::{ + capability::{CapRef, Capability, MAX_CAPS_PER_PROCESS, derivation::descendants}, + error::KernelError, + process::MAX_PROCESSES, +}; +use platform::Platform; +use types::{ + bounded::CapSlot, + id::{ProcessId, RegionId}, +}; + +use crate::Kernel; + +/// Every capability slot in the system, flattened. +const ALL_CAPS: usize = MAX_PROCESSES * MAX_CAPS_PER_PROCESS; + +fn flat(pid: ProcessId, slot: CapSlot) -> usize { + pid.raw() as usize * MAX_CAPS_PER_PROCESS + slot.value() as usize +} + +fn unflat(i: usize) -> Option<(ProcessId, CapSlot)> { + let pid = ProcessId::from_raw((i / MAX_CAPS_PER_PROCESS) as u16); + let slot = CapSlot::new((i % MAX_CAPS_PER_PROCESS) as u8)?; + Some((pid, slot)) +} + +impl Kernel

{ + /// A reference to the capability currently in `slot` of `pid`. + pub(crate) fn cap_ref(&self, pid: ProcessId, slot: CapSlot) -> Option { + let process = self.processes.get(pid).ok()?; + Some(CapRef { + process: pid, + process_generation: self.processes.generation(pid)?, + slot, + slot_generation: process.capabilities().slot_generation(slot)?, + }) + } + + /// Whether `r` still names a live capability. + pub(crate) fn resolves(&self, r: CapRef) -> bool { + self.cap_ref(r.process, r.slot) == Some(r) + } + + /// Stores `cap` in `to`'s table as a child of `parent`. + pub(crate) fn insert_derived( + &mut self, + to: ProcessId, + cap: Capability, + parent: Option, + ) -> Result { + self.processes + .get_mut(to)? + .capabilities_mut() + .insert(cap.with_parent(parent)) + } + + /// Calls `f` with every live capability's flat index and reference. + fn for_each_cap(&self, mut f: impl FnMut(usize, CapRef, &Capability)) { + for p in 0..MAX_PROCESSES { + let pid = ProcessId::from_raw(p as u16); + let Ok(process) = self.processes.get(pid) else { + continue; + }; + for (slot, cap) in process.capabilities().iter() { + if let Some(r) = self.cap_ref(pid, slot) { + f(flat(pid, slot), r, cap); + } + } + } + } + + /// Removes every region mapping made through `via`. + fn unmap_mappings_via(&mut self, via: CapRef) { + let mut doomed: [Option; domain::memory::MAX_REGIONS] = + [None; domain::memory::MAX_REGIONS]; + for (slot, (id, _)) in doomed.iter_mut().zip( + self.regions + .iter() + .filter(|(_, r)| r.mappings().any(|m| m.via == Some(via))), + ) { + *slot = Some(id); + } + for id in doomed.into_iter().flatten() { + self.unmap_region_from(id, via.process); + } + } + + /// Removes one capability: its mappings go, its children are adopted + /// by its parent. + pub(crate) fn delete_cap(&mut self, pid: ProcessId, slot: CapSlot) -> Result<(), KernelError> { + let me = self + .cap_ref(pid, slot) + .ok_or(KernelError::InvalidSlot(slot))?; + let grandparent = self.processes.get(pid)?.capabilities().get(slot)?.parent(); + self.unmap_mappings_via(me); + + let mut children = [false; ALL_CAPS]; + self.for_each_cap(|i, _, cap| { + if cap.parent() == Some(me) { + children[i] = true; + } + }); + for (i, _) in children.iter().enumerate().filter(|(_, c)| **c) { + if let Some((cpid, cslot)) = unflat(i) + && let Ok(p) = self.processes.get_mut(cpid) + && let Ok(child) = p.capabilities_mut().get_mut(cslot) + { + child.set_parent(grandparent); + } + } + self.processes.get_mut(pid)?.capabilities_mut().remove(slot) + } + + /// Deletes every capability derived from `slot` of `pid`, transitively, + /// and keeps `slot` itself. + pub(crate) fn revoke_subtree( + &mut self, + pid: ProcessId, + slot: CapSlot, + ) -> Result<(), KernelError> { + if self.cap_ref(pid, slot).is_none() { + return Err(KernelError::InvalidSlot(slot)); + } + // Snapshot the tree as parent indices. A parent that no longer + // resolves cannot exist (deletion re-parents), but is treated as + // a root rather than trusted. + let mut parents: [Option; ALL_CAPS] = [None; ALL_CAPS]; + self.for_each_cap(|i, _, cap| { + parents[i] = cap + .parent() + .filter(|r| self.resolves(*r)) + .map(|r| flat(r.process, r.slot) as u16); + }); + let root = flat(pid, slot); + let marked = descendants(&parents, root); + for (i, _) in marked.iter().enumerate().filter(|(i, m)| **m && *i != root) { + if let Some((dpid, dslot)) = unflat(i) + && let Some(r) = self.cap_ref(dpid, dslot) + { + self.unmap_mappings_via(r); + let _ = self + .processes + .get_mut(dpid) + .map(|p| p.capabilities_mut().remove(dslot)); + } + } + Ok(()) + } + + /// Deletes all of a dying process's capabilities, re-parenting their + /// children so revocation still reaches them. + pub(crate) fn delete_all_caps(&mut self, pid: ProcessId) { + let mut slots = [None; MAX_CAPS_PER_PROCESS]; + if let Ok(p) = self.processes.get(pid) { + for (out, (slot, _)) in slots.iter_mut().zip(p.capabilities().iter()) { + *out = Some(slot); + } + } + for slot in slots.into_iter().flatten() { + let _ = self.delete_cap(pid, slot); + } + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 45a46b0..403c87e 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] +mod captree; pub mod interrupt; mod memory; #[cfg(kani)] @@ -278,6 +279,7 @@ impl Kernel

{ }; self.cleanup_threads(&thread_ids); + self.delete_all_caps(pid); self.destroy_endpoints_owned_by(pid); self.destroy_notifications_owned_by(pid); self.teardown_memory(pid); @@ -373,15 +375,6 @@ impl Kernel

{ } } - pub fn revoke_resource(&mut self, resource: &Resource) { - match resource { - Resource::Endpoint(id) => self.endpoints.revoke(*id), - Resource::Memory(id) => self.regions.revoke(*id), - Resource::Notification(id) => self.notifications.revoke(*id), - _ => {} - } - } - fn grant_cap( &mut self, to: ProcessId, diff --git a/crates/kernel/src/memory.rs b/crates/kernel/src/memory.rs index b665ca6..877ea3b 100644 --- a/crates/kernel/src/memory.rs +++ b/crates/kernel/src/memory.rs @@ -334,12 +334,28 @@ impl Kernel

{ } /// Maps all of region `id` at `va` in `pid`, all or nothing. + /// Maps region `id` for boot code; the mapping is not tied to any + /// capability and lasts until the region or the process dies. pub fn map_region( &mut self, id: RegionId, pid: ProcessId, va: VirtualAddress, perms: PagePerms, + ) -> Result<(), KernelError> { + self.map_region_via(id, pid, va, perms, None) + } + + /// Maps all of region `id` at `va` in `pid`, all or nothing. A mapping + /// made through a capability (`via`) is removed when that capability + /// is deleted. + pub(crate) fn map_region_via( + &mut self, + id: RegionId, + pid: ProcessId, + va: VirtualAddress, + perms: PagePerms, + via: Option, ) -> Result<(), KernelError> { self.processes.get(pid)?; let region = self.regions.get(id).ok_or(KernelError::InvalidRegion)?; @@ -366,6 +382,7 @@ impl Kernel

{ process: pid, va, perms, + via, }) }); if !recorded { @@ -377,6 +394,19 @@ impl Kernel

{ Ok(()) } + /// Removes `pid`'s mapping of region `id`, if it has one. + pub(crate) fn unmap_region_from(&mut self, id: RegionId, pid: ProcessId) { + let Some(region) = self.regions.get_mut(id) else { + return; + }; + let pages = region.pages().value() as usize; + if let Some(m) = region.remove_mapping(pid) { + for i in 0..pages { + self.platform.unmap_page(pid, page_at(m.va, i)); + } + } + } + /// Frees regions owned by `pid` (unmapping them from every other /// process) and forgets `pid`'s mappings of other regions. fn teardown_regions(&mut self, pid: ProcessId) { diff --git a/crates/kernel/src/proofs/delegation.rs b/crates/kernel/src/proofs/delegation.rs index 250a772..459080b 100644 --- a/crates/kernel/src/proofs/delegation.rs +++ b/crates/kernel/src/proofs/delegation.rs @@ -45,9 +45,6 @@ fn kernel_delegate_path_never_escalates() { if !original_perms.allows(Permission::Delegate) { assert!(!result.permissions().allows(Permission::Delegate)); } - if !original_perms.allows(Permission::Revoke) { - assert!(!result.permissions().allows(Permission::Revoke)); - } } #[kani::proof] diff --git a/crates/kernel/src/proofs/mediation.rs b/crates/kernel/src/proofs/mediation.rs index 4c72ccf..3a277ed 100644 --- a/crates/kernel/src/proofs/mediation.rs +++ b/crates/kernel/src/proofs/mediation.rs @@ -66,5 +66,4 @@ fn check_cap_faithfully_reflects_stored_permissions() { assert!( retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate) ); - assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke)); } diff --git a/crates/kernel/src/syscall/capability.rs b/crates/kernel/src/syscall/capability.rs index 46ed6ae..d6f4d4a 100644 --- a/crates/kernel/src/syscall/capability.rs +++ b/crates/kernel/src/syscall/capability.rs @@ -42,10 +42,8 @@ impl Kernel

{ Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; - let inserted = self - .processes - .get_mut(target_pid) - .and_then(|p| p.capabilities_mut().insert(copy)); + let parent = self.cap_ref(caller_pid, slot); + let inserted = self.insert_derived(target_pid, copy, parent); if let Err(e) = inserted { return SyscallOutcome::Complete(SyscallResult::Error(e)); } @@ -58,14 +56,10 @@ impl Kernel

{ caller_pid: types::id::ProcessId, slot: types::bounded::CapSlot, ) -> SyscallOutcome { - let resource = match self.check_cap(caller_pid, slot, types::permission::Permission::Revoke) - { - Ok(cap) => cap.resource().clone(), - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - - self.revoke_resource(&resource); - SyscallOutcome::Complete(SyscallResult::Ok) + SyscallOutcome::Complete(match self.revoke_subtree(caller_pid, slot) { + Ok(()) => SyscallResult::Ok, + Err(e) => SyscallResult::Error(e), + }) } pub(crate) fn handle_drop_cap( @@ -73,11 +67,7 @@ impl Kernel

{ caller_pid: types::id::ProcessId, slot: types::bounded::CapSlot, ) -> SyscallOutcome { - let result = self - .processes - .get_mut(caller_pid) - .and_then(|p| p.capabilities_mut().remove(slot)); - SyscallOutcome::Complete(match result { + SyscallOutcome::Complete(match self.delete_cap(caller_pid, slot) { Ok(()) => SyscallResult::Ok, Err(e) => SyscallResult::Error(e), }) diff --git a/crates/kernel/src/syscall/ipc.rs b/crates/kernel/src/syscall/ipc.rs index a6b8fc9..46fa2f5 100644 --- a/crates/kernel/src/syscall/ipc.rs +++ b/crates/kernel/src/syscall/ipc.rs @@ -150,12 +150,8 @@ impl Kernel

{ .check_cap(from, slot, Permission::Delegate) .ok()? .clone(); - self.processes - .get_mut(to) - .ok()? - .capabilities_mut() - .insert(copy) - .ok() + let parent = self.cap_ref(from, slot); + self.insert_derived(to, copy, parent).ok() } /// Hands `out` from `sender` to `receiver`. For a call, the receiver's @@ -361,13 +357,13 @@ impl Kernel

{ permissions: PermissionSet, ) -> Result { let source = self.check_cap(pid, slot, Permission::Delegate)?; - if !matches!( + let badgeable = matches!( source.resource(), Resource::Endpoint(_) | Resource::Notification(_) - ) { - return Err(KernelError::WrongObject); - } - if !source.badge().is_none() { + ); + // Any capability can be copied with narrower rights; only those a + // receiver can observe the badge of take one, and only once. + if (!badgeable && !badge.is_none()) || !source.badge().is_none() { return Err(KernelError::InvalidArgument); } let minted = Capability::new( @@ -376,10 +372,8 @@ impl Kernel

{ *source.generation(), ) .with_badge(badge); - self.processes - .get_mut(pid)? - .capabilities_mut() - .insert(minted) + let parent = self.cap_ref(pid, slot); + self.insert_derived(pid, minted, parent) } pub(crate) fn handle_create_endpoint(&mut self, pid: ProcessId) -> SyscallOutcome { diff --git a/crates/kernel/src/syscall/lifecycle.rs b/crates/kernel/src/syscall/lifecycle.rs index 9028e12..2279160 100644 --- a/crates/kernel/src/syscall/lifecycle.rs +++ b/crates/kernel/src/syscall/lifecycle.rs @@ -65,9 +65,12 @@ impl Kernel

{ match self.grant_cap( caller_pid, domain::resource::Resource::Process(new_pid), + // The creator has full authority over what it creates, + // including handing that authority on. types::permission::PermissionSet::empty() .with(types::permission::Permission::Execute) - .with(types::permission::Permission::Read), + .with(types::permission::Permission::Read) + .with(types::permission::Permission::Delegate), ) { Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Err(e) => { diff --git a/crates/kernel/src/syscall/memory.rs b/crates/kernel/src/syscall/memory.rs index 0fb8528..8e8b737 100644 --- a/crates/kernel/src/syscall/memory.rs +++ b/crates/kernel/src/syscall/memory.rs @@ -75,4 +75,44 @@ impl Kernel

{ .map(|()| SyscallResult::Ok), ) } + + pub(crate) fn create_region_cap( + &mut self, + pid: ProcessId, + pages: PageCount, + ) -> Result { + let id = self.create_pool_region(pid, pages)?; + let all = types::permission::PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Write) + .with(Permission::Execute) + .with(Permission::Delegate); + self.grant_cap(pid, Resource::Memory(id), all) + } + + /// Maps the region behind `slot` into the caller. The capability must + /// allow every access `perms` grants; the mapping lasts as long as + /// the capability. + pub(crate) fn map_region_cap( + &mut self, + pid: ProcessId, + slot: CapSlot, + va: VirtualAddress, + perms: PagePerms, + ) -> Result<(), KernelError> { + let cap = self.check_cap(pid, slot, Permission::Read)?; + let needed = match perms { + PagePerms::ReadOnly => Permission::Read, + PagePerms::ReadWrite => Permission::Write, + PagePerms::ReadExecute => Permission::Execute, + }; + if !cap.permissions().allows(needed) { + return Err(KernelError::PermissionDenied { slot, needed }); + } + let Resource::Memory(id) = *cap.resource() else { + return Err(KernelError::WrongObject); + }; + let via = self.cap_ref(pid, slot); + self.map_region_via(id, pid, va, perms, via) + } } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 450b47c..6e6c080 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -116,6 +116,13 @@ impl Kernel

{ Syscall::Kill { process } => self.handle_kill(pid, process), Syscall::Revoke { slot } => self.handle_revoke(pid, slot), Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot), + Syscall::CreateRegion { pages } => { + complete(self.create_region_cap(pid, pages).map(SyscallResult::Slot)) + } + Syscall::MapRegion { region, va, perms } => complete( + self.map_region_cap(pid, region, va, perms) + .map(|()| SyscallResult::Ok), + ), Syscall::Delegate { slot, to_endpoint, diff --git a/crates/kernel/src/tests/capability.rs b/crates/kernel/src/tests/capability.rs index 23cba6b..5975fac 100644 --- a/crates/kernel/src/tests/capability.rs +++ b/crates/kernel/src/tests/capability.rs @@ -1,10 +1,9 @@ use super::*; use crate::{SyscallOutcome, SyscallResult}; -use domain::resource::Resource; use domain::syscall::Syscall; use types::address::DataRef; use types::bounded::CapSlot; -use types::permission::{Permission, PermissionSet}; +use types::permission::PermissionSet; #[test] fn delegate_without_delegate_permission_fails() { @@ -57,147 +56,6 @@ fn delegate_with_valid_cap_succeeds() { // --- Generation-based revocation --- -#[test] -fn revoke_resource_invalidates_all_caps() { - let (mut k, pid) = kernel_with_process(); - let ep_id = k.create_endpoint_for_test(); - - let cap_slot = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep_id), - PermissionSet::empty().with(Permission::Read), - ); - - // Revoke the endpoint - k.revoke_resource(&Resource::Endpoint(ep_id)); - - // Cap is now invalid — check_cap should reject with RevokedCapability - let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::RevokedCapability(_) - )) - )); -} - -#[test] -fn new_cap_after_revoke_uses_current_generation() { - let (mut k, pid) = kernel_with_process(); - let ep_id = k.create_endpoint_for_test(); - - // Grant, revoke, grant again - let old_slot = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep_id), - PermissionSet::empty().with(Permission::Read), - ); - - k.revoke_resource(&Resource::Endpoint(ep_id)); - - // Old cap is dead - let outcome = k.handle_syscall(Syscall::Receive { endpoint: old_slot }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::RevokedCapability(_) - )) - )); - - // New cap with current generation works - let new_slot = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep_id), - PermissionSet::empty().with(Permission::Read), - ); - - let outcome = k.handle_syscall(Syscall::Receive { endpoint: new_slot }); - // Should block (no sender), not error — proving the cap is valid - assert!(matches!(outcome, SyscallOutcome::BlockCaller(_))); -} - -#[test] -fn revoke_does_not_affect_other_resources() { - let (mut k, pid) = kernel_with_process(); - let ep1 = k.create_endpoint_for_test(); - let ep2 = k.create_endpoint_for_test(); - - let slot1 = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep1), - PermissionSet::empty().with(Permission::Read), - ); - let slot2 = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep2), - PermissionSet::empty().with(Permission::Read), - ); - - // Revoke only ep1 - k.revoke_resource(&Resource::Endpoint(ep1)); - - // ep1 cap is revoked - let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot1 }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::RevokedCapability(_) - )) - )); - - // ep2 cap still works - let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot2 }); - assert!(matches!(outcome, SyscallOutcome::BlockCaller(_))); -} - -#[test] -fn revoke_syscall_invalidates_via_generation() { - let (mut k, pid) = kernel_with_process(); - let ep_id = k.create_endpoint_for_test(); - - let cap_slot = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep_id), - PermissionSet::empty() - .with(Permission::Read) - .with(Permission::Revoke), - ); - - // Revoke via syscall - let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Ok) - )); - - // The cap itself is now revoked (generation mismatch) - let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::RevokedCapability(_) - )) - )); -} - -#[test] -fn revoke_syscall_without_revoke_permission_fails() { - let (mut k, pid) = kernel_with_process(); - let ep_id = k.create_endpoint_for_test(); - - let cap_slot = k.grant_cap_for_test( - pid, - Resource::Endpoint(ep_id), - PermissionSet::empty().with(Permission::Read), - ); - - let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error(_)) - )); -} - #[test] fn every_representable_slot_is_rejected_not_panicking() { let (mut k, _) = kernel_with_process(); diff --git a/crates/kernel/src/tests/captree_model.rs b/crates/kernel/src/tests/captree_model.rs new file mode 100644 index 0000000..bb9de30 --- /dev/null +++ b/crates/kernel/src/tests/captree_model.rs @@ -0,0 +1,326 @@ +//! Randomized check of the derivation tree against an independent model. +//! The model applies the rules of docs/ipc.md (derive records a parent, +//! delete re-parents, revoke removes the subtree, death deletes everything +//! a process holds) to its own data; after every step the kernel must hold +//! exactly the capabilities the model says are alive. + +extern crate std; + +use std::collections::HashMap; +use std::vec::Vec; + +use super::*; +use crate::{SyscallOutcome, SyscallResult}; +use domain::syscall::Syscall; +use types::address::{DataRef, VirtualAddress}; +use types::badge::Badge; +use types::bounded::{CapSlot, PageCount}; +use types::id::ThreadId; +use types::mapping::PagePerms; +use types::permission::{Permission, PermissionSet}; + +const STEPS: usize = 1500; + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + + fn pick(&mut self, xs: &[T]) -> Option { + (!xs.is_empty()).then(|| xs[self.below(xs.len())]) + } +} + +#[derive(Clone, Copy)] +struct Node { + parent: Option, + is_region: bool, +} + +/// The reference model: logical capabilities and where each one lives. +#[derive(Default)] +struct Model { + nodes: Vec, + alive: HashMap, +} + +impl Model { + fn add(&mut self, at: (ProcessId, CapSlot), parent: Option, is_region: bool) -> usize { + self.nodes.push(Node { parent, is_region }); + let id = self.nodes.len() - 1; + self.alive.insert(id, at); + id + } + + fn delete(&mut self, id: usize) { + let grandparent = self.nodes[id].parent; + for n in 0..self.nodes.len() { + if self.alive.contains_key(&n) && self.nodes[n].parent == Some(id) { + self.nodes[n].parent = grandparent; + } + } + self.alive.remove(&id); + } + + fn is_descendant(&self, mut n: usize, root: usize) -> bool { + while let Some(p) = self.nodes[n].parent { + if p == root { + return true; + } + n = p; + } + false + } + + fn revoke(&mut self, root: usize) { + let doomed: Vec = self + .alive + .keys() + .copied() + .filter(|&n| self.is_descendant(n, root)) + .collect(); + for n in doomed { + self.alive.remove(&n); + } + } + + fn held_by(&self, pid: ProcessId) -> Vec<(usize, CapSlot)> { + self.alive + .iter() + .filter(|(_, (p, _))| *p == pid) + .map(|(id, (_, s))| (*id, *s)) + .collect() + } +} + +fn all() -> PermissionSet { + PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Write) + .with(Permission::Execute) + .with(Permission::Delegate) +} + +struct World { + k: Kernel, + m: Model, + pids: [ProcessId; 3], + tids: [ThreadId; 3], +} + +impl World { + fn new() -> Self { + let (k, a) = kernel_with_process(); + let ta = k.current_thread().unwrap(); + let mut w = Self { + k, + m: Model::default(), + pids: [a; 3], + tids: [ta; 3], + }; + // init's own boot capability (ProcessCreate) is a kernel root. + let create = CapSlot::new(0).unwrap(); + w.m.add((a, create), None, false); + for i in 1..3 { + w.spawn(i); + } + for call in [Syscall::CreateEndpoint, Syscall::CreateNotification] { + let s = w.slot(0, call); + w.m.add((a, s), None, false); + } + let r = w.slot( + 0, + Syscall::CreateRegion { + pages: PageCount::new(1).unwrap(), + }, + ); + w.m.add((a, r), None, true); + w + } + + fn slot(&mut self, who: usize, call: Syscall) -> CapSlot { + self.k.switch_to_thread_for_test(self.tids[who]); + match self.k.handle_syscall(call) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("expected a slot"), + } + } + + /// (Re)creates process `i` from init; init gets a root process cap. + fn spawn(&mut self, i: usize) { + let cap = self.slot( + 0, + Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + }, + ); + self.pids[i] = self.k.process_of_cap_for_test(cap); + self.tids[i] = + self.k + .spawn_thread_for_test(self.pids[i], Priority::DEFAULT, TimeSlice::DEFAULT); + self.m.add((self.pids[0], cap), None, false); + } + + fn run(&mut self, who: usize, call: Syscall) -> bool { + self.k.switch_to_thread_for_test(self.tids[who]); + !matches!( + self.k.handle_syscall(call), + SyscallOutcome::Complete(SyscallResult::Error(_)) + ) + } + + fn step(&mut self, rng: &mut Rng) { + let who = rng.below(3); + let pid = self.pids[who]; + let held = self.m.held_by(pid); + let Some((id, slot)) = rng.pick(&held) else { + return; + }; + + match rng.below(10) { + 0..=2 => { + // Badges only go on endpoints and notifications; the kernel + // must refuse the rest, so try both. + let badge = Badge::new(rng.below(2)); + let before = self.k.cap_slots_for_test(pid); + let ok = self.run( + who, + Syscall::Mint { + slot, + badge, + permissions: all(), + }, + ); + if ok { + let new = newly_used(before, self.k.cap_slots_for_test(pid)); + let is_region = self.m.nodes[id].is_region; + self.m.add((pid, new), Some(id), is_region); + } + } + 3..=4 => { + // Delegate through any process cap the caller holds. + let proc_caps: Vec<(CapSlot, ProcessId)> = held + .iter() + .filter_map(|(_, s)| { + self.k.process_cap_target_for_test(pid, *s).map(|t| (*s, t)) + }) + .collect(); + if let Some((to, target)) = rng.pick(&proc_caps) { + let before = self.k.cap_slots_for_test(target); + let ok = self.run( + who, + Syscall::Delegate { + slot, + to_endpoint: to, + permissions: all(), + }, + ); + if ok { + let new = newly_used(before, self.k.cap_slots_for_test(target)); + let is_region = self.m.nodes[id].is_region; + self.m.add((target, new), Some(id), is_region); + } + } + } + // init keeps its ProcessCreate cap so processes can be re-made. + 5 if !(who == 0 && slot == CapSlot::new(0).unwrap()) => { + if self.run(who, Syscall::DropCap { slot }) { + self.m.delete(id); + } + } + 6..=7 => { + assert!( + self.run(who, Syscall::Revoke { slot }), + "revoking a held cap failed" + ); + self.m.revoke(id); + } + 8 => { + let va = VirtualAddress::from_raw(0x30000 + rng.below(16) * 0x1000); + let _ = self.run( + who, + Syscall::MapRegion { + region: slot, + va, + perms: PagePerms::ReadWrite, + }, + ); + } + _ if who == 0 && rng.below(4) == 0 => { + let victim = 1 + rng.below(2); + let vp = self.pids[victim]; + // init's cap to the victim may already be revoked; kill it + // directly, as a supervisor holding a cap would. + for (n, _) in self.m.held_by(vp) { + self.m.delete(n); + } + self.k.destroy_process(vp); + self.spawn(victim); + } + _ => {} + } + } + + fn check(&self) { + for &pid in &self.pids { + let used = self.k.cap_slots_for_test(pid); + let mut expected = [false; 64]; + for (_, s) in self.m.held_by(pid) { + expected[s.value() as usize] = true; + } + assert_eq!(used, expected, "{pid:?} holds the wrong capabilities"); + } + // Every capability-backed mapping is backed by a live capability + // of the mapping process. + for (_, region) in self.k.regions.iter() { + for m in region.mappings() { + if let Some(via) = m.via { + assert!(self.k.resolves(via), "mapping outlived its capability"); + assert_eq!(via.process, m.process); + } + } + } + } +} + +fn newly_used(before: [bool; 64], after: [bool; 64]) -> CapSlot { + (0..64) + .find(|&i| after[i] && !before[i]) + .and_then(|i| CapSlot::new(i as u8)) + .expect("a new slot") +} + +#[test] +fn derivation_tree_matches_the_model() { + for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024] { + let mut world = World::new(); + let mut rng = Rng(seed); + let mut revokes = 0; + for _ in 0..STEPS { + let before = world.m.alive.len(); + world.step(&mut rng); + if world.m.alive.len() < before { + revokes += 1; + } + world.check(); + } + assert!( + revokes > 20, + "seed {seed:#x}: too little deletion exercised" + ); + assert!( + world.m.nodes.len() > 100, + "seed {seed:#x}: too little derivation" + ); + } +} diff --git a/crates/kernel/src/tests/helpers.rs b/crates/kernel/src/tests/helpers.rs index ca732b3..86f1a22 100644 --- a/crates/kernel/src/tests/helpers.rs +++ b/crates/kernel/src/tests/helpers.rs @@ -128,6 +128,33 @@ impl Kernel { self.destroy_notifications_owned_by(owner); } + /// The process a Process cap in `pid`'s table names, if it is one. + pub fn process_cap_target_for_test(&self, pid: ProcessId, slot: CapSlot) -> Option { + match self + .processes + .get(pid) + .ok()? + .capabilities() + .get(slot) + .ok()? + .resource() + { + Resource::Process(target) if self.process_exists(*target) => Some(*target), + _ => None, + } + } + + /// Which slots of `pid`'s capability table are occupied. + pub fn cap_slots_for_test(&self, pid: ProcessId) -> [bool; 64] { + let mut used = [false; 64]; + if let Ok(p) = self.processes.get(pid) { + for (slot, _) in p.capabilities().iter() { + used[slot.value() as usize] = true; + } + } + used + } + pub fn thread_state_for_test(&self, tid: ThreadId) -> Option { self.threads.get(tid).ok().map(|t| *t.state()) } diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index 5853a0c..4ed2bd6 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -613,7 +613,7 @@ fn transfer_needs_delegate_on_the_transferred_cap() { } #[test] -fn a_cap_revoked_while_the_sender_waits_is_not_delivered() { +fn a_cap_dropped_while_the_sender_waits_is_not_delivered() { let mut t = Ipc::new(); let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv); let other = t.k.create_endpoint(a).unwrap(); @@ -626,7 +626,8 @@ fn a_cap_revoked_while_the_sender_waits_is_not_delivered() { message: msg([0; 4]).with_cap(gift), }, ); - t.k.revoke_resource(&Resource::Endpoint(other)); + // A is blocked sending; delete the cap as its death or a revoke would. + t.k.delete_cap(a, gift).unwrap(); let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); assert_eq!(d.cap, None); } diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index b62466a..19c372e 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -2,6 +2,7 @@ mod helpers; mod mock_platform; mod capability; +mod captree_model; mod exit; mod invariants; mod ipc; @@ -10,6 +11,7 @@ mod isolation; mod lifecycle; mod lifetimes; mod memory; +mod revocation; mod simple; mod tick; diff --git a/crates/kernel/src/tests/revocation.rs b/crates/kernel/src/tests/revocation.rs new file mode 100644 index 0000000..7d6a2cc --- /dev/null +++ b/crates/kernel/src/tests/revocation.rs @@ -0,0 +1,365 @@ +//! Revocation through the capability derivation tree: `Revoke(cap)` +//! deletes everything derived from `cap`, transitively, across processes, +//! and keeps `cap`. Mappings made through a deleted capability go with it. + +use super::*; +use crate::{SyscallOutcome, SyscallResult}; +use domain::message::Message; +use domain::resource::Resource; +use domain::syscall::Syscall; +use types::address::{DataRef, VirtualAddress}; +use types::badge::Badge; +use types::bounded::{CapSlot, PageCount}; +use types::id::ThreadId; +use types::mapping::PagePerms; +use types::permission::{Permission, PermissionSet}; + +fn perms(ps: &[Permission]) -> PermissionSet { + ps.iter().fold(PermissionSet::empty(), |s, p| s.with(*p)) +} + +fn all() -> PermissionSet { + perms(&[ + Permission::Read, + Permission::Write, + Permission::Execute, + Permission::Delegate, + ]) +} + +fn ok(outcome: &SyscallOutcome) -> bool { + !matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))) +} + +fn slot_of(outcome: SyscallOutcome) -> CapSlot { + match outcome { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("expected a slot"), + } +} + +/// Init (A) plus B and C, each with one thread; A holds process caps to +/// both and a root capability to an endpoint it owns. +struct Tree { + k: Kernel, + a: ProcessId, + b: ProcessId, + c: ProcessId, + ta: ThreadId, + tb: ThreadId, + a_to_b: CapSlot, + a_to_c: CapSlot, + root: CapSlot, +} + +impl Tree { + fn new() -> Self { + let (mut k, a) = kernel_with_process(); + let ta = k.current_thread().unwrap(); + let create = || Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + }; + let a_to_b = slot_of(k.handle_syscall(create())); + let a_to_c = slot_of(k.handle_syscall(create())); + let (b, c) = (ProcessId::from_raw(1), ProcessId::from_raw(2)); + let tb = k.spawn_thread_for_test(b, Priority::DEFAULT, TimeSlice::DEFAULT); + k.spawn_thread_for_test(c, Priority::DEFAULT, TimeSlice::DEFAULT); + let root = slot_of(k.handle_syscall(Syscall::CreateEndpoint)); + Self { + k, + a, + b, + c, + ta, + tb, + a_to_b, + a_to_c, + root, + } + } + + fn run(&mut self, tid: ThreadId, call: Syscall) -> SyscallOutcome { + self.k.switch_to_thread_for_test(tid); + self.k.handle_syscall(call) + } + + fn mint(&mut self, tid: ThreadId, slot: CapSlot, badge: usize) -> CapSlot { + slot_of(self.run( + tid, + Syscall::Mint { + slot, + badge: Badge::new(badge), + permissions: all(), + }, + )) + } + + /// Gives `slot` (in the caller's table) to the process behind `to`; + /// returns where it landed there. + fn give(&mut self, tid: ThreadId, slot: CapSlot, to: CapSlot, target: ProcessId) -> CapSlot { + let before = self.k.cap_slots_for_test(target); + assert!(ok(&self.run( + tid, + Syscall::Delegate { + slot, + to_endpoint: to, + permissions: all() + } + ))); + let after = self.k.cap_slots_for_test(target); + (0..=CapSlot::MAX) + .map(|n| CapSlot::new(n).unwrap()) + .find(|s| after[s.value() as usize] && !before[s.value() as usize]) + .expect("delegated cap") + } + + fn holds(&self, pid: ProcessId, slot: CapSlot) -> bool { + self.k.cap_slots_for_test(pid)[slot.value() as usize] + } +} + +#[test] +fn revoke_deletes_minted_children_but_keeps_the_cap_itself() { + let mut t = Tree::new(); + let (a, ta, root) = (t.a, t.ta, t.root); + let m1 = t.mint(ta, root, 1); + let m2 = t.mint(ta, root, 2); + assert!(ok(&t.run(ta, Syscall::Revoke { slot: root }))); + assert!(!t.holds(a, m1) && !t.holds(a, m2)); + assert!(t.holds(a, root)); + // The root still works. + assert!(ok(&t.run( + ta, + Syscall::Mint { + slot: root, + badge: Badge::new(3), + permissions: all() + } + ))); +} + +#[test] +fn revoke_reaches_grandchildren_in_other_processes() { + let mut t = Tree::new(); + let (a, b, c, ta, tb, root, a_to_b) = (t.a, t.b, t.c, t.ta, t.tb, t.root, t.a_to_b); + let m = t.mint(ta, root, 7); + let in_b = t.give(ta, m, a_to_b, b); + // B passes it on to C through a process cap A delegated to B. + let b_to_c = t.give(ta, t.a_to_c, a_to_b, b); + let in_c = t.give(tb, in_b, b_to_c, c); + + assert!(ok(&t.run(ta, Syscall::Revoke { slot: m }))); + assert!(!t.holds(b, in_b), "child survived"); + assert!(!t.holds(c, in_c), "grandchild survived"); + assert!(t.holds(a, m) && t.holds(a, root)); +} + +#[test] +fn revoking_one_client_spares_another() { + let mut t = Tree::new(); + let (b, c, ta, root, a_to_b, a_to_c) = (t.b, t.c, t.ta, t.root, t.a_to_b, t.a_to_c); + let for_b = t.mint(ta, root, 1); + let for_c = t.mint(ta, root, 2); + let in_b = t.give(ta, for_b, a_to_b, b); + let in_c = t.give(ta, for_c, a_to_c, c); + assert!(ok(&t.run(ta, Syscall::Revoke { slot: for_b }))); + assert!(!t.holds(b, in_b)); + assert!(t.holds(c, in_c)); +} + +#[test] +fn dropping_a_middleman_does_not_shield_the_grandchild() { + let mut t = Tree::new(); + let (b, c, ta, tb, root, a_to_b) = (t.b, t.c, t.ta, t.tb, t.root, t.a_to_b); + let m = t.mint(ta, root, 7); + let in_b = t.give(ta, m, a_to_b, b); + let b_to_c = t.give(ta, t.a_to_c, a_to_b, b); + let in_c = t.give(tb, in_b, b_to_c, c); + + assert!(ok(&t.run(tb, Syscall::DropCap { slot: in_b }))); + assert!(ok(&t.run(ta, Syscall::Revoke { slot: m }))); + assert!(!t.holds(c, in_c), "grandchild escaped revocation"); +} + +#[test] +fn a_dying_middleman_does_not_shield_the_grandchild() { + let mut t = Tree::new(); + let (b, c, ta, tb, root, a_to_b) = (t.b, t.c, t.ta, t.tb, t.root, t.a_to_b); + let m = t.mint(ta, root, 7); + let in_b = t.give(ta, m, a_to_b, b); + let b_to_c = t.give(ta, t.a_to_c, a_to_b, b); + let in_c = t.give(tb, in_b, b_to_c, c); + + assert!(ok(&t.run(ta, Syscall::Kill { process: a_to_b }))); + assert!(ok(&t.run(ta, Syscall::Revoke { slot: m }))); + assert!(!t.holds(c, in_c), "grandchild escaped revocation"); +} + +#[test] +fn a_capability_sent_over_ipc_is_revocable() { + let mut t = Tree::new(); + let (a, b, ta, tb, root, a_to_b) = (t.a, t.b, t.ta, t.tb, t.root, t.a_to_b); + let ep = t.k.create_endpoint(a).unwrap(); + let send = t.k.grant_cap_for_test(a, Resource::Endpoint(ep), all()); + let recv = t.k.grant_cap_for_test(b, Resource::Endpoint(ep), all()); + let gift = t.mint(ta, root, 9); + t.run(tb, Syscall::Receive { endpoint: recv }); + t.run( + ta, + Syscall::Send { + endpoint: send, + message: Message::empty().with_cap(gift), + }, + ); + let received = match t.k.take_pending_result(tb) { + Some(SyscallResult::Received(d)) => d.cap.expect("cap transferred"), + _ => panic!("B got nothing"), + }; + assert!(ok(&t.run(ta, Syscall::Revoke { slot: gift }))); + assert!(!t.holds(b, received)); + let _ = a_to_b; +} + +#[test] +fn a_holder_can_only_revoke_its_own_subtree() { + let mut t = Tree::new(); + let (a, b, c, ta, tb, root, a_to_b) = (t.a, t.b, t.c, t.ta, t.tb, t.root, t.a_to_b); + let m = t.mint(ta, root, 7); + let in_b = t.give(ta, m, a_to_b, b); + let b_to_c = t.give(ta, t.a_to_c, a_to_b, b); + let in_c = t.give(tb, in_b, b_to_c, c); + let sibling = t.mint(ta, root, 8); + + assert!(ok(&t.run(tb, Syscall::Revoke { slot: in_b }))); + assert!(!t.holds(c, in_c)); + assert!(t.holds(b, in_b)); + assert!(t.holds(a, m) && t.holds(a, sibling) && t.holds(a, root)); +} + +#[test] +fn revoking_an_empty_slot_is_an_error() { + let mut t = Tree::new(); + let ta = t.ta; + assert!(!ok(&t.run( + ta, + Syscall::Revoke { + slot: CapSlot::new(40).unwrap() + } + ))); +} + +// ---- memory ---- + +fn region(t: &mut Tree, tid: ThreadId, pages: u16) -> CapSlot { + slot_of(t.run( + tid, + Syscall::CreateRegion { + pages: PageCount::new(pages).unwrap(), + }, + )) +} + +fn map(t: &mut Tree, tid: ThreadId, cap: CapSlot, va: usize, p: PagePerms) -> bool { + ok(&t.run( + tid, + Syscall::MapRegion { + region: cap, + va: VirtualAddress::from_raw(va), + perms: p, + }, + )) +} + +#[test] +fn map_region_maps_every_page() { + let mut t = Tree::new(); + let (a, ta) = (t.a, t.ta); + let r = region(&mut t, ta, 2); + assert!(map(&mut t, ta, r, 0x30000, PagePerms::ReadWrite)); + let p0 = t.k.platform_mut().mapping(a, 0x30000).unwrap(); + assert_eq!(t.k.platform_mut().mapping(a, 0x31000), Some(p0 + 4096)); +} + +#[test] +fn map_region_needs_the_matching_permissions() { + let mut t = Tree::new(); + let (a, ta) = (t.a, t.ta); + let r = region(&mut t, ta, 1); + let read_only = slot_of(t.run( + ta, + Syscall::Mint { + slot: r, + badge: Badge::NONE, + permissions: perms(&[Permission::Read]), + }, + )); + assert!(!map(&mut t, ta, read_only, 0x30000, PagePerms::ReadWrite)); + assert!(!map(&mut t, ta, read_only, 0x30000, PagePerms::ReadExecute)); + assert!(map(&mut t, ta, read_only, 0x30000, PagePerms::ReadOnly)); + assert_eq!( + t.k.platform_mut().perms(a, 0x30000), + Some(PagePerms::ReadOnly) + ); +} + +#[test] +fn revoking_a_region_cap_unmaps_it_from_every_derived_holder() { + let mut t = Tree::new(); + let (a, b, ta, tb, a_to_b) = (t.a, t.b, t.ta, t.tb, t.a_to_b); + let r = region(&mut t, ta, 1); + assert!(map(&mut t, ta, r, 0x30000, PagePerms::ReadWrite)); + let in_b = t.give(ta, r, a_to_b, b); + assert!(map(&mut t, tb, in_b, 0x50000, PagePerms::ReadWrite)); + assert!(t.k.platform_mut().mapping(b, 0x50000).is_some()); + + assert!(ok(&t.run(ta, Syscall::Revoke { slot: r }))); + assert_eq!( + t.k.platform_mut().mapping(b, 0x50000), + None, + "revoked mapping still live" + ); + assert!( + t.k.platform_mut().mapping(a, 0x30000).is_some(), + "revoker lost its own mapping" + ); +} + +#[test] +fn dropping_the_cap_a_mapping_came_through_unmaps_it() { + let mut t = Tree::new(); + let (a, ta) = (t.a, t.ta); + let r = region(&mut t, ta, 1); + let copy = slot_of(t.run( + ta, + Syscall::Mint { + slot: r, + badge: Badge::NONE, + permissions: all(), + }, + )); + assert!(map(&mut t, ta, copy, 0x30000, PagePerms::ReadWrite)); + assert!(ok(&t.run(ta, Syscall::DropCap { slot: copy }))); + assert_eq!(t.k.platform_mut().mapping(a, 0x30000), None); +} + +#[test] +fn a_region_can_be_remapped_after_its_mapping_was_revoked() { + let mut t = Tree::new(); + let (b, ta, tb, a_to_b) = (t.b, t.ta, t.tb, t.a_to_b); + let r = region(&mut t, ta, 1); + let in_b = t.give(ta, r, a_to_b, b); + assert!(map(&mut t, tb, in_b, 0x50000, PagePerms::ReadWrite)); + t.run(ta, Syscall::Revoke { slot: r }); + let again = t.give(ta, r, a_to_b, b); + assert!(map(&mut t, tb, again, 0x50000, PagePerms::ReadWrite)); +} + +#[test] +fn create_region_is_charged_to_the_caller() { + let mut t = Tree::new(); + let (a, ta) = (t.a, t.ta); + let before = t.k.page_usage(a); + region(&mut t, ta, 3); + assert_eq!(t.k.page_usage(a), before + 3); +} diff --git a/crates/libkos/src/syscall.rs b/crates/libkos/src/syscall.rs index 9dcfb27..937708e 100644 --- a/crates/libkos/src/syscall.rs +++ b/crates/libkos/src/syscall.rs @@ -6,6 +6,7 @@ use types::address::{EntryPoint, StackPointer, VirtualAddress}; use types::badge::Badge; use types::bounded::{CapSlot, PageCount}; use types::id::ProcessId; +use types::mapping::PagePerms; use types::permission::PermissionSet; use types::syscall_abi::*; use types::time::Ticks; @@ -180,6 +181,30 @@ pub fn map_page( .is_ok() } +/// Allocates `pages` of physically contiguous shared memory, charged to +/// this process, and returns a capability to it. +pub fn create_region(pages: PageCount) -> Result { + slot_of(raw( + SYS_CREATE_REGION, + [pages.value() as usize, 0, 0, 0, 0, 0], + )) +} + +/// Maps the region behind `region` at `va`. The mapping lasts as long as +/// the capability: dropping or having it revoked unmaps it. +pub fn map_region(region: CapSlot, va: VirtualAddress, perms: PagePerms) -> Result<()> { + let perms = match perms { + PagePerms::ReadOnly => PERMS_READ_ONLY, + PagePerms::ReadWrite => PERMS_READ_WRITE, + PagePerms::ReadExecute => PERMS_READ_EXECUTE, + }; + raw( + SYS_MAP_REGION, + [region.value() as usize, va.raw(), perms, 0, 0, 0], + ) + .map(|_| ()) +} + // ---- capabilities ---- pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool { @@ -213,6 +238,8 @@ pub fn mint(slot: CapSlot, badge: Badge, permissions: PermissionSet) -> Result bool { raw(SYS_REVOKE, [slot.value() as usize, 0, 0, 0, 0, 0]).is_ok() } diff --git a/crates/types/src/permission.rs b/crates/types/src/permission.rs index 5d3acb0..e02f1bd 100644 --- a/crates/types/src/permission.rs +++ b/crates/types/src/permission.rs @@ -4,7 +4,6 @@ pub enum Permission { Write, Execute, Delegate, - Revoke, } impl Permission { diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index aa6f9f4..69d2330 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -34,6 +34,13 @@ pub const SYS_WAIT: usize = 29; pub const SYS_POLL: usize = 30; pub const SYS_BIND_NOTIFICATION: usize = 31; pub const SYS_SET_IRQ_NOTIFICATION: usize = 32; +pub const SYS_CREATE_REGION: usize = 33; +pub const SYS_MAP_REGION: usize = 34; + +/// `PagePerms` in a register. +pub const PERMS_READ_ONLY: usize = 0; +pub const PERMS_READ_WRITE: usize = 1; +pub const PERMS_READ_EXECUTE: usize = 2; /// "No capability" in a capability argument or result register. pub const NONE: usize = usize::MAX; diff --git a/docs/ipc.md b/docs/ipc.md index 9083f7a..ca67e60 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -19,8 +19,25 @@ Both are named by capabilities whose generation must match (see P2). ## Capabilities A capability to an endpoint or notification carries a **badge** (a word; -`0` means unbadged). `Mint` copies an unbadged capability with a badge and -narrower permissions. A badge cannot be changed once set. +`0` means unbadged). `Mint` copies a capability with narrower permissions +and, for endpoints and notifications, a badge. A badge cannot be changed +once set: a badged capability cannot be minted again. + +## Derivation and revocation + +Every capability made from another (by `Mint`, `Delegate` or an IPC +transfer) records it as its **parent**; capabilities the kernel creates +directly have none. `Revoke(cap)` deletes every capability derived from +`cap`, transitively and across processes, and keeps `cap` itself. Any +holder may revoke, but only its own subtree. + +Deleting a capability (`DropCap`, or its process dying) makes its parent +adopt its children, so stepping out of the chain never shields anyone +from a revocation further up. + +A region mapping made with `MapRegion(cap, va, perms)` lasts only as long +as `cap`: deleting or revoking `cap` unmaps it. `perms` needs `Read`, plus +`Write` for read-write or `Execute` for read-execute. | Object | Permission | Allows | |--------------|------------|--------------------------------------------|