From d43421e33eb55d484d89bce1b15add690ea79ae9 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 19:54:23 +0100 Subject: [PATCH] feat(kernel): object lifetimes via slot generations - SlotGenerations per table, never wraps, retires exhausted slots - caps to dead/reused process, endpoint, region rejected - endpoints owned, die w/ owner, waiters woken w/ error - DropCap syscall + libkos wrapper - irq table bounds-checked, routes dropped w/ thread - self-kill terminates caller, exit_thread full cleanup - sched run_now: switch to named thread, not queue head - lifetime tests, fuzz kills/recreates, kani gen proofs --- crates/adapter-riscv/src/boot/caps.rs | 6 +- crates/adapter-riscv/src/trap/mod.rs | 4 + crates/domain/src/capability/tests.rs | 2 +- crates/domain/src/endpoint/mod.rs | 60 +++-- crates/domain/src/endpoint/tests.rs | 65 ++++-- crates/domain/src/error.rs | 2 + crates/domain/src/generations.rs | 86 ++++++++ crates/domain/src/generations_tests.rs | 36 +++ crates/domain/src/irq/mod.rs | 25 ++- crates/domain/src/lib.rs | 1 + crates/domain/src/memory/mod.rs | 40 ++-- crates/domain/src/process/mod.rs | 26 ++- crates/domain/src/scheduler/multi_core.rs | 8 + crates/domain/src/scheduler/per_core.rs | 9 + crates/domain/src/scheduler/tests.rs | 23 ++ crates/domain/src/syscall.rs | 4 + crates/domain/src/thread/mod.rs | 2 + crates/kernel/src/lib.rs | 98 +++++---- crates/kernel/src/proofs/revocation.rs | 87 -------- crates/kernel/src/syscall/capability.rs | 15 ++ crates/kernel/src/syscall/ipc.rs | 2 +- crates/kernel/src/syscall/lifecycle.rs | 4 + crates/kernel/src/syscall/mod.rs | 1 + crates/kernel/src/syscall/simple.rs | 5 +- crates/kernel/src/tests/helpers.rs | 42 +++- crates/kernel/src/tests/invariants.rs | 54 ++++- crates/kernel/src/tests/lifetimes.rs | 255 ++++++++++++++++++++++ crates/kernel/src/tests/mod.rs | 1 + crates/libkos/src/syscall.rs | 5 + crates/types/src/generation.rs | 13 +- crates/types/src/proofs/generation.rs | 15 +- crates/types/src/syscall_abi.rs | 1 + 32 files changed, 790 insertions(+), 207 deletions(-) create mode 100644 crates/domain/src/generations.rs create mode 100644 crates/domain/src/generations_tests.rs create mode 100644 crates/kernel/src/tests/lifetimes.rs diff --git a/crates/adapter-riscv/src/boot/caps.rs b/crates/adapter-riscv/src/boot/caps.rs index a6d18d5..f7d5018 100644 --- a/crates/adapter-riscv/src/boot/caps.rs +++ b/crates/adapter-riscv/src/boot/caps.rs @@ -23,7 +23,7 @@ pub fn grant_all( .with(types::permission::Permission::Execute); let storage_ep = kern() - .create_endpoint() + .create_endpoint(storage_pid) .unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed")); kern() @@ -50,7 +50,7 @@ pub fn grant_all( .unwrap_or_else(|_| crate::halt(uart, "grant failed")); let display_ep = kern() - .create_endpoint() + .create_endpoint(display_pid) .unwrap_or_else(|_| crate::halt(uart, "display endpoint failed")); kern() @@ -77,7 +77,7 @@ pub fn grant_all( .unwrap_or_else(|_| crate::halt(uart, "grant failed")); let input_ep = kern() - .create_endpoint() + .create_endpoint(input_pid) .unwrap_or_else(|_| crate::halt(uart, "input endpoint failed")); kern() diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index c206c0c..8d4ce8d 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -251,6 +251,10 @@ fn decode_syscall(frame: &TrapFrame) -> Option { }) } SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint), + SYS_DROP_CAP => { + let slot = types::bounded::CapSlot::new(a0 as u8)?; + Some(Syscall::DropCap { slot }) + } SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }), SYS_MAP_PAGE => { let slot = types::bounded::CapSlot::new(a0 as u8)?; diff --git a/crates/domain/src/capability/tests.rs b/crates/domain/src/capability/tests.rs index 3351def..a234986 100644 --- a/crates/domain/src/capability/tests.rs +++ b/crates/domain/src/capability/tests.rs @@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() { #[test] fn generation_mismatch_detected() { let gen_v1 = Generation::initial(); - let gen_v2 = gen_v1.next(); + let gen_v2 = gen_v1.next().unwrap(); assert!(gen_v1.is_valid_against(&gen_v1)); assert!(!gen_v1.is_valid_against(&gen_v2)); diff --git a/crates/domain/src/endpoint/mod.rs b/crates/domain/src/endpoint/mod.rs index 8de00cc..7789e3e 100644 --- a/crates/domain/src/endpoint/mod.rs +++ b/crates/domain/src/endpoint/mod.rs @@ -4,7 +4,10 @@ use types::{ id::{EndpointId, ProcessId, ThreadId}, }; -use crate::{endpoint::wait_queue::WaitQueue, error::KernelError, message::Message}; +use crate::{ + endpoint::wait_queue::WaitQueue, error::KernelError, generations::SlotGenerations, + message::Message, +}; pub mod wait_queue; @@ -14,18 +17,19 @@ mod tests; pub struct Endpoint { id: EndpointId, + /// The process that created it; the endpoint dies with its owner. + owner: ProcessId, waiting_senders: WaitQueue, waiting_receivers: WaitQueue, - generation: Generation, } impl Endpoint { - pub fn new(id: EndpointId) -> Self { + pub fn new(id: EndpointId, owner: ProcessId) -> Self { Self { id, + owner, waiting_senders: WaitQueue::new(), waiting_receivers: WaitQueue::new(), - generation: Generation::initial(), } } @@ -33,8 +37,8 @@ impl Endpoint { &self.id } - pub fn generation(&self) -> &Generation { - &self.generation + pub fn owner(&self) -> ProcessId { + self.owner } pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> { @@ -68,32 +72,52 @@ impl Endpoint { pub fn remove_receiver(&mut self, thread: ThreadId) -> bool { self.waiting_receivers.remove_thread(thread) } - - pub fn revoke(&mut self) { - self.generation = self.generation.next(); - } } -const MAX_ENDPOINTS: usize = 256; +pub const MAX_ENDPOINTS: usize = 256; pub struct EndpointTable { endpoints: [Option; MAX_ENDPOINTS], + generations: SlotGenerations, } impl EndpointTable { pub fn new() -> Self { Self { endpoints: [const { None }; MAX_ENDPOINTS], + generations: SlotGenerations::new(), } } - pub fn create(&mut self) -> Option { - let idx = self.endpoints.iter().position(|s| s.is_none())?; + pub fn create(&mut self, owner: ProcessId) -> Option { + let idx = (0..MAX_ENDPOINTS) + .find(|&i| self.endpoints[i].is_none() && self.generations.is_usable(i))?; let id = EndpointId::from_raw(idx as u16); - self.endpoints[idx] = Some(Endpoint::new(id)); + self.endpoints[idx] = Some(Endpoint::new(id, owner)); Some(id) } + /// Generation capabilities to a live endpoint must carry. + pub fn generation(&self, id: EndpointId) -> Option { + self.get(id)?; + 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() + .flatten() + .filter(move |ep| ep.owner == owner) + .map(|ep| ep.id) + } + pub fn get(&self, id: EndpointId) -> Option<&Endpoint> { self.endpoints .get(id.raw() as usize) @@ -106,10 +130,12 @@ impl EndpointTable { .and_then(|s| s.as_mut()) } + /// Removes the endpoint; capabilities to it become invalid even if the + /// slot is later reused. pub fn destroy(&mut self, id: EndpointId) -> Option { - self.endpoints - .get_mut(id.raw() as usize) - .and_then(|s| s.take()) + let ep = self.endpoints.get_mut(id.raw() as usize)?.take()?; + self.generations.advance(id.raw() as usize); + Some(ep) } pub fn remove_thread_from_all(&mut self, thread: ThreadId) { diff --git a/crates/domain/src/endpoint/tests.rs b/crates/domain/src/endpoint/tests.rs index 58a7f0c..58e21ef 100644 --- a/crates/domain/src/endpoint/tests.rs +++ b/crates/domain/src/endpoint/tests.rs @@ -1,14 +1,18 @@ use crate::endpoint::wait_queue::MAX_WAITERS; use super::*; -use types::id::{EndpointId, ThreadId}; +use types::id::{EndpointId, ProcessId, ThreadId}; fn thread(id: u16) -> ThreadId { ThreadId::from_raw(id) } +fn owner() -> ProcessId { + ProcessId::from_raw(0) +} + fn test_endpoint() -> Endpoint { - Endpoint::new(EndpointId::from_raw(1)) + Endpoint::new(EndpointId::from_raw(1), owner()) } // --- WaitQueue tests --- @@ -99,13 +103,46 @@ fn senders_and_receivers_are_independent() { } #[test] -fn revoke_bumps_generation() { - let mut ep = test_endpoint(); - let gen_before = *ep.generation(); - ep.revoke(); - let gen_after = *ep.generation(); +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(); + let id = table.create(owner()).unwrap(); + let old = table.generation(id).unwrap(); + table.destroy(id); + assert_eq!( + table.generation(id), + None, + "dead endpoints have no generation" + ); + let again = table.create(owner()).unwrap(); + assert_eq!(again, id); + assert!(!old.is_valid_against(&table.generation(again).unwrap())); +} + +#[test] +fn ids_owned_by_lists_only_that_owners_endpoints() { + let mut table = EndpointTable::new(); + let a = table.create(owner()).unwrap(); + table.create(ProcessId::from_raw(1)).unwrap(); + let c = table.create(owner()).unwrap(); + let mut ids = [EndpointId::from_raw(u16::MAX); 4]; + let mut n = 0; + for id in table.ids_owned_by(owner()) { + ids[n] = id; + n += 1; + } + assert_eq!(&ids[..n], &[a, c]); } #[test] @@ -141,22 +178,22 @@ fn rendezvous_receiver_first() { #[test] fn table_create_and_lookup() { let mut table = EndpointTable::new(); - let id = table.create().unwrap(); + let id = table.create(owner()).unwrap(); assert!(table.get(id).is_some()); } #[test] fn table_create_returns_unique_ids() { let mut table = EndpointTable::new(); - let a = table.create().unwrap(); - let b = table.create().unwrap(); + let a = table.create(owner()).unwrap(); + let b = table.create(owner()).unwrap(); assert_ne!(a, b); } #[test] fn table_destroy_then_get_fails() { let mut table = EndpointTable::new(); - let id = table.create().unwrap(); + let id = table.create(owner()).unwrap(); let removed = table.destroy(id); assert!(removed.is_some()); assert!(table.get(id).is_none()); @@ -165,7 +202,7 @@ fn table_destroy_then_get_fails() { #[test] fn table_get_mut_allows_enqueue() { let mut table = EndpointTable::new(); - let id = table.create().unwrap(); + let id = table.create(owner()).unwrap(); table .get_mut(id) .unwrap() @@ -177,8 +214,8 @@ fn table_get_mut_allows_enqueue() { #[test] fn table_destroy_reuses_slot() { let mut table = EndpointTable::new(); - let id = table.create().unwrap(); + let id = table.create(owner()).unwrap(); table.destroy(id); - let new_id = table.create().unwrap(); + let new_id = table.create(owner()).unwrap(); assert_eq!(id, new_id); } diff --git a/crates/domain/src/error.rs b/crates/domain/src/error.rs index f5bb87f..cebe4a9 100644 --- a/crates/domain/src/error.rs +++ b/crates/domain/src/error.rs @@ -31,4 +31,6 @@ pub enum KernelError { /// The target virtual range already holds a mapping. AddressInUse, InvalidRegion, + /// The object a capability would refer to no longer exists. + NoSuchObject, } diff --git a/crates/domain/src/generations.rs b/crates/domain/src/generations.rs new file mode 100644 index 0000000..cd8e26e --- /dev/null +++ b/crates/domain/src/generations.rs @@ -0,0 +1,86 @@ +//! Per-slot generation counters shared by the kernel's object tables. + +use types::generation::Generation; + +#[cfg(test)] +#[path = "generations_tests.rs"] +mod tests; + +/// A slot's generation changes every time the object in it is destroyed +/// or revoked, so capabilities minted earlier stop matching. A slot whose +/// counter runs out is retired for good rather than reused. +pub struct SlotGenerations { + gens: [Option; N], +} + +impl SlotGenerations { + pub fn new() -> Self { + Self { + gens: [Some(Generation::initial()); N], + } + } + + /// Current generation of `idx`, `None` if retired or out of range. + pub fn current(&self, idx: usize) -> Option { + *self.gens.get(idx)? + } + + pub fn is_usable(&self, idx: usize) -> bool { + self.current(idx).is_some() + } + + /// Invalidates every capability minted for the current generation. + pub fn advance(&mut self, idx: usize) { + if let Some(g) = self.gens.get_mut(idx) { + *g = g.and_then(|g| g.next()); + } + } +} + +impl Default for SlotGenerations { + fn default() -> Self { + Self::new() + } +} + +#[cfg(kani)] +mod proofs { + use super::SlotGenerations; + use types::generation::Generation; + + /// However many times a slot is advanced, a generation observed earlier + /// never comes back, so a capability minted for it stays dead. + #[kani::proof] + #[kani::unwind(5)] + fn an_observed_generation_never_returns() { + let mut g: SlotGenerations<1> = SlotGenerations::new(); + g.gens[0] = if kani::any() { + Some(Generation::from_raw(kani::any())) + } else { + None + }; + let observed = g.current(0); + let steps: u8 = kani::any(); + kani::assume(steps >= 1 && steps <= 4); + for _ in 0..steps { + g.advance(0); + if let (Some(old), Some(now)) = (observed, g.current(0)) { + assert!(!old.is_valid_against(&now)); + } + } + } + + /// A retired slot is never usable again. + #[kani::proof] + #[kani::unwind(5)] + fn retired_slots_stay_retired() { + let mut g: SlotGenerations<1> = SlotGenerations::new(); + g.gens[0] = None; + let steps: u8 = kani::any(); + kani::assume(steps <= 4); + for _ in 0..steps { + g.advance(0); + } + assert!(!g.is_usable(0)); + } +} diff --git a/crates/domain/src/generations_tests.rs b/crates/domain/src/generations_tests.rs new file mode 100644 index 0000000..b735e08 --- /dev/null +++ b/crates/domain/src/generations_tests.rs @@ -0,0 +1,36 @@ +use super::SlotGenerations; +use types::generation::Generation; + +#[test] +fn slots_start_at_the_initial_generation() { + let g: SlotGenerations<2> = SlotGenerations::new(); + assert_eq!(g.current(0), Some(Generation::initial())); + assert!(g.is_usable(1)); +} + +#[test] +fn advance_changes_the_generation() { + let mut g: SlotGenerations<2> = SlotGenerations::new(); + let before = g.current(0); + g.advance(0); + assert_ne!(g.current(0), before); + assert_eq!(g.current(1), before, "other slots untouched"); +} + +#[test] +fn out_of_range_slots_are_never_usable() { + let mut g: SlotGenerations<2> = SlotGenerations::new(); + assert_eq!(g.current(2), None); + g.advance(2); // no panic +} + +#[test] +fn exhausted_counter_retires_the_slot() { + let mut g: SlotGenerations<1> = SlotGenerations::new(); + g.gens[0] = Some(Generation::from_raw(u32::MAX)); + g.advance(0); + assert_eq!(g.current(0), None); + assert!(!g.is_usable(0)); + g.advance(0); + assert_eq!(g.current(0), None, "retired slots stay retired"); +} diff --git a/crates/domain/src/irq/mod.rs b/crates/domain/src/irq/mod.rs index d8530ce..5765aa5 100644 --- a/crates/domain/src/irq/mod.rs +++ b/crates/domain/src/irq/mod.rs @@ -20,20 +20,35 @@ impl IrqRoutingTable { } pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> { - let idx = irq.raw() as usize; - if self.owners[idx].is_some() { + let slot = self + .owners + .get_mut(irq.raw() as usize) + .ok_or(KernelError::InvalidIrq(irq))?; + if slot.is_some() { return Err(KernelError::IrqAlreadyOwned(irq)); } - self.owners[idx] = Some(thread); + *slot = Some(thread); Ok(()) } pub fn unregister(&mut self, irq: IrqId) { - self.owners[irq.raw() as usize] = None; + if let Some(slot) = self.owners.get_mut(irq.raw() as usize) { + *slot = None; + } } + /// Drops every route to `thread`, e.g. when it dies. + pub fn unregister_thread(&mut self, thread: ThreadId) { + for slot in &mut self.owners { + if *slot == Some(thread) { + *slot = None; + } + } + } + + /// The thread an interrupt is routed to; `None` for unknown lines. pub fn owner(&self, irq: IrqId) -> Option { - self.owners[irq.raw() as usize] + *self.owners.get(irq.raw() as usize)? } } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 91866fb..fbb30f4 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -7,6 +7,7 @@ pub mod capability; pub mod endpoint; pub mod error; pub mod fault; +pub mod generations; pub mod irq; pub mod memory; pub mod message; diff --git a/crates/domain/src/memory/mod.rs b/crates/domain/src/memory/mod.rs index a8ed5ce..e0186d3 100644 --- a/crates/domain/src/memory/mod.rs +++ b/crates/domain/src/memory/mod.rs @@ -1,3 +1,4 @@ +use crate::generations::SlotGenerations; use types::{ address::{PhysicalAddress, VirtualAddress}, bounded::PageCount, @@ -43,7 +44,6 @@ pub struct Region { backing: RegionBacking, base: PhysicalAddress, pages: PageCount, - generation: Generation, mappings: [Option; MAX_REGION_MAPPINGS], } @@ -59,7 +59,6 @@ impl Region { backing, base, pages, - generation: Generation::initial(), mappings: [None; MAX_REGION_MAPPINGS], } } @@ -80,14 +79,6 @@ impl Region { self.pages } - pub fn generation(&self) -> &Generation { - &self.generation - } - - pub fn revoke(&mut self) { - self.generation = self.generation.next(); - } - pub fn mappings(&self) -> impl Iterator { self.mappings.iter().flatten() } @@ -124,23 +115,42 @@ impl Region { pub struct RegionTable { regions: [Option; MAX_REGIONS], + generations: SlotGenerations, } impl RegionTable { pub fn new() -> Self { Self { regions: [const { None }; MAX_REGIONS], + generations: SlotGenerations::new(), } } + /// Generation capabilities to a live region must carry. + pub fn generation(&self, id: RegionId) -> Option { + self.get(id)?; + 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)) + } + /// The id the next `insert` will return. pub fn next_free_id(&self) -> Option { - let idx = self.regions.iter().position(|s| s.is_none())?; + let idx = self.free_slot()?; Some(RegionId::from_raw(idx as u16)) } pub fn insert(&mut self, region: Region) -> Option { - let idx = self.regions.iter().position(|s| s.is_none())?; + let idx = self.free_slot()?; self.regions[idx] = Some(region); Some(RegionId::from_raw(idx as u16)) } @@ -153,8 +163,12 @@ impl RegionTable { self.regions.get_mut(id.raw() as usize)?.as_mut() } + /// Removes the region; capabilities to it become invalid even if the + /// slot is later reused. pub fn remove(&mut self, id: RegionId) -> Option { - self.regions.get_mut(id.raw() as usize)?.take() + let region = self.regions.get_mut(id.raw() as usize)?.take()?; + self.generations.advance(id.raw() as usize); + Some(region) } pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator + '_ { diff --git a/crates/domain/src/process/mod.rs b/crates/domain/src/process/mod.rs index 302abfa..8334a29 100644 --- a/crates/domain/src/process/mod.rs +++ b/crates/domain/src/process/mod.rs @@ -1,9 +1,10 @@ use types::{ bounded::CapSlot, + generation::Generation, id::{ProcessId, ThreadId}, }; -use crate::{capability::CapabilityTable, error::KernelError}; +use crate::{capability::CapabilityTable, error::KernelError, generations::SlotGenerations}; #[cfg(test)] #[path = "tests.rs"] @@ -75,24 +76,30 @@ impl Process { pub struct ProcessTable { processes: [Option; MAX_PROCESSES], + generations: SlotGenerations, } impl ProcessTable { pub fn new() -> Self { Self { processes: [const { None }; MAX_PROCESSES], + generations: SlotGenerations::new(), } } + /// Generation capabilities to a live process must carry. + pub fn generation(&self, id: ProcessId) -> Option { + self.get(id).ok()?; + self.generations.current(id.raw() as usize) + } + pub fn create( &mut self, capabilities: CapabilityTable, fault_handler: Option, ) -> Result { - let idx = self - .processes - .iter() - .position(|s| s.is_none()) + let idx = (0..MAX_PROCESSES) + .find(|&i| self.processes[i].is_none() && self.generations.is_usable(i)) .ok_or(KernelError::TooManyProcesses)?; let id = ProcessId::from_raw(idx as u16); @@ -116,12 +123,17 @@ impl ProcessTable { .ok_or(KernelError::InvalidProcess(id)) } + /// Removes the process; capabilities to it become invalid even if the + /// pid is later reused. pub fn destroy(&mut self, id: ProcessId) -> Result { let idx = id.raw() as usize; - self.processes + let process = self + .processes .get_mut(idx) .and_then(|s| s.take()) - .ok_or(KernelError::InvalidProcess(id)) + .ok_or(KernelError::InvalidProcess(id))?; + self.generations.advance(idx); + Ok(process) } } diff --git a/crates/domain/src/scheduler/multi_core.rs b/crates/domain/src/scheduler/multi_core.rs index 58154ce..eeadf0c 100644 --- a/crates/domain/src/scheduler/multi_core.rs +++ b/crates/domain/src/scheduler/multi_core.rs @@ -38,6 +38,14 @@ impl MultiCoreScheduler { self.cores[core.raw() as usize].set_current(thread); } + /// Runs `thread` next on `core` regardless of its place in the queue. + /// Fails if it is not queued there. + pub fn run_now(&mut self, core: CoreId, thread: ThreadId) -> bool { + self.cores + .get_mut(core.raw() as usize) + .is_some_and(|c| c.run_now(thread)) + } + fn thread_count_on(&self, core: CoreId) -> usize { self.placements .iter() diff --git a/crates/domain/src/scheduler/per_core.rs b/crates/domain/src/scheduler/per_core.rs index 34ac853..f93bde3 100644 --- a/crates/domain/src/scheduler/per_core.rs +++ b/crates/domain/src/scheduler/per_core.rs @@ -39,6 +39,15 @@ impl PerCoreScheduler { Some(thread) } + /// Takes `thread` out of the run queue and makes it current. + pub fn run_now(&mut self, thread: ThreadId) -> bool { + if !self.queue.remove(thread) { + return false; + } + self.current = Some(thread); + true + } + pub fn clear_current(&mut self) { self.current = None; } diff --git a/crates/domain/src/scheduler/tests.rs b/crates/domain/src/scheduler/tests.rs index fb5113a..0bd4b0d 100644 --- a/crates/domain/src/scheduler/tests.rs +++ b/crates/domain/src/scheduler/tests.rs @@ -284,3 +284,26 @@ fn removed_thread_is_skipped_and_order_kept() { assert_eq!(q.pop_highest().unwrap().0, thread(3)); assert!(q.pop_highest().is_none()); } + +#[test] +fn run_now_picks_the_named_thread_not_the_queue_head() { + let mut s = MultiCoreScheduler::new(1); + s.add(thread(1), prio(10), core(0)); + s.add(thread(2), prio(10), core(0)); + s.add(thread(3), prio(10), core(0)); + + assert!(s.run_now(core(0), thread(2))); + assert_eq!(s.current(core(0)), Some(thread(2))); + // The others keep their order; thread 2 is no longer queued. + assert_eq!(s.next(core(0)), Some(thread(1))); + assert_eq!(s.next(core(0)), Some(thread(3))); + assert_eq!(s.next(core(0)), None); +} + +#[test] +fn run_now_rejects_a_thread_that_is_not_queued() { + let mut s = MultiCoreScheduler::new(1); + s.add(thread(1), prio(10), core(0)); + assert!(!s.run_now(core(0), thread(9))); + assert_eq!(s.current(core(0)), None); +} diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index 70ff620..da1066f 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -69,6 +69,10 @@ pub enum Syscall { Revoke { slot: CapSlot, }, + /// Removes a capability from the caller's own table. + DropCap { + slot: CapSlot, + }, SpawnThread { entry: EntryPoint, stack: StackPointer, diff --git a/crates/domain/src/thread/mod.rs b/crates/domain/src/thread/mod.rs index bacbdb0..b61c497 100644 --- a/crates/domain/src/thread/mod.rs +++ b/crates/domain/src/thread/mod.rs @@ -20,6 +20,7 @@ pub struct Thread { core_affinity: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ThreadState { Ready, Running(CoreId), @@ -77,6 +78,7 @@ impl SavedContext { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BlockReason { Sending(CapSlot), Receiving(CapSlot), diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 7554483..e262262 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -205,8 +205,13 @@ impl Kernel

{ Some(tid) } - pub fn create_endpoint(&mut self) -> Result { - self.endpoints.create().ok_or(KernelError::NoFreeSlot) + /// Creates an endpoint owned by `owner`; it is destroyed with `owner`. + pub fn create_endpoint( + &mut self, + owner: ProcessId, + ) -> Result { + self.processes.get(owner)?; + self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot) } pub fn grant_capability( @@ -237,8 +242,8 @@ impl Kernel

{ if let Ok(t) = self.threads.get(tid) && matches!(t.state(), ThreadState::Ready) + && self.scheduler.run_now(core, tid) { - self.scheduler.next(core); let _ = self.threads.get_mut(tid).map(|t| t.set_running(core)); } } @@ -274,16 +279,9 @@ impl Kernel

{ }; self.cleanup_threads(&thread_ids); - + self.destroy_endpoints_owned_by(pid); self.teardown_memory(pid); - // Unblock threads in other processes that were blocked waiting - // on endpoints where this process's threads were the other side. - // The cleanup_threads call above already removed this process's - // threads from all endpoint wait queues. Threads blocked waiting - // for this process will eventually time out or be rescheduled - // when the endpoint is revoked. - let _ = self.processes.destroy(pid); } @@ -292,6 +290,7 @@ impl Kernel

{ for tid in thread_ids.iter().flatten() { self.endpoints.remove_thread_from_all(*tid); self.cleanup_wait_any(*tid); + self.irqs.unregister_thread(*tid); self.scheduler.remove(*tid); self.pending_messages[tid.raw() as usize] = None; self.pending_results[tid.raw() as usize] = None; @@ -299,6 +298,41 @@ impl Kernel

{ } } + /// Destroys every endpoint `owner` created, waking anyone blocked on + /// them with an error instead of leaving them blocked forever. + fn destroy_endpoints_owned_by(&mut self, owner: ProcessId) { + let mut owned = [None; domain::endpoint::MAX_ENDPOINTS]; + for (slot, id) in owned.iter_mut().zip(self.endpoints.ids_owned_by(owner)) { + *slot = Some(id); + } + for id in owned.into_iter().flatten() { + let Some(mut ep) = self.endpoints.destroy(id) else { + continue; + }; + while let Some(tid) = ep.dequeue_sender().or_else(|| ep.dequeue_receiver()) { + self.wake_with_error(tid, KernelError::InvalidEndpoint(id)); + } + } + } + + /// Makes a blocked thread runnable with `error` as its syscall result. + fn wake_with_error(&mut self, tid: ThreadId, error: KernelError) { + self.cleanup_wait_any(tid); + self.pending_messages[tid.raw() as usize] = None; + let Ok(thread) = self.threads.get_mut(tid) else { + return; + }; + if !matches!(thread.state(), ThreadState::Blocked(_)) { + return; + } + let priority = thread.priority(); + if thread.unblock().is_ok() { + self.pending_results[tid.raw() as usize] = Some(SyscallResult::Error(error)); + let core = self.platform.core_id(); + self.scheduler.add(tid, priority, core); + } + } + pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) { if self.wait_sets[tid.raw() as usize].take().is_some() { self.endpoints.remove_thread_from_all(tid); @@ -316,41 +350,27 @@ impl Kernel

{ if !cap.permissions().allows(needed) { return Err(KernelError::PermissionDenied { slot, needed }); } - let current_gen = self.resource_generation(cap.resource()); - if !cap.generation().is_valid_against(¤t_gen) { - return Err(KernelError::RevokedCapability(slot)); + match self.resource_generation(cap.resource()) { + Some(current) if cap.generation().is_valid_against(¤t) => Ok(cap), + _ => Err(KernelError::RevokedCapability(slot)), } - Ok(cap) } - fn resource_generation(&self, resource: &Resource) -> Generation { + /// The generation a capability to `resource` must carry, or `None` if + /// the object no longer exists. + fn resource_generation(&self, resource: &Resource) -> Option { match resource { - Resource::Endpoint(id) => self - .endpoints - .get(*id) - .map(|ep| *ep.generation()) - .unwrap_or_else(Generation::initial), - Resource::Memory(id) => self - .regions - .get(*id) - .map(|r| *r.generation()) - .unwrap_or_else(Generation::initial), - _ => Generation::initial(), + Resource::Endpoint(id) => self.endpoints.generation(*id), + Resource::Memory(id) => self.regions.generation(*id), + Resource::Process(pid) => self.processes.generation(*pid), + Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()), } } pub fn revoke_resource(&mut self, resource: &Resource) { match resource { - Resource::Endpoint(id) => { - if let Some(ep) = self.endpoints.get_mut(*id) { - ep.revoke(); - } - } - Resource::Memory(id) => { - if let Some(r) = self.regions.get_mut(*id) { - r.revoke(); - } - } + Resource::Endpoint(id) => self.endpoints.revoke(*id), + Resource::Memory(id) => self.regions.revoke(*id), _ => {} } } @@ -361,7 +381,9 @@ impl Kernel

{ resource: Resource, permissions: PermissionSet, ) -> Result { - let generation = self.resource_generation(&resource); + let generation = self + .resource_generation(&resource) + .ok_or(KernelError::NoSuchObject)?; let cap = Capability::new(resource, permissions, generation); self.processes.get_mut(to)?.capabilities_mut().insert(cap) } diff --git a/crates/kernel/src/proofs/revocation.rs b/crates/kernel/src/proofs/revocation.rs index fe11da0..b3515ef 100644 --- a/crates/kernel/src/proofs/revocation.rs +++ b/crates/kernel/src/proofs/revocation.rs @@ -1,6 +1,5 @@ use super::{arbitrary_endpoint_cap, insert_cap}; use domain::capability::{Capability, CapabilityTable}; -use domain::endpoint::Endpoint; use domain::resource::Resource; use types::generation::Generation; use types::id::EndpointId; @@ -62,89 +61,3 @@ fn grant_cap_preserves_resource_identity() { _ => panic!("resource type changed"), } } - -#[kani::proof] -fn generation_revocation_invalidates_all_caps() { - let bits: u8 = kani::any(); - let ep_id: u16 = kani::any(); - kani::assume(ep_id < 256); - - let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id)); - let original_gen = *endpoint.generation(); - - let cap = Capability::new( - Resource::Endpoint(EndpointId::from_raw(ep_id)), - PermissionSet::from_bits(bits), - original_gen, - ); - - let mut table = CapabilityTable::new(); - let slot = insert_cap(&mut table, cap); - - assert!( - table - .get(slot) - .unwrap() - .generation() - .is_valid_against(&original_gen) - ); - - endpoint.revoke(); - let new_gen = *endpoint.generation(); - - assert!( - !table - .get(slot) - .unwrap() - .generation() - .is_valid_against(&new_gen) - ); -} - -#[kani::proof] -fn cap_created_after_revoke_is_valid() { - let ep_id: u16 = kani::any(); - kani::assume(ep_id < 256); - let bits: u8 = kani::any(); - - let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id)); - endpoint.revoke(); - let current_gen = *endpoint.generation(); - - let cap = Capability::new( - Resource::Endpoint(EndpointId::from_raw(ep_id)), - PermissionSet::from_bits(bits), - current_gen, - ); - - assert!(cap.generation().is_valid_against(¤t_gen)); -} - -#[kani::proof] -fn multiple_revocations_invalidate_all_prior_caps() { - let ep_id: u16 = kani::any(); - kani::assume(ep_id < 256); - - let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id)); - - let gen0 = *endpoint.generation(); - let cap0 = Capability::new( - Resource::Endpoint(EndpointId::from_raw(ep_id)), - PermissionSet::empty().with(Permission::Read), - gen0, - ); - - endpoint.revoke(); - let gen1 = *endpoint.generation(); - let cap1 = Capability::new( - Resource::Endpoint(EndpointId::from_raw(ep_id)), - PermissionSet::empty().with(Permission::Read), - gen1, - ); - - endpoint.revoke(); - let gen2 = *endpoint.generation(); - - assert!(!cap0.generation().is_valid_against(&gen2)); - assert!(!cap1.generation().is_valid_against(&gen2)); -} diff --git a/crates/kernel/src/syscall/capability.rs b/crates/kernel/src/syscall/capability.rs index 9cb2a3e..a9f1721 100644 --- a/crates/kernel/src/syscall/capability.rs +++ b/crates/kernel/src/syscall/capability.rs @@ -59,4 +59,19 @@ impl Kernel

{ self.revoke_resource(&resource); SyscallOutcome::Complete(SyscallResult::Ok) } + + pub(crate) fn handle_drop_cap( + &mut self, + 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 { + 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 c659a7a..5b95781 100644 --- a/crates/kernel/src/syscall/ipc.rs +++ b/crates/kernel/src/syscall/ipc.rs @@ -8,7 +8,7 @@ impl Kernel

{ &mut self, caller_pid: types::id::ProcessId, ) -> SyscallOutcome { - let ep_id = match self.create_endpoint() { + let ep_id = match self.create_endpoint(caller_pid) { Ok(id) => id, Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; diff --git a/crates/kernel/src/syscall/lifecycle.rs b/crates/kernel/src/syscall/lifecycle.rs index 6234a43..9028e12 100644 --- a/crates/kernel/src/syscall/lifecycle.rs +++ b/crates/kernel/src/syscall/lifecycle.rs @@ -159,6 +159,10 @@ impl Kernel

{ }; self.destroy_process(target_pid); + if target_pid == caller_pid { + // The caller no longer exists; nothing to return to. + return SyscallOutcome::TerminateCaller; + } SyscallOutcome::Complete(SyscallResult::Ok) } } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 4223120..761fa2a 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -48,6 +48,7 @@ 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::Delegate { slot, to_endpoint, diff --git a/crates/kernel/src/syscall/simple.rs b/crates/kernel/src/syscall/simple.rs index 110bf1c..efd6b36 100644 --- a/crates/kernel/src/syscall/simple.rs +++ b/crates/kernel/src/syscall/simple.rs @@ -70,10 +70,7 @@ impl Kernel

{ caller_tid: types::id::ThreadId, pid: types::id::ProcessId, ) -> SyscallOutcome { - use domain::scheduler::Scheduler; - - self.scheduler.remove(caller_tid); - let _ = self.threads.destroy(caller_tid); + self.cleanup_threads(&[Some(caller_tid)]); if let Ok(process) = self.processes.get_mut(pid) { process.remove_thread(caller_tid); diff --git a/crates/kernel/src/tests/helpers.rs b/crates/kernel/src/tests/helpers.rs index e23e6ae..4f8332e 100644 --- a/crates/kernel/src/tests/helpers.rs +++ b/crates/kernel/src/tests/helpers.rs @@ -74,14 +74,16 @@ impl Kernel { if let Ok(t) = self.threads.get(tid) && matches!(t.state(), ThreadState::Ready) + && self.scheduler.run_now(core, tid) { - self.scheduler.next(core); let _ = self.threads.get_mut(tid).map(|t| t.set_running(core)); } } pub fn create_endpoint_for_test(&mut self) -> EndpointId { - self.endpoints.create().expect("create endpoint") + self.endpoints + .create(ProcessId::from_raw(0)) + .expect("create endpoint") } pub fn grant_cap_for_test( @@ -93,4 +95,40 @@ impl Kernel { self.grant_cap(pid, resource, permissions) .expect("grant cap") } + + pub fn endpoint_of_cap_for_test(&self, pid: ProcessId, slot: CapSlot) -> EndpointId { + match self + .processes + .get(pid) + .unwrap() + .capabilities() + .get(slot) + .unwrap() + .resource() + { + Resource::Endpoint(id) => *id, + _ => panic!("not an endpoint cap"), + } + } + + /// The process a Process cap in init's table points at. + pub fn process_of_cap_for_test(&self, slot: CapSlot) -> ProcessId { + let init = ProcessId::from_raw(0); + match self + .processes + .get(init) + .unwrap() + .capabilities() + .get(slot) + .unwrap() + .resource() + { + Resource::Process(pid) => *pid, + _ => panic!("not a process cap"), + } + } + + 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/invariants.rs b/crates/kernel/src/tests/invariants.rs index 3620494..555ed6f 100644 --- a/crates/kernel/src/tests/invariants.rs +++ b/crates/kernel/src/tests/invariants.rs @@ -52,6 +52,7 @@ struct World { caps: [CapSlot; PROCS], known: [[usize; 64]; PROCS], known_len: [usize; PROCS], + rebirths: usize, } impl World { @@ -80,6 +81,7 @@ impl World { caps, known: [[0; 64]; PROCS], known_len: [0; PROCS], + rebirths: 0, } } @@ -152,14 +154,46 @@ impl World { } } } - 8 if who == 0 && rng.below(200) == 0 => { + 8 if who == 0 && rng.below(8) == 0 => { let victim = 1 + rng.below(PROCS - 1); - if self.alive[victim] { - self.k.handle_syscall(Syscall::Kill { - process: self.caps[victim], - }); - self.alive[victim] = false; - } + let stale = self.caps[victim]; + self.k.handle_syscall(Syscall::Kill { process: stale }); + self.rebirths += 1; + + // Re-create it; the pid slot is reused. + let fresh = match self.k.handle_syscall(Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + }) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("re-create process"), + }; + assert_eq!(self.k.process_of_cap_for_test(fresh), self.pids[victim]); + self.tids[victim] = self.k.spawn_thread_for_test( + self.pids[victim], + Priority::DEFAULT, + TimeSlice::DEFAULT, + ); + self.known_len[victim] = 0; + + // The old cap names the same pid but must reach nothing. + let src = rng.va(known); + let outcome = self.k.handle_syscall(Syscall::MapPage { + process: stale, + src_va: VirtualAddress::from_raw(src), + dst_va: VirtualAddress::from_raw(0x10000), + executable: false, + }); + assert!( + matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))), + "stale process cap still works" + ); + assert!(matches!( + self.k.handle_syscall(Syscall::Kill { process: stale }), + SyscallOutcome::Complete(SyscallResult::Error(_)) + )); + self.k.handle_syscall(Syscall::DropCap { slot: stale }); + self.caps[victim] = fresh; } _ => {} } @@ -196,7 +230,7 @@ impl World { frames_owned + region_pages, "quota of {pid:?} drifted" ); - if !self.alive[i] { + if !self.alive[i] && i != 0 { assert_eq!(k.page_usage(pid), 0); assert_eq!( k.platform.mapping_count(pid), @@ -257,5 +291,9 @@ fn memory_model_holds_under_random_hostile_operations() { world.step(&mut rng); world.check(); } + assert!( + world.rebirths >= 5, + "seed {seed:#x} barely exercised kill/re-create" + ); } } diff --git a/crates/kernel/src/tests/lifetimes.rs b/crates/kernel/src/tests/lifetimes.rs new file mode 100644 index 0000000..a487a51 --- /dev/null +++ b/crates/kernel/src/tests/lifetimes.rs @@ -0,0 +1,255 @@ +//! Object lifetimes: what happens to capabilities, blocked threads and +//! routing entries when the object they refer to dies and its id is reused. + +use super::*; +use crate::{SyscallOutcome, SyscallResult}; +use domain::message::Message; +use domain::resource::Resource; +use domain::syscall::Syscall; +use domain::thread::{BlockReason, ThreadState}; +use types::address::DataRef; +use types::bounded::CapSlot; +use types::id::{IrqId, ThreadId}; +use types::permission::{Permission, PermissionSet}; + +fn create_process(k: &mut Kernel) -> CapSlot { + match k.handle_syscall(Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + }) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("create process"), + } +} + +fn is_error(outcome: &SyscallOutcome) -> bool { + matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))) +} + +fn ipc_perms() -> PermissionSet { + PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Execute) +} + +/// Process B (pid 1) with one thread that has created an endpoint. +/// Returns (B's pid, B's thread, the endpoint, A's cap to B). +fn b_with_endpoint(k: &mut Kernel) -> (ProcessId, ThreadId, EndpointRef, CapSlot) { + let tid_a = k.current_thread().unwrap(); + let cap_b = create_process(k); + let pid_b = ProcessId::from_raw(1); + let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT); + k.switch_to_thread_for_test(tid_b); + let ep_slot_in_b = match k.handle_syscall(Syscall::CreateEndpoint) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("create endpoint"), + }; + let ep = k.endpoint_of_cap_for_test(pid_b, ep_slot_in_b); + k.switch_to_thread_for_test(tid_a); + (pid_b, tid_b, ep, cap_b) +} + +type EndpointRef = types::id::EndpointId; + +#[test] +fn stale_process_cap_cannot_reach_a_process_that_reused_the_pid() { + let (mut k, _) = kernel_with_process(); + let old_cap = create_process(&mut k); + assert!(!is_error( + &k.handle_syscall(Syscall::Kill { process: old_cap }) + )); + + let _new_cap = create_process(&mut k); // reuses pid 1 + assert!(k.process_exists(ProcessId::from_raw(1))); + + assert!(is_error( + &k.handle_syscall(Syscall::Kill { process: old_cap }) + )); + assert!( + k.process_exists(ProcessId::from_raw(1)), + "stale cap killed the new process" + ); +} + +#[test] +fn endpoints_die_with_their_creator() { + let (mut k, pid_a) = kernel_with_process(); + let (_, _, ep, cap_b) = b_with_endpoint(&mut k); + let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); + + k.handle_syscall(Syscall::Kill { process: cap_b }); + + let outcome = k.handle_syscall(Syscall::Send { + endpoint: a_cap, + message: Message::empty(), + }); + assert!( + is_error(&outcome), + "send to a dead process's endpoint must fail, not block" + ); +} + +#[test] +fn stale_endpoint_cap_does_not_reach_a_reused_endpoint_slot() { + let (mut k, pid_a) = kernel_with_process(); + let (_, _, ep, cap_b) = b_with_endpoint(&mut k); + let stale = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); + k.handle_syscall(Syscall::Kill { process: cap_b }); + + // A new endpoint lands in the freed slot. + let fresh = match k.handle_syscall(Syscall::CreateEndpoint) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("create endpoint"), + }; + assert_eq!(k.endpoint_of_cap_for_test(pid_a, fresh), ep); + + let outcome = k.handle_syscall(Syscall::Send { + endpoint: stale, + message: Message::empty(), + }); + assert!(is_error(&outcome)); +} + +#[test] +fn receiver_blocked_on_a_dying_endpoint_is_woken_with_an_error() { + let (mut k, pid_a) = kernel_with_process(); + let tid_a = k.current_thread().unwrap(); + let (pid_b, _, ep, _) = b_with_endpoint(&mut k); + let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); + + let outcome = k.handle_syscall(Syscall::Receive { endpoint: a_cap }); + assert!(matches!(outcome, SyscallOutcome::BlockCaller(_))); + + k.destroy_process(pid_b); + + assert!(matches!( + k.thread_state_for_test(tid_a), + Some(ThreadState::Ready) + )); + assert!(matches!( + k.pending_result(tid_a), + Some(SyscallResult::Error(_)) + )); +} + +#[test] +fn sender_blocked_on_a_dying_endpoint_is_woken_with_an_error() { + let (mut k, pid_a) = kernel_with_process(); + let tid_a = k.current_thread().unwrap(); + let (pid_b, _, ep, _) = b_with_endpoint(&mut k); + let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); + + let outcome = k.handle_syscall(Syscall::Send { + endpoint: a_cap, + message: Message::from_words([1, 2, 3, 4]), + }); + assert!(matches!(outcome, SyscallOutcome::BlockCaller(_))); + + k.destroy_process(pid_b); + + assert!(matches!( + k.thread_state_for_test(tid_a), + Some(ThreadState::Ready) + )); + assert!(matches!( + k.pending_result(tid_a), + Some(SyscallResult::Error(_)) + )); +} + +#[test] +fn endpoint_slots_are_reclaimed_when_processes_die() { + let (mut k, _) = kernel_with_process(); + let tid_a = k.current_thread().unwrap(); + // Far more endpoints than the table holds, created and abandoned. + for _ in 0..600 { + let cap = create_process(&mut k); + let pid = ProcessId::from_raw(1); + let tid = k.spawn_thread_for_test(pid, Priority::DEFAULT, TimeSlice::DEFAULT); + k.switch_to_thread_for_test(tid); + assert!( + !is_error(&k.handle_syscall(Syscall::CreateEndpoint)), + "endpoint table exhausted" + ); + k.switch_to_thread_for_test(tid_a); + k.handle_syscall(Syscall::Kill { process: cap }); + k.handle_syscall(Syscall::DropCap { slot: cap }); + } +} + +#[test] +fn interrupt_routing_does_not_survive_the_owning_thread() { + let (mut k, pid_a) = kernel_with_process(); + let tid_a = k.current_thread().unwrap(); + let cap_b = create_process(&mut k); + let pid_b = ProcessId::from_raw(1); + let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT); + let irq = IrqId::from_raw(10); + k.register_irq_for_test(irq, tid_b); + + k.switch_to_thread_for_test(tid_a); + k.handle_syscall(Syscall::Kill { process: cap_b }); + + // A new thread reuses B's thread id and happens to be blocked. + let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT); + assert_eq!(reused, tid_b); + k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap())); + + assert_eq!( + k.handle_interrupt(irq), + None, + "IRQ delivered to an unrelated thread" + ); +} + +#[test] +fn interrupt_numbers_beyond_the_table_are_ignored() { + let (mut k, _) = kernel_with_process(); + // The PLIC can report sources up to 1023. + assert_eq!(k.handle_interrupt(IrqId::from_raw(1000)), None); +} + +#[test] +fn exiting_a_thread_releases_its_interrupt_route() { + let (mut k, pid_a) = kernel_with_process(); + let tid_a = k.current_thread().unwrap(); + let irq = IrqId::from_raw(10); + k.register_irq_for_test(irq, tid_a); + k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT); + + assert!(matches!( + k.handle_syscall(Syscall::ExitThread), + SyscallOutcome::TerminateCaller + )); + + let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT); + assert_eq!(reused, tid_a); + k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap())); + assert_eq!(k.handle_interrupt(irq), None); +} + +#[test] +fn drop_cap_frees_the_slot() { + let (mut k, pid_a) = kernel_with_process(); + let ep = k.create_endpoint_for_test(); + let cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); + assert!(!is_error(&k.handle_syscall(Syscall::DropCap { slot: cap }))); + assert!(is_error(&k.handle_syscall(Syscall::Send { + endpoint: cap, + message: Message::empty(), + }))); + assert!(is_error(&k.handle_syscall(Syscall::DropCap { slot: cap }))); +} + +#[test] +fn killing_yourself_terminates_the_caller() { + let (mut k, pid_a) = kernel_with_process(); + let self_cap = k.grant_cap_for_test( + pid_a, + Resource::Process(pid_a), + PermissionSet::empty().with(Permission::Execute), + ); + let outcome = k.handle_syscall(Syscall::Kill { process: self_cap }); + assert!(matches!(outcome, SyscallOutcome::TerminateCaller)); + assert!(!k.process_exists(pid_a)); +} diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index f2323b4..11de71d 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -7,6 +7,7 @@ mod invariants; mod ipc; mod isolation; mod lifecycle; +mod lifetimes; mod memory; mod simple; mod tick; diff --git a/crates/libkos/src/syscall.rs b/crates/libkos/src/syscall.rs index 1299eb6..9d7b657 100644 --- a/crates/libkos/src/syscall.rs +++ b/crates/libkos/src/syscall.rs @@ -80,6 +80,11 @@ pub fn revoke(slot: CapSlot) -> bool { arch::syscall1(SYS_REVOKE, slot.value() as usize) == 0 } +/// Removes a capability from this process's own table, freeing the slot. +pub fn drop_cap(slot: CapSlot) -> bool { + arch::syscall1(SYS_DROP_CAP, slot.value() as usize) == 0 +} + pub fn debug_putchar(byte: u8) { arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize); } diff --git a/crates/types/src/generation.rs b/crates/types/src/generation.rs index 991208a..4eb1cf9 100644 --- a/crates/types/src/generation.rs +++ b/crates/types/src/generation.rs @@ -6,15 +6,22 @@ impl Generation { Self(0) } - pub fn next(&self) -> Self { - Self(self.0.wrapping_add(1)) + /// The following generation, or `None` once the counter is exhausted. + /// Never wraps: a wrapped counter would make stale capabilities valid. + pub fn next(&self) -> Option { + self.0.checked_add(1).map(Self) + } + + pub fn raw(&self) -> u32 { + self.0 } pub fn is_valid_against(&self, current: &Generation) -> bool { self == current } - #[cfg(kani)] + /// Only kernel code builds capabilities, so exposing this cannot + /// forge one; it exists for tests and proofs. pub fn from_raw(value: u32) -> Self { Self(value) } diff --git a/crates/types/src/proofs/generation.rs b/crates/types/src/proofs/generation.rs index beab2e2..0958505 100644 --- a/crates/types/src/proofs/generation.rs +++ b/crates/types/src/proofs/generation.rs @@ -1,10 +1,17 @@ use crate::generation::Generation; +/// Generations only ever increase, so a capability minted for an earlier +/// generation can never match again. (The old wrapping counter returned to +/// earlier values after 2^32 bumps.) #[kani::proof] -fn generation_next_always_invalidates() { +fn generation_next_is_strictly_greater_or_exhausted() { let value: u32 = kani::any(); let current = Generation::from_raw(value); - let next = current.next(); - - assert!(!current.is_valid_against(&next)); + match current.next() { + Some(next) => { + assert!(next.raw() > current.raw()); + assert!(!current.is_valid_against(&next)); + } + None => assert!(value == u32::MAX), + } } diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index f866aa0..b2f9bb0 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -20,3 +20,4 @@ pub const SYS_CREATE_ENDPOINT: usize = 18; pub const SYS_DEBUG_PUTCHAR: usize = 19; pub const SYS_MAP_PAGE: usize = 20; pub const SYS_SPAWN_THREAD_IN: usize = 21; +pub const SYS_DROP_CAP: usize = 22;