//! Randomized check of the derivation tree against a model of the rules in docs/ipc.md. 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); } } /// Sorted, so a seed always means the same run (HashMap order is /// random per process). fn held_by(&self, pid: ProcessId) -> Vec<(usize, CapSlot)> { let mut held: Vec<(usize, CapSlot)> = self .alive .iter() .filter(|(_, (p, _))| *p == pid) .map(|(id, (_, s))| (*id, *s)) .collect(); held.sort_by_key(|(id, _)| *id); held } } 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(), limits: super::helpers::CHILD_LIMITS, }, ); 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() { let extra: u64 = std::env::var("MODEL_SEEDS") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(0); for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024] .into_iter() .chain(1000..1000 + extra) { 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(); } if seed >= 1000 { continue; // extra seeds hunt for mismatches, not coverage } assert!( revokes > 20, "seed {seed:#x}: too little deletion exercised" ); assert!( world.m.nodes.len() > 100, "seed {seed:#x}: too little derivation" ); } }