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

@@ -74,14 +74,16 @@ impl Kernel<MockPlatform> {
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<MockPlatform> {
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<ThreadState> {
self.threads.get(tid).ok().map(|t| *t.state())
}
}

View File

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

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 isolation;
mod lifecycle;
mod lifetimes;
mod memory;
mod simple;
mod tick;