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
This commit is contained in:
2026-10-26 19:54:23 +01:00
parent 8d2c40cb1e
commit d43421e33e
32 changed files with 790 additions and 207 deletions

View File

@@ -23,7 +23,7 @@ pub fn grant_all(
.with(types::permission::Permission::Execute); .with(types::permission::Permission::Execute);
let storage_ep = kern() let storage_ep = kern()
.create_endpoint() .create_endpoint(storage_pid)
.unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed"));
kern() kern()
@@ -50,7 +50,7 @@ pub fn grant_all(
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
let display_ep = kern() let display_ep = kern()
.create_endpoint() .create_endpoint(display_pid)
.unwrap_or_else(|_| crate::halt(uart, "display endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "display endpoint failed"));
kern() kern()
@@ -77,7 +77,7 @@ pub fn grant_all(
.unwrap_or_else(|_| crate::halt(uart, "grant failed")); .unwrap_or_else(|_| crate::halt(uart, "grant failed"));
let input_ep = kern() let input_ep = kern()
.create_endpoint() .create_endpoint(input_pid)
.unwrap_or_else(|_| crate::halt(uart, "input endpoint failed")); .unwrap_or_else(|_| crate::halt(uart, "input endpoint failed"));
kern() kern()

View File

@@ -251,6 +251,10 @@ fn decode_syscall(frame: &TrapFrame) -> Option<domain::syscall::Syscall> {
}) })
} }
SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint), 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_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }),
SYS_MAP_PAGE => { SYS_MAP_PAGE => {
let slot = types::bounded::CapSlot::new(a0 as u8)?; let slot = types::bounded::CapSlot::new(a0 as u8)?;

View File

@@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() {
#[test] #[test]
fn generation_mismatch_detected() { fn generation_mismatch_detected() {
let gen_v1 = Generation::initial(); 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_v1));
assert!(!gen_v1.is_valid_against(&gen_v2)); assert!(!gen_v1.is_valid_against(&gen_v2));

View File

@@ -4,7 +4,10 @@ use types::{
id::{EndpointId, ProcessId, ThreadId}, 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; pub mod wait_queue;
@@ -14,18 +17,19 @@ mod tests;
pub struct Endpoint { pub struct Endpoint {
id: EndpointId, id: EndpointId,
/// The process that created it; the endpoint dies with its owner.
owner: ProcessId,
waiting_senders: WaitQueue, waiting_senders: WaitQueue,
waiting_receivers: WaitQueue, waiting_receivers: WaitQueue,
generation: Generation,
} }
impl Endpoint { impl Endpoint {
pub fn new(id: EndpointId) -> Self { pub fn new(id: EndpointId, owner: ProcessId) -> Self {
Self { Self {
id, id,
owner,
waiting_senders: WaitQueue::new(), waiting_senders: WaitQueue::new(),
waiting_receivers: WaitQueue::new(), waiting_receivers: WaitQueue::new(),
generation: Generation::initial(),
} }
} }
@@ -33,8 +37,8 @@ impl Endpoint {
&self.id &self.id
} }
pub fn generation(&self) -> &Generation { pub fn owner(&self) -> ProcessId {
&self.generation self.owner
} }
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> { pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
@@ -68,32 +72,52 @@ impl Endpoint {
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool { pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
self.waiting_receivers.remove_thread(thread) 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 { pub struct EndpointTable {
endpoints: [Option<Endpoint>; MAX_ENDPOINTS], endpoints: [Option<Endpoint>; MAX_ENDPOINTS],
generations: SlotGenerations<MAX_ENDPOINTS>,
} }
impl EndpointTable { impl EndpointTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
endpoints: [const { None }; MAX_ENDPOINTS], endpoints: [const { None }; MAX_ENDPOINTS],
generations: SlotGenerations::new(),
} }
} }
pub fn create(&mut self) -> Option<EndpointId> { pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
let idx = self.endpoints.iter().position(|s| s.is_none())?; 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); 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) Some(id)
} }
/// Generation capabilities to a live endpoint must carry.
pub fn generation(&self, id: EndpointId) -> Option<Generation> {
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<Item = EndpointId> + '_ {
self.endpoints
.iter()
.flatten()
.filter(move |ep| ep.owner == owner)
.map(|ep| ep.id)
}
pub fn get(&self, id: EndpointId) -> Option<&Endpoint> { pub fn get(&self, id: EndpointId) -> Option<&Endpoint> {
self.endpoints self.endpoints
.get(id.raw() as usize) .get(id.raw() as usize)
@@ -106,10 +130,12 @@ impl EndpointTable {
.and_then(|s| s.as_mut()) .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<Endpoint> { pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> {
self.endpoints let ep = self.endpoints.get_mut(id.raw() as usize)?.take()?;
.get_mut(id.raw() as usize) self.generations.advance(id.raw() as usize);
.and_then(|s| s.take()) Some(ep)
} }
pub fn remove_thread_from_all(&mut self, thread: ThreadId) { pub fn remove_thread_from_all(&mut self, thread: ThreadId) {

View File

@@ -1,14 +1,18 @@
use crate::endpoint::wait_queue::MAX_WAITERS; use crate::endpoint::wait_queue::MAX_WAITERS;
use super::*; use super::*;
use types::id::{EndpointId, ThreadId}; use types::id::{EndpointId, ProcessId, ThreadId};
fn thread(id: u16) -> ThreadId { fn thread(id: u16) -> ThreadId {
ThreadId::from_raw(id) ThreadId::from_raw(id)
} }
fn owner() -> ProcessId {
ProcessId::from_raw(0)
}
fn test_endpoint() -> Endpoint { fn test_endpoint() -> Endpoint {
Endpoint::new(EndpointId::from_raw(1)) Endpoint::new(EndpointId::from_raw(1), owner())
} }
// --- WaitQueue tests --- // --- WaitQueue tests ---
@@ -99,13 +103,46 @@ fn senders_and_receivers_are_independent() {
} }
#[test] #[test]
fn revoke_bumps_generation() { fn revoke_bumps_generation_but_keeps_the_endpoint() {
let mut ep = test_endpoint(); let mut table = EndpointTable::new();
let gen_before = *ep.generation(); let id = table.create(owner()).unwrap();
ep.revoke(); let gen_before = table.generation(id).unwrap();
let gen_after = *ep.generation(); table.revoke(id);
let gen_after = table.generation(id).unwrap();
assert!(!gen_before.is_valid_against(&gen_after)); 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] #[test]
@@ -141,22 +178,22 @@ fn rendezvous_receiver_first() {
#[test] #[test]
fn table_create_and_lookup() { fn table_create_and_lookup() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
assert!(table.get(id).is_some()); assert!(table.get(id).is_some());
} }
#[test] #[test]
fn table_create_returns_unique_ids() { fn table_create_returns_unique_ids() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let a = table.create().unwrap(); let a = table.create(owner()).unwrap();
let b = table.create().unwrap(); let b = table.create(owner()).unwrap();
assert_ne!(a, b); assert_ne!(a, b);
} }
#[test] #[test]
fn table_destroy_then_get_fails() { fn table_destroy_then_get_fails() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
let removed = table.destroy(id); let removed = table.destroy(id);
assert!(removed.is_some()); assert!(removed.is_some());
assert!(table.get(id).is_none()); assert!(table.get(id).is_none());
@@ -165,7 +202,7 @@ fn table_destroy_then_get_fails() {
#[test] #[test]
fn table_get_mut_allows_enqueue() { fn table_get_mut_allows_enqueue() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table table
.get_mut(id) .get_mut(id)
.unwrap() .unwrap()
@@ -177,8 +214,8 @@ fn table_get_mut_allows_enqueue() {
#[test] #[test]
fn table_destroy_reuses_slot() { fn table_destroy_reuses_slot() {
let mut table = EndpointTable::new(); let mut table = EndpointTable::new();
let id = table.create().unwrap(); let id = table.create(owner()).unwrap();
table.destroy(id); table.destroy(id);
let new_id = table.create().unwrap(); let new_id = table.create(owner()).unwrap();
assert_eq!(id, new_id); assert_eq!(id, new_id);
} }

View File

@@ -31,4 +31,6 @@ pub enum KernelError {
/// The target virtual range already holds a mapping. /// The target virtual range already holds a mapping.
AddressInUse, AddressInUse,
InvalidRegion, InvalidRegion,
/// The object a capability would refer to no longer exists.
NoSuchObject,
} }

View File

@@ -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<const N: usize> {
gens: [Option<Generation>; N],
}
impl<const N: usize> SlotGenerations<N> {
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<Generation> {
*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<const N: usize> Default for SlotGenerations<N> {
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));
}
}

View File

@@ -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");
}

View File

@@ -20,20 +20,35 @@ impl IrqRoutingTable {
} }
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> { pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> {
let idx = irq.raw() as usize; let slot = self
if self.owners[idx].is_some() { .owners
.get_mut(irq.raw() as usize)
.ok_or(KernelError::InvalidIrq(irq))?;
if slot.is_some() {
return Err(KernelError::IrqAlreadyOwned(irq)); return Err(KernelError::IrqAlreadyOwned(irq));
} }
self.owners[idx] = Some(thread); *slot = Some(thread);
Ok(()) Ok(())
} }
pub fn unregister(&mut self, irq: IrqId) { 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<ThreadId> { pub fn owner(&self, irq: IrqId) -> Option<ThreadId> {
self.owners[irq.raw() as usize] *self.owners.get(irq.raw() as usize)?
} }
} }

View File

@@ -7,6 +7,7 @@ pub mod capability;
pub mod endpoint; pub mod endpoint;
pub mod error; pub mod error;
pub mod fault; pub mod fault;
pub mod generations;
pub mod irq; pub mod irq;
pub mod memory; pub mod memory;
pub mod message; pub mod message;

View File

@@ -1,3 +1,4 @@
use crate::generations::SlotGenerations;
use types::{ use types::{
address::{PhysicalAddress, VirtualAddress}, address::{PhysicalAddress, VirtualAddress},
bounded::PageCount, bounded::PageCount,
@@ -43,7 +44,6 @@ pub struct Region {
backing: RegionBacking, backing: RegionBacking,
base: PhysicalAddress, base: PhysicalAddress,
pages: PageCount, pages: PageCount,
generation: Generation,
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS], mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
} }
@@ -59,7 +59,6 @@ impl Region {
backing, backing,
base, base,
pages, pages,
generation: Generation::initial(),
mappings: [None; MAX_REGION_MAPPINGS], mappings: [None; MAX_REGION_MAPPINGS],
} }
} }
@@ -80,14 +79,6 @@ impl Region {
self.pages 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<Item = &RegionMapping> { pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
self.mappings.iter().flatten() self.mappings.iter().flatten()
} }
@@ -124,23 +115,42 @@ impl Region {
pub struct RegionTable { pub struct RegionTable {
regions: [Option<Region>; MAX_REGIONS], regions: [Option<Region>; MAX_REGIONS],
generations: SlotGenerations<MAX_REGIONS>,
} }
impl RegionTable { impl RegionTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
regions: [const { None }; MAX_REGIONS], regions: [const { None }; MAX_REGIONS],
generations: SlotGenerations::new(),
} }
} }
/// Generation capabilities to a live region must carry.
pub fn generation(&self, id: RegionId) -> Option<Generation> {
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<usize> {
(0..MAX_REGIONS).find(|&i| self.regions[i].is_none() && self.generations.is_usable(i))
}
/// The id the next `insert` will return. /// The id the next `insert` will return.
pub fn next_free_id(&self) -> Option<RegionId> { pub fn next_free_id(&self) -> Option<RegionId> {
let idx = self.regions.iter().position(|s| s.is_none())?; let idx = self.free_slot()?;
Some(RegionId::from_raw(idx as u16)) Some(RegionId::from_raw(idx as u16))
} }
pub fn insert(&mut self, region: Region) -> Option<RegionId> { pub fn insert(&mut self, region: Region) -> Option<RegionId> {
let idx = self.regions.iter().position(|s| s.is_none())?; let idx = self.free_slot()?;
self.regions[idx] = Some(region); self.regions[idx] = Some(region);
Some(RegionId::from_raw(idx as u16)) Some(RegionId::from_raw(idx as u16))
} }
@@ -153,8 +163,12 @@ impl RegionTable {
self.regions.get_mut(id.raw() as usize)?.as_mut() 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<Region> { pub fn remove(&mut self, id: RegionId) -> Option<Region> {
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<Item = RegionId> + '_ { pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {

View File

@@ -1,9 +1,10 @@
use types::{ use types::{
bounded::CapSlot, bounded::CapSlot,
generation::Generation,
id::{ProcessId, ThreadId}, id::{ProcessId, ThreadId},
}; };
use crate::{capability::CapabilityTable, error::KernelError}; use crate::{capability::CapabilityTable, error::KernelError, generations::SlotGenerations};
#[cfg(test)] #[cfg(test)]
#[path = "tests.rs"] #[path = "tests.rs"]
@@ -75,24 +76,30 @@ impl Process {
pub struct ProcessTable { pub struct ProcessTable {
processes: [Option<Process>; MAX_PROCESSES], processes: [Option<Process>; MAX_PROCESSES],
generations: SlotGenerations<MAX_PROCESSES>,
} }
impl ProcessTable { impl ProcessTable {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
processes: [const { None }; MAX_PROCESSES], processes: [const { None }; MAX_PROCESSES],
generations: SlotGenerations::new(),
} }
} }
/// Generation capabilities to a live process must carry.
pub fn generation(&self, id: ProcessId) -> Option<Generation> {
self.get(id).ok()?;
self.generations.current(id.raw() as usize)
}
pub fn create( pub fn create(
&mut self, &mut self,
capabilities: CapabilityTable, capabilities: CapabilityTable,
fault_handler: Option<CapSlot>, fault_handler: Option<CapSlot>,
) -> Result<ProcessId, KernelError> { ) -> Result<ProcessId, KernelError> {
let idx = self let idx = (0..MAX_PROCESSES)
.processes .find(|&i| self.processes[i].is_none() && self.generations.is_usable(i))
.iter()
.position(|s| s.is_none())
.ok_or(KernelError::TooManyProcesses)?; .ok_or(KernelError::TooManyProcesses)?;
let id = ProcessId::from_raw(idx as u16); let id = ProcessId::from_raw(idx as u16);
@@ -116,12 +123,17 @@ impl ProcessTable {
.ok_or(KernelError::InvalidProcess(id)) .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<Process, KernelError> { pub fn destroy(&mut self, id: ProcessId) -> Result<Process, KernelError> {
let idx = id.raw() as usize; let idx = id.raw() as usize;
self.processes let process = self
.processes
.get_mut(idx) .get_mut(idx)
.and_then(|s| s.take()) .and_then(|s| s.take())
.ok_or(KernelError::InvalidProcess(id)) .ok_or(KernelError::InvalidProcess(id))?;
self.generations.advance(idx);
Ok(process)
} }
} }

View File

@@ -38,6 +38,14 @@ impl MultiCoreScheduler {
self.cores[core.raw() as usize].set_current(thread); 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 { fn thread_count_on(&self, core: CoreId) -> usize {
self.placements self.placements
.iter() .iter()

View File

@@ -39,6 +39,15 @@ impl PerCoreScheduler {
Some(thread) 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) { pub fn clear_current(&mut self) {
self.current = None; self.current = None;
} }

View File

@@ -284,3 +284,26 @@ fn removed_thread_is_skipped_and_order_kept() {
assert_eq!(q.pop_highest().unwrap().0, thread(3)); assert_eq!(q.pop_highest().unwrap().0, thread(3));
assert!(q.pop_highest().is_none()); 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);
}

View File

@@ -69,6 +69,10 @@ pub enum Syscall {
Revoke { Revoke {
slot: CapSlot, slot: CapSlot,
}, },
/// Removes a capability from the caller's own table.
DropCap {
slot: CapSlot,
},
SpawnThread { SpawnThread {
entry: EntryPoint, entry: EntryPoint,
stack: StackPointer, stack: StackPointer,

View File

@@ -20,6 +20,7 @@ pub struct Thread {
core_affinity: Option<CoreId>, core_affinity: Option<CoreId>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadState { pub enum ThreadState {
Ready, Ready,
Running(CoreId), Running(CoreId),
@@ -77,6 +78,7 @@ impl SavedContext {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockReason { pub enum BlockReason {
Sending(CapSlot), Sending(CapSlot),
Receiving(CapSlot), Receiving(CapSlot),

View File

@@ -205,8 +205,13 @@ impl<P: Platform> Kernel<P> {
Some(tid) Some(tid)
} }
pub fn create_endpoint(&mut self) -> Result<types::id::EndpointId, KernelError> { /// Creates an endpoint owned by `owner`; it is destroyed with `owner`.
self.endpoints.create().ok_or(KernelError::NoFreeSlot) pub fn create_endpoint(
&mut self,
owner: ProcessId,
) -> Result<types::id::EndpointId, KernelError> {
self.processes.get(owner)?;
self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot)
} }
pub fn grant_capability( pub fn grant_capability(
@@ -237,8 +242,8 @@ impl<P: Platform> Kernel<P> {
if let Ok(t) = self.threads.get(tid) if let Ok(t) = self.threads.get(tid)
&& matches!(t.state(), ThreadState::Ready) && 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)); let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
} }
} }
@@ -274,16 +279,9 @@ impl<P: Platform> Kernel<P> {
}; };
self.cleanup_threads(&thread_ids); self.cleanup_threads(&thread_ids);
self.destroy_endpoints_owned_by(pid);
self.teardown_memory(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); let _ = self.processes.destroy(pid);
} }
@@ -292,6 +290,7 @@ impl<P: Platform> Kernel<P> {
for tid in thread_ids.iter().flatten() { for tid in thread_ids.iter().flatten() {
self.endpoints.remove_thread_from_all(*tid); self.endpoints.remove_thread_from_all(*tid);
self.cleanup_wait_any(*tid); self.cleanup_wait_any(*tid);
self.irqs.unregister_thread(*tid);
self.scheduler.remove(*tid); self.scheduler.remove(*tid);
self.pending_messages[tid.raw() as usize] = None; self.pending_messages[tid.raw() as usize] = None;
self.pending_results[tid.raw() as usize] = None; self.pending_results[tid.raw() as usize] = None;
@@ -299,6 +298,41 @@ impl<P: Platform> Kernel<P> {
} }
} }
/// 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) { pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) {
if self.wait_sets[tid.raw() as usize].take().is_some() { if self.wait_sets[tid.raw() as usize].take().is_some() {
self.endpoints.remove_thread_from_all(tid); self.endpoints.remove_thread_from_all(tid);
@@ -316,41 +350,27 @@ impl<P: Platform> Kernel<P> {
if !cap.permissions().allows(needed) { if !cap.permissions().allows(needed) {
return Err(KernelError::PermissionDenied { slot, needed }); return Err(KernelError::PermissionDenied { slot, needed });
} }
let current_gen = self.resource_generation(cap.resource()); match self.resource_generation(cap.resource()) {
if !cap.generation().is_valid_against(&current_gen) { Some(current) if cap.generation().is_valid_against(&current) => Ok(cap),
return Err(KernelError::RevokedCapability(slot)); _ => 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<Generation> {
match resource { match resource {
Resource::Endpoint(id) => self Resource::Endpoint(id) => self.endpoints.generation(*id),
.endpoints Resource::Memory(id) => self.regions.generation(*id),
.get(*id) Resource::Process(pid) => self.processes.generation(*pid),
.map(|ep| *ep.generation()) Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()),
.unwrap_or_else(Generation::initial),
Resource::Memory(id) => self
.regions
.get(*id)
.map(|r| *r.generation())
.unwrap_or_else(Generation::initial),
_ => Generation::initial(),
} }
} }
pub fn revoke_resource(&mut self, resource: &Resource) { pub fn revoke_resource(&mut self, resource: &Resource) {
match resource { match resource {
Resource::Endpoint(id) => { Resource::Endpoint(id) => self.endpoints.revoke(*id),
if let Some(ep) = self.endpoints.get_mut(*id) { Resource::Memory(id) => self.regions.revoke(*id),
ep.revoke();
}
}
Resource::Memory(id) => {
if let Some(r) = self.regions.get_mut(*id) {
r.revoke();
}
}
_ => {} _ => {}
} }
} }
@@ -361,7 +381,9 @@ impl<P: Platform> Kernel<P> {
resource: Resource, resource: Resource,
permissions: PermissionSet, permissions: PermissionSet,
) -> Result<CapSlot, KernelError> { ) -> Result<CapSlot, KernelError> {
let generation = self.resource_generation(&resource); let generation = self
.resource_generation(&resource)
.ok_or(KernelError::NoSuchObject)?;
let cap = Capability::new(resource, permissions, generation); let cap = Capability::new(resource, permissions, generation);
self.processes.get_mut(to)?.capabilities_mut().insert(cap) self.processes.get_mut(to)?.capabilities_mut().insert(cap)
} }

View File

@@ -1,6 +1,5 @@
use super::{arbitrary_endpoint_cap, insert_cap}; use super::{arbitrary_endpoint_cap, insert_cap};
use domain::capability::{Capability, CapabilityTable}; use domain::capability::{Capability, CapabilityTable};
use domain::endpoint::Endpoint;
use domain::resource::Resource; use domain::resource::Resource;
use types::generation::Generation; use types::generation::Generation;
use types::id::EndpointId; use types::id::EndpointId;
@@ -62,89 +61,3 @@ fn grant_cap_preserves_resource_identity() {
_ => panic!("resource type changed"), _ => 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(&current_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));
}

View File

@@ -59,4 +59,19 @@ impl<P: Platform> Kernel<P> {
self.revoke_resource(&resource); self.revoke_resource(&resource);
SyscallOutcome::Complete(SyscallResult::Ok) 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),
})
}
} }

View File

@@ -8,7 +8,7 @@ impl<P: Platform> Kernel<P> {
&mut self, &mut self,
caller_pid: types::id::ProcessId, caller_pid: types::id::ProcessId,
) -> SyscallOutcome { ) -> SyscallOutcome {
let ep_id = match self.create_endpoint() { let ep_id = match self.create_endpoint(caller_pid) {
Ok(id) => id, Ok(id) => id,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };

View File

@@ -159,6 +159,10 @@ impl<P: Platform> Kernel<P> {
}; };
self.destroy_process(target_pid); 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) SyscallOutcome::Complete(SyscallResult::Ok)
} }
} }

