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

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

View File

@@ -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<Endpoint>; MAX_ENDPOINTS],
generations: SlotGenerations<MAX_ENDPOINTS>,
}
impl EndpointTable {
pub fn new() -> Self {
Self {
endpoints: [const { None }; MAX_ENDPOINTS],
generations: SlotGenerations::new(),
}
}
pub fn create(&mut self) -> Option<EndpointId> {
let idx = self.endpoints.iter().position(|s| s.is_none())?;
pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
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<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> {
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<Endpoint> {
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) {

View File

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

View File

@@ -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,
}

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> {
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<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 error;
pub mod fault;
pub mod generations;
pub mod irq;
pub mod memory;
pub mod message;

View File

@@ -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<RegionMapping>; 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<Item = &RegionMapping> {
self.mappings.iter().flatten()
}
@@ -124,23 +115,42 @@ impl Region {
pub struct RegionTable {
regions: [Option<Region>; MAX_REGIONS],
generations: SlotGenerations<MAX_REGIONS>,
}
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<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.
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))
}
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);
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<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> + '_ {

View File

@@ -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<Process>; MAX_PROCESSES],
generations: SlotGenerations<MAX_PROCESSES>,
}
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<Generation> {
self.get(id).ok()?;
self.generations.current(id.raw() as usize)
}
pub fn create(
&mut self,
capabilities: CapabilityTable,
fault_handler: Option<CapSlot>,
) -> Result<ProcessId, KernelError> {
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<Process, KernelError> {
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)
}
}

View File

@@ -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()

View File

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

View File

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

View File

@@ -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,

View File

@@ -20,6 +20,7 @@ pub struct Thread {
core_affinity: Option<CoreId>,
}
#[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),