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:
255
crates/kernel/src/tests/lifetimes.rs
Normal file
255
crates/kernel/src/tests/lifetimes.rs
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user