View File

@@ -48,6 +48,7 @@ impl<P: Platform> Kernel<P> {
} }
Syscall::Kill { process } => self.handle_kill(pid, process), Syscall::Kill { process } => self.handle_kill(pid, process),
Syscall::Revoke { slot } => self.handle_revoke(pid, slot), Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),
Syscall::Delegate { Syscall::Delegate {
slot, slot,
to_endpoint, to_endpoint,

View File

@@ -70,10 +70,7 @@ impl<P: Platform> Kernel<P> {
caller_tid: types::id::ThreadId, caller_tid: types::id::ThreadId,
pid: types::id::ProcessId, pid: types::id::ProcessId,
) -> SyscallOutcome { ) -> SyscallOutcome {
use domain::scheduler::Scheduler; self.cleanup_threads(&[Some(caller_tid)]);
self.scheduler.remove(caller_tid);
let _ = self.threads.destroy(caller_tid);
if let Ok(process) = self.processes.get_mut(pid) { if let Ok(process) = self.processes.get_mut(pid) {
process.remove_thread(caller_tid); process.remove_thread(caller_tid);

View File

@@ -74,14 +74,16 @@ impl Kernel<MockPlatform> {
if let Ok(t) = self.threads.get(tid) if let Ok(t) = self.threads.get(tid)
&& matches!(t.state(), ThreadState::Ready) && 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)); let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
} }
} }
pub fn create_endpoint_for_test(&mut self) -> EndpointId { 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( pub fn grant_cap_for_test(
@@ -93,4 +95,40 @@ impl Kernel<MockPlatform> {
self.grant_cap(pid, resource, permissions) self.grant_cap(pid, resource, permissions)
.expect("grant cap") .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<ThreadState> {
self.threads.get(tid).ok().map(|t| *t.state())
}
} }

View File

@@ -52,6 +52,7 @@ struct World {
caps: [CapSlot; PROCS], caps: [CapSlot; PROCS],
known: [[usize; 64]; PROCS], known: [[usize; 64]; PROCS],
known_len: [usize; PROCS], known_len: [usize; PROCS],
rebirths: usize,
} }
impl World { impl World {
@@ -80,6 +81,7 @@ impl World {
caps, caps,
known: [[0; 64]; PROCS], known: [[0; 64]; PROCS],
known_len: [0; 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); let victim = 1 + rng.below(PROCS - 1);
if self.alive[victim] { let stale = self.caps[victim];
self.k.handle_syscall(Syscall::Kill { self.k.handle_syscall(Syscall::Kill { process: stale });
process: self.caps[victim], 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,
}); });
self.alive[victim] = 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, frames_owned + region_pages,
"quota of {pid:?} drifted" "quota of {pid:?} drifted"
); );
if !self.alive[i] { if !self.alive[i] && i != 0 {
assert_eq!(k.page_usage(pid), 0); assert_eq!(k.page_usage(pid), 0);
assert_eq!( assert_eq!(
k.platform.mapping_count(pid), k.platform.mapping_count(pid),
@@ -257,5 +291,9 @@ fn memory_model_holds_under_random_hostile_operations() {
world.step(&mut rng); world.step(&mut rng);
world.check(); world.check();
} }
assert!(
world.rebirths >= 5,
"seed {seed:#x} barely exercised kill/re-create"
);
} }
} }

