//! 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::ThreadState; use types::address::DataRef; use types::bounded::CapSlot; use types::id::ThreadId; use types::permission::{Permission, PermissionSet}; fn create_process(k: &mut Kernel) -> 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) -> (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 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)); } #[test] fn exiting_a_thread_unbinds_its_notification() { let (mut k, pid_a) = kernel_with_process(); let tid_a = k.current_thread().unwrap(); let n = k.create_notification(pid_a).unwrap(); let rw = PermissionSet::empty() .with(Permission::Read) .with(Permission::Write); let ncap = k.grant_cap_for_test(pid_a, Resource::Notification(n), rw); assert!(!is_error(&k.handle_syscall(Syscall::BindNotification { notification: ncap }))); let other = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT); assert!(matches!( k.handle_syscall(Syscall::ExitThread), SyscallOutcome::TerminateCaller )); // A new thread reuses the id and blocks in an unrelated receive. let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT); assert_eq!(reused, tid_a); let ep = k.create_endpoint_for_test(); let recv = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms()); k.switch_to_thread_for_test(reused); assert!(matches!( k.handle_syscall(Syscall::Receive { endpoint: recv }), SyscallOutcome::BlockCaller(_) )); k.switch_to_thread_for_test(other); k.handle_syscall(Syscall::Signal { notification: ncap }); assert!( matches!( k.thread_state_for_test(reused), Some(ThreadState::Blocked(_)) ), "signal reached a thread that never bound the notification" ); }