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:
@@ -205,8 +205,13 @@ impl<P: Platform> Kernel<P> {
|
||||
Some(tid)
|
||||
}
|
||||
|
||||
pub fn create_endpoint(&mut self) -> Result<types::id::EndpointId, KernelError> {
|
||||
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<types::id::EndpointId, KernelError> {
|
||||
self.processes.get(owner)?;
|
||||
self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot)
|
||||
}
|
||||
|
||||
pub fn grant_capability(
|
||||
@@ -237,8 +242,8 @@ impl<P: Platform> Kernel<P> {
|
||||
|
||||
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<P: Platform> Kernel<P> {
|
||||
};
|
||||
|
||||
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<P: Platform> Kernel<P> {
|
||||
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<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) {
|
||||
if self.wait_sets[tid.raw() as usize].take().is_some() {
|
||||
self.endpoints.remove_thread_from_all(tid);
|
||||
@@ -316,41 +350,27 @@ impl<P: Platform> Kernel<P> {
|
||||
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<Generation> {
|
||||
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<P: Platform> Kernel<P> {
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
) -> 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);
|
||||
self.processes.get_mut(to)?.capabilities_mut().insert(cap)
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -59,4 +59,19 @@ impl<P: Platform> Kernel<P> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ impl<P: Platform> Kernel<P> {
|
||||
&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)),
|
||||
};
|
||||
|
||||
@@ -159,6 +159,10 @@ impl<P: Platform> Kernel<P> {
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ impl<P: Platform> Kernel<P> {
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -70,10 +70,7 @@ impl<P: Platform> Kernel<P> {
|
||||
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);
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod invariants;
|
||||
mod ipc;
|
||||
mod isolation;
|
||||
mod lifecycle;
|
||||
mod lifetimes;
|
||||
mod memory;
|
||||
mod simple;
|
||||
mod tick;
|
||||
|
||||
Reference in New Issue
Block a user