View File

@@ -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<MockPlatform>) -> 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<MockPlatform>) -> (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));
}

View File

@@ -7,6 +7,7 @@ mod invariants;
mod ipc; mod ipc;
mod isolation; mod isolation;
mod lifecycle; mod lifecycle;
mod lifetimes;
mod memory; mod memory;
mod simple; mod simple;
mod tick; mod tick;

View File

@@ -80,6 +80,11 @@ pub fn revoke(slot: CapSlot) -> bool {
arch::syscall1(SYS_REVOKE, slot.value() as usize) == 0 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) { pub fn debug_putchar(byte: u8) {
arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize); arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize);
} }

View File

@@ -6,15 +6,22 @@ impl Generation {
Self(0) Self(0)
} }
pub fn next(&self) -> Self { /// The following generation, or `None` once the counter is exhausted.
Self(self.0.wrapping_add(1)) /// Never wraps: a wrapped counter would make stale capabilities valid.
pub fn next(&self) -> Option<Self> {
self.0.checked_add(1).map(Self)
}
pub fn raw(&self) -> u32 {
self.0
} }
pub fn is_valid_against(&self, current: &Generation) -> bool { pub fn is_valid_against(&self, current: &Generation) -> bool {
self == current 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 { pub fn from_raw(value: u32) -> Self {
Self(value) Self(value)
} }

View File

@@ -1,10 +1,17 @@
use crate::generation::Generation; 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] #[kani::proof]
fn generation_next_always_invalidates() { fn generation_next_is_strictly_greater_or_exhausted() {
let value: u32 = kani::any(); let value: u32 = kani::any();
let current = Generation::from_raw(value); let current = Generation::from_raw(value);
let next = current.next(); match current.next() {
Some(next) => {
assert!(next.raw() > current.raw());
assert!(!current.is_valid_against(&next)); assert!(!current.is_valid_against(&next));
}
None => assert!(value == u32::MAX),
}
} }

View File

@@ -20,3 +20,4 @@ pub const SYS_CREATE_ENDPOINT: usize = 18;
pub const SYS_DEBUG_PUTCHAR: usize = 19; pub const SYS_DEBUG_PUTCHAR: usize = 19;
pub const SYS_MAP_PAGE: usize = 20; pub const SYS_MAP_PAGE: usize = 20;
pub const SYS_SPAWN_THREAD_IN: usize = 21; pub const SYS_SPAWN_THREAD_IN: usize = 21;
pub const SYS_DROP_CAP: usize = 22;