From 76f10ff1f172272fd4f185ce4d20729b278a5173 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 20:27:38 +0100 Subject: [PATCH] feat(kernel): seL4-style IPC, notifications, status ABI - badges + Mint, Call/Reply/ReplyRecv w/ gen-checked reply slot - cap transfer (1/msg, copy, needs Delegate, rechecked at delivery) - notifications: signal/wait/poll/bind, irq -> notification routing - WaitAny/WaitForInterrupt removed; a0=status, ErrorCode - libkos: single ecall primitive, compat wrappers + typed new api - fix: trap return clobbered user t0 on every trap - fix: wait queues FIFO, delegate keeps badge, fault cap checked - docs/ipc.md spec, ipc tests, ipc fuzz, kani ntfn + waitqueue --- crates/adapter-riscv/src/trap/mod.rs | 303 +++--- crates/domain/src/capability/mod.rs | 15 +- crates/domain/src/endpoint/mod.rs | 8 + crates/domain/src/endpoint/tests.rs | 47 +- crates/domain/src/endpoint/wait_queue.rs | 118 ++- crates/domain/src/error.rs | 5 + crates/domain/src/irq/mod.rs | 58 +- crates/domain/src/irq/tests.rs | 63 +- crates/domain/src/lib.rs | 1 + crates/domain/src/message/mod.rs | 59 +- crates/domain/src/message/tests.rs | 73 +- crates/domain/src/notification/mod.rs | 186 ++++ crates/domain/src/notification/proofs.rs | 43 + crates/domain/src/notification/tests.rs | 116 +++ crates/domain/src/proofs/thread.rs | 6 +- crates/domain/src/resource.rs | 3 +- crates/domain/src/scheduler/multi_core.rs | 7 + crates/domain/src/scheduler/per_core.rs | 4 + crates/domain/src/syscall.rs | 72 +- crates/domain/src/thread/mod.rs | 64 +- crates/domain/src/thread/tests.rs | 45 +- crates/kernel/src/interrupt.rs | 20 +- crates/kernel/src/lib.rs | 65 +- crates/kernel/src/syscall/capability.rs | 24 +- crates/kernel/src/syscall/fault.rs | 69 +- crates/kernel/src/syscall/ipc.rs | 827 ++++++++++------- crates/kernel/src/syscall/mod.rs | 119 ++- crates/kernel/src/tests/helpers.rs | 10 +- crates/kernel/src/tests/ipc.rs | 1017 +++++++++++++++++---- crates/kernel/src/tests/ipc_invariants.rs | 350 +++++++ crates/kernel/src/tests/lifetimes.rs | 95 +- crates/kernel/src/tests/mod.rs | 1 + crates/kernel/src/tests/tick.rs | 25 +- crates/libkos/src/alloc_impl.rs | 8 +- crates/libkos/src/arch/mock.rs | 29 +- crates/libkos/src/arch/riscv.rs | 112 +-- crates/libkos/src/display.rs | 11 +- crates/libkos/src/fs.rs | 25 +- crates/libkos/src/input.rs | 2 +- crates/libkos/src/lib.rs | 8 +- crates/libkos/src/syscall.rs | 327 ++++++- crates/types/src/badge.rs | 21 + crates/types/src/id.rs | 1 + crates/types/src/lib.rs | 1 + crates/types/src/syscall_abi.rs | 67 +- docs/ipc.md | 105 +++ 46 files changed, 3339 insertions(+), 1296 deletions(-) create mode 100644 crates/domain/src/notification/mod.rs create mode 100644 crates/domain/src/notification/proofs.rs create mode 100644 crates/domain/src/notification/tests.rs create mode 100644 crates/kernel/src/tests/ipc_invariants.rs create mode 100644 crates/types/src/badge.rs create mode 100644 docs/ipc.md diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 8d4ce8d..484f4c8 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -75,7 +75,6 @@ _restore_smode: ld x1, 8(sp) ld x3, 24(sp) ld x4, 32(sp) - ld x5, 40(sp) ld x6, 48(sp) ld x7, 56(sp) ld x8, 64(sp) @@ -103,13 +102,19 @@ _restore_smode: ld x30, 240(sp) ld x31, 248(sp) - addi sp, sp, 272 - + # Every GPR except t0 (x5) is restored. Decide where we return to while + # t0 is still free, and restore it last on each path; using t0 after + # its restore would corrupt the interrupted program's t0. csrr t0, sstatus andi t0, t0, 0x100 bnez t0, _sret_smode + ld x5, 40(sp) + addi sp, sp, 272 csrrw sp, sscratch, sp + sret _sret_smode: + ld x5, 40(sp) + addi sp, sp, 272 sret "# ); @@ -139,156 +144,138 @@ pub fn set_kernel_stack(kernel_sp: usize) { } } +/// Decodes the syscall in `frame`, or `None` for an unknown number or an +/// argument that is not representable (e.g. a slot out of range). fn decode_syscall(frame: &TrapFrame) -> Option { use domain::message::Message; - use domain::syscall::{Syscall, WaitSet}; + use domain::syscall::Syscall; + use types::address::{DataRef, EntryPoint, StackPointer, VirtualAddress}; + use types::badge::Badge; + use types::bounded::{CapSlot, PageCount}; + use types::permission::PermissionSet; use types::syscall_abi::*; let nr = frame.regs[17]; // a7 - let a0 = frame.regs[10]; - let a1 = frame.regs[11]; - let a2 = frame.regs[12]; - let a3 = frame.regs[13]; - let a4 = frame.regs[14]; + let a = |i: usize| frame.regs[10 + i]; + let slot = |i: usize| CapSlot::new(u8::try_from(a(i)).ok()?); + let message = || -> Option { + let msg = Message::from_words([a(1), a(2), a(3), a(4)]); + Some(match a(5) { + NONE => msg, + _ => msg.with_cap(slot(5)?), + }) + }; - match nr { - SYS_EXIT => Some(Syscall::Exit), - SYS_HALT => Some(Syscall::Halt), - SYS_EXIT_THREAD => Some(Syscall::ExitThread), - SYS_YIELD => Some(Syscall::Yield), - SYS_GET_TIME => Some(Syscall::GetTime), - SYS_GET_PROCESS_ID => Some(Syscall::GetProcessId), - SYS_SEND => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - let msg = Message::from_words([a1, a2, a3, a4]); - Some(Syscall::Send { - endpoint: slot, - message: msg, - }) - } - SYS_RECEIVE => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::Receive { endpoint: slot }) - } - SYS_DELEGATE => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - let to = types::bounded::CapSlot::new(a1 as u8)?; - let perms = types::permission::PermissionSet::from_bits(a2 as u8); - Some(Syscall::Delegate { - slot, - to_endpoint: to, - permissions: perms, - }) - } - SYS_WAIT_FOR_INTERRUPT => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::WaitForInterrupt { irq: slot }) - } - SYS_ACKNOWLEDGE_INTERRUPT => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::AcknowledgeInterrupt { irq: slot }) - } - SYS_WAIT_ANY => { - let ep0 = if a0 != usize::MAX { - types::bounded::CapSlot::new(a0 as u8) - } else { - None - }; - let ep1 = if a1 != usize::MAX { - types::bounded::CapSlot::new(a1 as u8) - } else { - None - }; - let ep2 = if a2 != usize::MAX { - types::bounded::CapSlot::new(a2 as u8) - } else { - None - }; - let ep3 = if a3 != usize::MAX { - types::bounded::CapSlot::new(a3 as u8) - } else { - None - }; - let sources = WaitSet::new([ep0, ep1, ep2, ep3], [None, None, None, None]); - Some(Syscall::WaitAny { sources }) - } - SYS_CREATE_PROCESS => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::CreateProcess { - create_cap: slot, - image: types::address::DataRef::empty(), - }) - } - SYS_KILL => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::Kill { process: slot }) - } - SYS_REVOKE => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::Revoke { slot }) - } - SYS_SPAWN_THREAD => Some(Syscall::SpawnThread { - entry: types::address::EntryPoint::from_raw(a0), - stack: types::address::StackPointer::from_raw(a1), - }), - SYS_REQUEST_PAGES => { - let count = types::bounded::PageCount::new(a0 as u16)?; - Some(Syscall::RequestPages { count }) - } - SYS_RELEASE_PAGES => { - let count = types::bounded::PageCount::new(a1 as u16)?; - Some(Syscall::ReleasePages { - addr: types::address::VirtualAddress::from_raw(a0), - count, - }) - } - SYS_SPAWN_THREAD_IN => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::SpawnThreadIn { - process: slot, - entry: types::address::EntryPoint::from_raw(a1), - stack: types::address::StackPointer::from_raw(a2), - }) - } - SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint), - SYS_DROP_CAP => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::DropCap { slot }) - } - SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }), - SYS_MAP_PAGE => { - let slot = types::bounded::CapSlot::new(a0 as u8)?; - Some(Syscall::MapPage { - process: slot, - src_va: types::address::VirtualAddress::from_raw(a1), - dst_va: types::address::VirtualAddress::from_raw(a2), - executable: a3 != 0, - }) - } - _ => None, - } + Some(match nr { + SYS_EXIT => Syscall::Exit, + SYS_HALT => Syscall::Halt, + SYS_EXIT_THREAD => Syscall::ExitThread, + SYS_YIELD => Syscall::Yield, + SYS_GET_TIME => Syscall::GetTime, + SYS_GET_PROCESS_ID => Syscall::GetProcessId, + SYS_SEND => Syscall::Send { + endpoint: slot(0)?, + message: message()?, + }, + SYS_CALL => Syscall::Call { + endpoint: slot(0)?, + message: message()?, + }, + SYS_RECEIVE => Syscall::Receive { endpoint: slot(0)? }, + SYS_REPLY => Syscall::Reply { + message: message()?, + }, + SYS_REPLY_RECV => Syscall::ReplyRecv { + endpoint: slot(0)?, + message: message()?, + }, + SYS_DELEGATE => Syscall::Delegate { + slot: slot(0)?, + to_endpoint: slot(1)?, + permissions: PermissionSet::from_bits(u8::try_from(a(2)).ok()?), + }, + SYS_MINT => Syscall::Mint { + slot: slot(0)?, + badge: Badge::new(a(1)), + permissions: PermissionSet::from_bits(u8::try_from(a(2)).ok()?), + }, + SYS_CREATE_NOTIFICATION => Syscall::CreateNotification, + SYS_SIGNAL => Syscall::Signal { + notification: slot(0)?, + }, + SYS_WAIT => Syscall::Wait { + notification: slot(0)?, + }, + SYS_POLL => Syscall::Poll { + notification: slot(0)?, + }, + SYS_BIND_NOTIFICATION => Syscall::BindNotification { + notification: slot(0)?, + }, + SYS_SET_IRQ_NOTIFICATION => Syscall::SetIrqNotification { + irq: slot(0)?, + notification: slot(1)?, + }, + SYS_ACKNOWLEDGE_INTERRUPT => Syscall::AcknowledgeInterrupt { irq: slot(0)? }, + SYS_CREATE_PROCESS => Syscall::CreateProcess { + create_cap: slot(0)?, + image: DataRef::empty(), + }, + SYS_KILL => Syscall::Kill { process: slot(0)? }, + SYS_REVOKE => Syscall::Revoke { slot: slot(0)? }, + SYS_DROP_CAP => Syscall::DropCap { slot: slot(0)? }, + SYS_SPAWN_THREAD => Syscall::SpawnThread { + entry: EntryPoint::from_raw(a(0)), + stack: StackPointer::from_raw(a(1)), + }, + SYS_REQUEST_PAGES => Syscall::RequestPages { + count: PageCount::new(u16::try_from(a(0)).ok()?)?, + }, + SYS_RELEASE_PAGES => Syscall::ReleasePages { + addr: VirtualAddress::from_raw(a(0)), + count: PageCount::new(u16::try_from(a(1)).ok()?)?, + }, + SYS_SPAWN_THREAD_IN => Syscall::SpawnThreadIn { + process: slot(0)?, + entry: EntryPoint::from_raw(a(1)), + stack: StackPointer::from_raw(a(2)), + }, + SYS_CREATE_ENDPOINT => Syscall::CreateEndpoint, + SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a(0) as u8 }, + SYS_MAP_PAGE => Syscall::MapPage { + process: slot(0)?, + src_va: VirtualAddress::from_raw(a(1)), + dst_va: VirtualAddress::from_raw(a(2)), + executable: a(3) != 0, + }, + _ => return None, + }) } -fn write_result(frame: &mut TrapFrame, outcome: &kernel::SyscallOutcome) { - use kernel::{SyscallOutcome, SyscallResult}; +/// Writes a completed syscall's result per the register ABI: status in +/// `a0`, results in `a1..`. +fn write_result(frame: &mut TrapFrame, result: &kernel::SyscallResult) { + use kernel::SyscallResult; + use kernel::syscall::DeliveryKind; + use types::syscall_abi::{KIND_CALL, KIND_MESSAGE, KIND_SIGNAL, NONE}; - match outcome { - SyscallOutcome::Complete(result) => match result { - SyscallResult::Ok => frame.regs[10] = 0, - SyscallResult::Value(v) => frame.regs[10] = *v, - SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize, - SyscallResult::Message(m) => { - frame.regs[10] = m.word(0); - frame.regs[11] = m.word(1); - frame.regs[12] = m.word(2); - frame.regs[13] = m.word(3); - } - SyscallResult::Error(_) => frame.regs[10] = usize::MAX, - SyscallResult::WakeReason(_) => frame.regs[10] = 0, - }, - SyscallOutcome::BlockCaller(_) => {} - SyscallOutcome::SwitchTo(_) => frame.regs[10] = 0, - SyscallOutcome::TerminateCaller => {} + let r = &mut frame.regs; + r[10] = 0; + match result { + SyscallResult::Ok => {} + SyscallResult::Value(v) => r[11] = *v, + SyscallResult::Slot(s) => r[11] = s.value() as usize, + SyscallResult::Received(d) => { + r[11] = d.badge; + r[12..16].copy_from_slice(&d.words); + r[16] = d.cap.map_or(NONE, |s| s.value() as usize); + r[17] = match d.kind { + DeliveryKind::Message => KIND_MESSAGE, + DeliveryKind::Call => KIND_CALL, + DeliveryKind::Signal => KIND_SIGNAL, + }; + } + SyscallResult::Error(e) => r[10] = kernel::syscall::error_code(e) as usize, } } @@ -310,24 +297,7 @@ fn load_context_to_frame(ctx: &domain::thread::SavedContext, frame: &mut TrapFra fn apply_pending_result(frame: &mut TrapFrame, tid: types::id::ThreadId) { let kern = unsafe { &mut *crate::kernel_ptr() }; if let Some(result) = kern.take_pending_result(tid) { - write_result_direct(frame, &result); - } -} - -fn write_result_direct(frame: &mut TrapFrame, result: &kernel::SyscallResult) { - use kernel::SyscallResult; - match result { - SyscallResult::Ok => frame.regs[10] = 0, - SyscallResult::Value(v) => frame.regs[10] = *v, - SyscallResult::Slot(s) => frame.regs[10] = s.value() as usize, - SyscallResult::Message(m) => { - frame.regs[10] = m.word(0); - frame.regs[11] = m.word(1); - frame.regs[12] = m.word(2); - frame.regs[13] = m.word(3); - } - SyscallResult::Error(_) => frame.regs[10] = usize::MAX, - SyscallResult::WakeReason(_) => frame.regs[10] = 0, + write_result(frame, &result); } } @@ -441,14 +411,17 @@ fn handle_exception(frame: &mut TrapFrame, scause: usize) { CAUSE_ECALL_U => { frame.sepc += 4; - if let Some(syscall) = decode_syscall(frame) { + let Some(syscall) = decode_syscall(frame) else { + frame.regs[10] = types::syscall_abi::ErrorCode::UnknownSyscall as usize; + return; + }; + { let kern = unsafe { &mut *crate::kernel_ptr() }; let current_tid = kern.current_thread(); let outcome = kern.handle_syscall(syscall); - write_result(frame, &outcome); match outcome { - kernel::SyscallOutcome::Complete(_) => {} + kernel::SyscallOutcome::Complete(result) => write_result(frame, &result), kernel::SyscallOutcome::BlockCaller(_) => { if let Some(tid) = current_tid { switch_to_next(frame, tid); diff --git a/crates/domain/src/capability/mod.rs b/crates/domain/src/capability/mod.rs index 3bbe6f9..6d204d9 100644 --- a/crates/domain/src/capability/mod.rs +++ b/crates/domain/src/capability/mod.rs @@ -1,4 +1,4 @@ -use types::{bounded::CapSlot, generation::Generation, permission::PermissionSet}; +use types::{badge::Badge, bounded::CapSlot, generation::Generation, permission::PermissionSet}; pub mod authority; @@ -11,10 +11,12 @@ mod tests; // Sized so that every representable `CapSlot` indexes a real slot. pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1; +#[derive(Clone)] pub struct Capability { resource: Resource, permissions: PermissionSet, generation: Generation, + badge: Badge, } impl Capability { @@ -23,9 +25,20 @@ impl Capability { resource, permissions, generation, + badge: Badge::NONE, } } + /// The same capability with `badge` stamped on it. + pub fn with_badge(mut self, badge: Badge) -> Self { + self.badge = badge; + self + } + + pub fn badge(&self) -> Badge { + self.badge + } + pub fn resource(&self) -> &Resource { &self.resource } diff --git a/crates/domain/src/endpoint/mod.rs b/crates/domain/src/endpoint/mod.rs index 7789e3e..7dbe6cc 100644 --- a/crates/domain/src/endpoint/mod.rs +++ b/crates/domain/src/endpoint/mod.rs @@ -57,6 +57,14 @@ impl Endpoint { self.waiting_receivers.pop() } + pub fn is_sender(&self, thread: ThreadId) -> bool { + self.waiting_senders.contains(thread) + } + + pub fn is_receiver(&self, thread: ThreadId) -> bool { + self.waiting_receivers.contains(thread) + } + pub fn has_waiting_sender(&self) -> bool { !self.waiting_senders.is_empty() } diff --git a/crates/domain/src/endpoint/tests.rs b/crates/domain/src/endpoint/tests.rs index 58e21ef..b2c99cf 100644 --- a/crates/domain/src/endpoint/tests.rs +++ b/crates/domain/src/endpoint/tests.rs @@ -19,21 +19,21 @@ fn test_endpoint() -> Endpoint { #[test] fn empty_queue_pop_returns_none() { - let mut queue = WaitQueue::new(); + let mut queue: WaitQueue = WaitQueue::new(); assert!(queue.pop().is_none()); assert!(queue.is_empty()); } #[test] fn push_then_pop_returns_same_thread() { - let mut queue = WaitQueue::new(); + let mut queue: WaitQueue = WaitQueue::new(); queue.push(thread(1)).unwrap(); assert_eq!(queue.pop(), Some(thread(1))); } #[test] fn pop_order_is_fifo() { - let mut queue = WaitQueue::new(); + let mut queue: WaitQueue = WaitQueue::new(); queue.push(thread(1)).unwrap(); queue.push(thread(2)).unwrap(); queue.push(thread(3)).unwrap(); @@ -46,7 +46,7 @@ fn pop_order_is_fifo() { #[test] fn full_queue_rejects_push() { - let mut queue = WaitQueue::new(); + let mut queue: WaitQueue = WaitQueue::new(); for i in 0..MAX_WAITERS { assert!(queue.push(thread(i as u16)).is_some()); } @@ -55,7 +55,7 @@ fn full_queue_rejects_push() { #[test] fn is_empty_after_drain() { - let mut queue = WaitQueue::new(); + let mut queue: WaitQueue = WaitQueue::new(); queue.push(thread(1)).unwrap(); queue.push(thread(2)).unwrap(); queue.pop(); @@ -219,3 +219,40 @@ fn table_destroy_reuses_slot() { let new_id = table.create(owner()).unwrap(); assert_eq!(id, new_id); } + +#[test] +fn wait_queue_is_fifo_even_after_a_pop_leaves_a_hole() { + let mut q: WaitQueue = WaitQueue::new(); + q.push(thread(1)).unwrap(); + q.push(thread(2)).unwrap(); + assert_eq!(q.pop(), Some(thread(1))); + q.push(thread(3)).unwrap(); + assert_eq!(q.pop(), Some(thread(2)), "thread 3 jumped the queue"); + assert_eq!(q.pop(), Some(thread(3))); +} + +#[test] +fn wait_queue_keeps_order_around_a_removed_thread() { + let mut q: WaitQueue = WaitQueue::new(); + for n in 1..=3 { + q.push(thread(n)).unwrap(); + } + assert!(q.remove_thread(thread(2))); + q.push(thread(4)).unwrap(); + assert_eq!(q.pop(), Some(thread(1))); + assert_eq!(q.pop(), Some(thread(3))); + assert_eq!(q.pop(), Some(thread(4))); + assert_eq!(q.pop(), None); +} + +#[test] +fn wait_queue_kani_trace_push_push_remove() { + let mut q: WaitQueue<3> = WaitQueue::new(); + q.push(thread(3)).unwrap(); + q.push(thread(2)).unwrap(); + assert!(q.remove_thread(thread(3))); + assert!(!q.remove_thread(thread(0))); + assert!(!q.remove_thread(thread(3))); + assert_eq!(q.pop(), Some(thread(2))); + assert_eq!(q.pop(), None); +} diff --git a/crates/domain/src/endpoint/wait_queue.rs b/crates/domain/src/endpoint/wait_queue.rs index 833b217..926eaee 100644 --- a/crates/domain/src/endpoint/wait_queue.rs +++ b/crates/domain/src/endpoint/wait_queue.rs @@ -2,51 +2,127 @@ use types::id::ThreadId; pub const MAX_WAITERS: usize = 16; -pub struct WaitQueue { - entries: [Option; MAX_WAITERS], - count: u8, +/// Bounded FIFO of blocked threads. Order is preserved across pops and +/// removals, so no waiter can be overtaken. +pub struct WaitQueue { + entries: [Option; N], + len: usize, } -impl WaitQueue { +impl WaitQueue { pub fn new() -> Self { Self { - entries: [None; MAX_WAITERS], - count: 0, + entries: [None; N], + len: 0, } } pub fn push(&mut self, thread: ThreadId) -> Option<()> { - let slot = self.entries.iter_mut().find(|s| s.is_none())?; - *slot = Some(thread); - self.count += 1; + if self.len == N || self.contains(thread) { + return None; + } + self.entries[self.len] = Some(thread); + self.len += 1; Some(()) } pub fn pop(&mut self) -> Option { - let slot = self.entries.iter_mut().find(|s| s.is_some())?; - let thread = slot.take(); - self.count -= 1; - thread + let head = self.entries[0]?; + self.close_gap(0); + Some(head) } pub fn remove_thread(&mut self, thread: ThreadId) -> bool { - for slot in &mut self.entries { - if *slot == Some(thread) { - *slot = None; - self.count -= 1; - return true; + match self.entries[..self.len] + .iter() + .position(|e| *e == Some(thread)) + { + Some(i) => { + self.close_gap(i); + true } + None => false, } - false + } + + pub fn contains(&self, thread: ThreadId) -> bool { + self.entries[..self.len].contains(&Some(thread)) } pub fn is_empty(&self) -> bool { - self.count == 0 + self.len == 0 + } + + fn close_gap(&mut self, i: usize) { + for j in i..self.len - 1 { + self.entries[j] = self.entries[j + 1]; + } + self.len -= 1; + self.entries[self.len] = None; } } -impl Default for WaitQueue { +impl Default for WaitQueue { fn default() -> Self { Self::new() } } + +#[cfg(kani)] +mod proofs { + use super::*; + + /// Against a simple reference model (a plain array kept in arrival + /// order), any sequence of pushes, pops and removals behaves the same: + /// FIFO order, no duplicates, nothing lost. + #[kani::proof] + #[kani::unwind(7)] + fn behaves_like_a_fifo_without_duplicates() { + const CAP: usize = 3; + let mut q: WaitQueue = WaitQueue::new(); + let mut model = [0u16; CAP]; + let mut len = 0usize; + + for _ in 0..5 { + let t: u16 = kani::any::() % 4; + match kani::any::() % 3 { + 0 => { + let ok = q.push(ThreadId::from_raw(t)).is_some(); + let dup = model[..len].contains(&t); + assert!(ok == (!dup && len < CAP)); + if ok { + model[len] = t; + len += 1; + } + } + 1 => { + let got = q.pop(); + if len == 0 { + assert!(got.is_none()); + } else { + assert!(got == Some(ThreadId::from_raw(model[0]))); + for j in 0..len - 1 { + model[j] = model[j + 1]; + } + len -= 1; + } + } + _ => { + let removed = q.remove_thread(ThreadId::from_raw(t)); + match model[..len].iter().position(|&m| m == t) { + Some(i) => { + assert!(removed); + for j in i..len - 1 { + model[j] = model[j + 1]; + } + len -= 1; + } + None => assert!(!removed), + } + } + } + assert!(q.len == len); + assert!(q.is_empty() == (len == 0)); + } + } +} diff --git a/crates/domain/src/error.rs b/crates/domain/src/error.rs index cebe4a9..0bf29ac 100644 --- a/crates/domain/src/error.rs +++ b/crates/domain/src/error.rs @@ -33,4 +33,9 @@ pub enum KernelError { InvalidRegion, /// The object a capability would refer to no longer exists. NoSuchObject, + /// The capability names the wrong kind of object for this operation. + WrongObject, + /// `Reply` with an empty reply slot, or a call whose reply was dropped. + NoReply, + InvalidArgument, } diff --git a/crates/domain/src/irq/mod.rs b/crates/domain/src/irq/mod.rs index 5765aa5..d857e8d 100644 --- a/crates/domain/src/irq/mod.rs +++ b/crates/domain/src/irq/mod.rs @@ -1,6 +1,8 @@ -use types::id::{IrqId, ThreadId}; - -use crate::error::KernelError; +use types::{ + badge::Badge, + generation::Generation, + id::{IrqId, NotificationId}, +}; #[cfg(test)] #[path = "tests.rs"] @@ -8,47 +10,47 @@ mod tests; const MAX_IRQS: usize = 256; +/// Where an interrupt is delivered: a signal on a notification, through a +/// capability snapshot (generation and badge) taken when the route was set. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IrqRoute { + pub notification: NotificationId, + pub generation: Generation, + pub badge: Badge, +} + pub struct IrqRoutingTable { - owners: [Option; MAX_IRQS], + routes: [Option; MAX_IRQS], } impl IrqRoutingTable { pub fn new() -> Self { Self { - owners: [None; MAX_IRQS], + routes: [None; MAX_IRQS], } } - pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> { - let slot = self - .owners - .get_mut(irq.raw() as usize) - .ok_or(KernelError::InvalidIrq(irq))?; - if slot.is_some() { - return Err(KernelError::IrqAlreadyOwned(irq)); + /// Routes `irq`, replacing any previous route. Fails for lines beyond + /// the table. + pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool { + match self.routes.get_mut(irq.raw() as usize) { + Some(slot) => { + *slot = Some(route); + true + } + None => false, } - *slot = Some(thread); - Ok(()) } - pub fn unregister(&mut self, irq: IrqId) { - if let Some(slot) = self.owners.get_mut(irq.raw() as usize) { + pub fn clear(&mut self, irq: IrqId) { + if let Some(slot) = self.routes.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 { - *self.owners.get(irq.raw() as usize)? + /// The route for `irq`; `None` for unknown or unrouted lines. + pub fn route(&self, irq: IrqId) -> Option { + *self.routes.get(irq.raw() as usize)? } } diff --git a/crates/domain/src/irq/tests.rs b/crates/domain/src/irq/tests.rs index 2954b97..554a720 100644 --- a/crates/domain/src/irq/tests.rs +++ b/crates/domain/src/irq/tests.rs @@ -1,50 +1,43 @@ use super::*; -use types::id::{IrqId, ThreadId}; +use types::badge::Badge; +use types::generation::Generation; +use types::id::{IrqId, NotificationId}; -fn irq(id: u16) -> IrqId { - IrqId::from_raw(id) -} - -fn thread(id: u16) -> ThreadId { - ThreadId::from_raw(id) +fn route(n: u16) -> IrqRoute { + IrqRoute { + notification: NotificationId::from_raw(n), + generation: Generation::initial(), + badge: Badge::new(1 << n), + } } #[test] -fn new_table_has_no_owners() { - let table = IrqRoutingTable::new(); - assert!(table.owner(irq(0)).is_none()); - assert!(table.owner(irq(10)).is_none()); +fn new_table_routes_nothing() { + let t = IrqRoutingTable::new(); + assert_eq!(t.route(IrqId::from_raw(10)), None); } #[test] -fn register_and_lookup() { - let mut table = IrqRoutingTable::new(); - table.register(irq(5), thread(1)).unwrap(); - assert_eq!(table.owner(irq(5)), Some(thread(1))); +fn set_then_route() { + let mut t = IrqRoutingTable::new(); + assert!(t.set(IrqId::from_raw(10), route(3))); + assert_eq!(t.route(IrqId::from_raw(10)), Some(route(3))); } #[test] -fn register_already_owned_fails() { - let mut table = IrqRoutingTable::new(); - table.register(irq(5), thread(1)).unwrap(); - assert!(table.register(irq(5), thread(2)).is_err()); +fn set_replaces_and_clear_removes() { + let mut t = IrqRoutingTable::new(); + t.set(IrqId::from_raw(10), route(3)); + t.set(IrqId::from_raw(10), route(4)); + assert_eq!(t.route(IrqId::from_raw(10)), Some(route(4))); + t.clear(IrqId::from_raw(10)); + assert_eq!(t.route(IrqId::from_raw(10)), None); } #[test] -fn unregister_then_reregister() { - let mut table = IrqRoutingTable::new(); - table.register(irq(5), thread(1)).unwrap(); - table.unregister(irq(5)); - assert!(table.owner(irq(5)).is_none()); - table.register(irq(5), thread(2)).unwrap(); - assert_eq!(table.owner(irq(5)), Some(thread(2))); -} - -#[test] -fn different_irqs_independent() { - let mut table = IrqRoutingTable::new(); - table.register(irq(0), thread(10)).unwrap(); - table.register(irq(1), thread(20)).unwrap(); - assert_eq!(table.owner(irq(0)), Some(thread(10))); - assert_eq!(table.owner(irq(1)), Some(thread(20))); +fn lines_beyond_the_table_are_rejected_without_panicking() { + let mut t = IrqRoutingTable::new(); + assert!(!t.set(IrqId::from_raw(1000), route(1))); + assert_eq!(t.route(IrqId::from_raw(1000)), None); + t.clear(IrqId::from_raw(1000)); } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index fbb30f4..4e50784 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -11,6 +11,7 @@ pub mod generations; pub mod irq; pub mod memory; pub mod message; +pub mod notification; pub mod process; pub mod resource; pub mod scheduler; diff --git a/crates/domain/src/message/mod.rs b/crates/domain/src/message/mod.rs index 3bf5fa7..21988c1 100644 --- a/crates/domain/src/message/mod.rs +++ b/crates/domain/src/message/mod.rs @@ -5,26 +5,26 @@ use types::bounded::CapSlot; mod tests; pub const MAX_MESSAGE_WORDS: usize = 4; -pub const MAX_CAP_TRANSFERS: usize = 4; +/// Four words and at most one capability (named by the sender's slot). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Message { words: [usize; MAX_MESSAGE_WORDS], - caps: [Option; MAX_CAP_TRANSFERS], + cap: Option, } impl Message { pub fn empty() -> Self { - Self { - words: [0; MAX_MESSAGE_WORDS], - caps: [None; MAX_CAP_TRANSFERS], - } + Self::from_words([0; MAX_MESSAGE_WORDS]) } pub fn from_words(words: [usize; MAX_MESSAGE_WORDS]) -> Self { - Self { - words, - caps: [None; MAX_CAP_TRANSFERS], - } + Self { words, cap: None } + } + + pub fn with_cap(mut self, slot: CapSlot) -> Self { + self.cap = Some(slot); + self } pub fn words(&self) -> &[usize; MAX_MESSAGE_WORDS] { @@ -35,42 +35,7 @@ impl Message { self.words[index] } - pub fn caps(&self) -> &[Option; MAX_CAP_TRANSFERS] { - &self.caps - } - - pub fn cap(&self, index: usize) -> Option { - self.caps[index] - } - - pub fn builder() -> MessageBuilder { - MessageBuilder { - message: Message::empty(), - } - } -} - -pub struct MessageBuilder { - message: Message, -} - -impl MessageBuilder { - pub fn word(mut self, index: usize, value: usize) -> Self { - self.message.words[index] = value; - self - } - - pub fn words(mut self, words: [usize; MAX_MESSAGE_WORDS]) -> Self { - self.message.words = words; - self - } - - pub fn cap(mut self, index: usize, slot: CapSlot) -> Self { - self.message.caps[index] = Some(slot); - self - } - - pub fn build(self) -> Message { - self.message + pub fn cap(&self) -> Option { + self.cap } } diff --git a/crates/domain/src/message/tests.rs b/crates/domain/src/message/tests.rs index 188106d..014836c 100644 --- a/crates/domain/src/message/tests.rs +++ b/crates/domain/src/message/tests.rs @@ -1,78 +1,23 @@ use super::*; use types::bounded::CapSlot; -fn slot(n: u8) -> CapSlot { - CapSlot::new(n).unwrap() -} - -// --- Construction --- - #[test] -fn empty_message_has_zeroed_words_and_no_caps() { +fn empty_message_has_zeroed_words_and_no_cap() { let msg = Message::empty(); assert_eq!(msg.words(), &[0; MAX_MESSAGE_WORDS]); - for i in 0..MAX_CAP_TRANSFERS { - assert!(msg.cap(i).is_none()); - } + assert!(msg.cap().is_none()); } #[test] -fn from_words_sets_words_and_no_caps() { +fn from_words_keeps_every_word() { let msg = Message::from_words([10, 20, 30, 40]); - assert_eq!(msg.word(0), 10); - assert_eq!(msg.word(1), 20); - assert_eq!(msg.word(2), 30); - assert_eq!(msg.word(3), 40); - assert!(msg.cap(0).is_none()); -} - -// --- Accessors --- - -#[test] -fn words_returns_full_array() { - let msg = Message::from_words([1, 2, 3, 4]); - assert_eq!(msg.words(), &[1, 2, 3, 4]); + assert_eq!(msg.words(), &[10, 20, 30, 40]); + assert!(msg.cap().is_none()); } #[test] -fn caps_returns_full_array() { - let msg = Message::empty(); - assert_eq!(msg.caps(), &[None; MAX_CAP_TRANSFERS]); -} - -// --- Builder --- - -#[test] -fn builder_sets_individual_words() { - let msg = Message::builder().word(0, 42).word(2, 99).build(); - assert_eq!(msg.word(0), 42); - assert_eq!(msg.word(1), 0); - assert_eq!(msg.word(2), 99); - assert_eq!(msg.word(3), 0); -} - -#[test] -fn builder_sets_caps() { - let msg = Message::builder().cap(0, slot(5)).cap(1, slot(10)).build(); - assert_eq!(msg.cap(0), Some(slot(5))); - assert_eq!(msg.cap(1), Some(slot(10))); - assert!(msg.cap(2).is_none()); -} - -#[test] -fn builder_sets_words_and_caps_together() { - let msg = Message::builder() - .word(0, 100) - .word(1, 200) - .cap(0, slot(3)) - .build(); - assert_eq!(msg.word(0), 100); - assert_eq!(msg.word(1), 200); - assert_eq!(msg.cap(0), Some(slot(3))); -} - -#[test] -fn builder_with_all_words() { - let msg = Message::builder().words([11, 22, 33, 44]).build(); - assert_eq!(msg.words(), &[11, 22, 33, 44]); +fn with_cap_attaches_one_capability() { + let msg = Message::from_words([1, 2, 3, 4]).with_cap(CapSlot::new(5).unwrap()); + assert_eq!(msg.cap(), CapSlot::new(5)); + assert_eq!(msg.word(3), 4); } diff --git a/crates/domain/src/notification/mod.rs b/crates/domain/src/notification/mod.rs new file mode 100644 index 0000000..e76c52d --- /dev/null +++ b/crates/domain/src/notification/mod.rs @@ -0,0 +1,186 @@ +//! Asynchronous signals: a word of bits, a FIFO of waiters and at most one +//! bound thread. See docs/ipc.md. + +use types::{ + badge::Badge, + generation::Generation, + id::{NotificationId, ProcessId, ThreadId}, +}; + +use crate::{endpoint::wait_queue::WaitQueue, generations::SlotGenerations}; + +#[cfg(test)] +#[path = "tests.rs"] +mod tests; + +#[cfg(kani)] +#[path = "proofs.rs"] +mod proofs; + +pub const MAX_NOTIFICATIONS: usize = 128; + +/// What the kernel must do after a signal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalEffect { + /// Wake this blocked `Wait`er with these bits. + WakeWaiter(ThreadId, usize), + /// Wake the bound thread out of its `Receive` with these bits. + WakeBound(ThreadId, usize), + /// Nobody to wake; the bits were stored. + Stored, +} + +pub struct Notification { + id: NotificationId, + owner: ProcessId, + bits: usize, + waiters: WaitQueue, + bound: Option, +} + +impl Notification { + pub fn new(id: NotificationId, owner: ProcessId) -> Self { + Self { + id, + owner, + bits: 0, + waiters: WaitQueue::new(), + bound: None, + } + } + + pub fn id(&self) -> NotificationId { + self.id + } + + pub fn owner(&self) -> ProcessId { + self.owner + } + + pub fn bound(&self) -> Option { + self.bound + } + + /// Delivers a signal. `bound_is_receiving` says whether the bound + /// thread is currently blocked in `Receive` and can be woken. + pub fn signal(&mut self, badge: Badge, bound_is_receiving: bool) -> SignalEffect { + let bits = if badge.is_none() { 1 } else { badge.raw() }; + if let Some(waiter) = self.waiters.pop() { + return SignalEffect::WakeWaiter(waiter, bits); + } + if let (Some(bound), true) = (self.bound, bound_is_receiving) { + return SignalEffect::WakeBound(bound, bits); + } + self.bits |= bits; + SignalEffect::Stored + } + + /// Returns and clears the pending bits. + pub fn take_bits(&mut self) -> usize { + core::mem::take(&mut self.bits) + } + + pub fn has_bits(&self) -> bool { + self.bits != 0 + } + + pub fn is_waiter(&self, thread: ThreadId) -> bool { + self.waiters.contains(thread) + } + + pub fn add_waiter(&mut self, thread: ThreadId) -> bool { + self.waiters.push(thread).is_some() + } + + pub fn bind(&mut self, thread: ThreadId) -> bool { + if self.bound.is_some() { + return false; + } + self.bound = Some(thread); + true + } + + pub fn unbind(&mut self, thread: ThreadId) { + if self.bound == Some(thread) { + self.bound = None; + } + } + + /// Forgets `thread` as waiter and as bound thread. + pub fn remove_thread(&mut self, thread: ThreadId) { + self.waiters.remove_thread(thread); + self.unbind(thread); + } + + /// Drains every waiter, e.g. when the notification is destroyed. + pub fn pop_waiter(&mut self) -> Option { + self.waiters.pop() + } +} + +pub struct NotificationTable { + slots: [Option; MAX_NOTIFICATIONS], + generations: SlotGenerations, +} + +impl NotificationTable { + pub fn new() -> Self { + Self { + slots: [const { None }; MAX_NOTIFICATIONS], + generations: SlotGenerations::new(), + } + } + + pub fn create(&mut self, owner: ProcessId) -> Option { + let idx = (0..MAX_NOTIFICATIONS) + .find(|&i| self.slots[i].is_none() && self.generations.is_usable(i))?; + let id = NotificationId::from_raw(idx as u16); + self.slots[idx] = Some(Notification::new(id, owner)); + Some(id) + } + + pub fn get(&self, id: NotificationId) -> Option<&Notification> { + self.slots.get(id.raw() as usize)?.as_ref() + } + + pub fn get_mut(&mut self, id: NotificationId) -> Option<&mut Notification> { + self.slots.get_mut(id.raw() as usize)?.as_mut() + } + + pub fn generation(&self, id: NotificationId) -> Option { + self.get(id)?; + self.generations.current(id.raw() as usize) + } + + pub fn revoke(&mut self, id: NotificationId) { + if self.get(id).is_some() { + self.generations.advance(id.raw() as usize); + } + } + + pub fn destroy(&mut self, id: NotificationId) -> Option { + let n = self.slots.get_mut(id.raw() as usize)?.take()?; + self.generations.advance(id.raw() as usize); + Some(n) + } + + pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator + '_ { + self.slots + .iter() + .flatten() + .filter(move |n| n.owner == owner) + .map(|n| n.id) + } + + pub fn remove_thread_from_all(&mut self, thread: ThreadId) { + for n in self.slots.iter_mut().flatten() { + n.remove_thread(thread); + } + } +} + +impl Default for NotificationTable { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/domain/src/notification/proofs.rs b/crates/domain/src/notification/proofs.rs new file mode 100644 index 0000000..2d26b48 --- /dev/null +++ b/crates/domain/src/notification/proofs.rs @@ -0,0 +1,43 @@ +//! Signals are never lost: every signal either wakes exactly one thread +//! with its bits, or leaves those bits set for the next `Wait`. + +use super::*; +use types::badge::Badge; +use types::id::{NotificationId, ProcessId, ThreadId}; + +#[kani::proof] +#[kani::unwind(5)] +fn a_signal_is_delivered_or_stored() { + let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0)); + n.bits = kani::any(); + if kani::any() { + n.add_waiter(ThreadId::from_raw(kani::any::() % 4)); + } + if kani::any() { + n.bind(ThreadId::from_raw(kani::any::() % 4)); + } + let before = n.bits; + let badge = Badge::new(kani::any()); + let expected = if badge.is_none() { 1 } else { badge.raw() }; + + match n.signal(badge, kani::any()) { + SignalEffect::WakeWaiter(_, bits) | SignalEffect::WakeBound(_, bits) => { + assert!(bits == expected); + assert!(n.bits == before, "woken signals are not also stored"); + } + SignalEffect::Stored => { + assert!(n.bits == before | expected); + assert!(n.bits & expected == expected); + } + } +} + +#[kani::proof] +#[kani::unwind(5)] +fn take_bits_returns_everything_and_clears() { + let mut n = Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0)); + let bits: usize = kani::any(); + n.bits = bits; + assert!(n.take_bits() == bits); + assert!(n.bits == 0); +} diff --git a/crates/domain/src/notification/tests.rs b/crates/domain/src/notification/tests.rs new file mode 100644 index 0000000..fa8e82d --- /dev/null +++ b/crates/domain/src/notification/tests.rs @@ -0,0 +1,116 @@ +use super::*; +use types::badge::Badge; +use types::id::{NotificationId, ProcessId, ThreadId}; + +fn ntfn() -> Notification { + Notification::new(NotificationId::from_raw(0), ProcessId::from_raw(0)) +} + +fn t(n: u16) -> ThreadId { + ThreadId::from_raw(n) +} + +#[test] +fn signal_with_nobody_waiting_stores_the_badge() { + let mut n = ntfn(); + assert_eq!(n.signal(Badge::new(0b100), false), SignalEffect::Stored); + assert_eq!(n.signal(Badge::new(0b001), false), SignalEffect::Stored); + assert_eq!(n.take_bits(), 0b101); + assert_eq!(n.take_bits(), 0, "taking clears"); +} + +#[test] +fn unbadged_signal_sets_bit_zero() { + let mut n = ntfn(); + n.signal(Badge::NONE, false); + assert_eq!(n.take_bits(), 1); +} + +#[test] +fn signal_wakes_the_first_waiter_with_its_badge() { + let mut n = ntfn(); + assert!(n.add_waiter(t(1))); + assert!(n.add_waiter(t(2))); + assert_eq!( + n.signal(Badge::new(8), false), + SignalEffect::WakeWaiter(t(1), 8) + ); + assert_eq!(n.take_bits(), 0, "delivered bits are not also stored"); + assert_eq!( + n.signal(Badge::new(4), false), + SignalEffect::WakeWaiter(t(2), 4) + ); +} + +#[test] +fn signal_wakes_a_receiving_bound_thread_when_no_waiter() { + let mut n = ntfn(); + assert!(n.bind(t(5))); + assert_eq!( + n.signal(Badge::new(2), true), + SignalEffect::WakeBound(t(5), 2) + ); + assert_eq!(n.signal(Badge::new(2), false), SignalEffect::Stored); +} + +#[test] +fn waiters_take_priority_over_the_bound_thread() { + let mut n = ntfn(); + n.bind(t(5)); + n.add_waiter(t(1)); + assert_eq!( + n.signal(Badge::new(2), true), + SignalEffect::WakeWaiter(t(1), 2) + ); +} + +#[test] +fn only_one_thread_can_be_bound() { + let mut n = ntfn(); + assert!(n.bind(t(1))); + assert!(!n.bind(t(2))); + n.unbind(t(1)); + assert!(n.bind(t(2))); +} + +#[test] +fn removed_waiter_is_not_woken() { + let mut n = ntfn(); + n.add_waiter(t(1)); + n.remove_thread(t(1)); + assert_eq!(n.signal(Badge::new(1), false), SignalEffect::Stored); +} + +#[test] +fn remove_thread_also_unbinds() { + let mut n = ntfn(); + n.bind(t(1)); + n.remove_thread(t(1)); + assert_eq!(n.bound(), None); +} + +#[test] +fn table_generations_follow_slot_reuse() { + let mut table = NotificationTable::new(); + let owner = ProcessId::from_raw(3); + let id = table.create(owner).unwrap(); + let g = table.generation(id).unwrap(); + assert!(table.destroy(id).is_some()); + assert_eq!(table.generation(id), None); + let again = table.create(owner).unwrap(); + assert_eq!(again, id); + assert!(!g.is_valid_against(&table.generation(again).unwrap())); +} + +#[test] +fn table_lists_notifications_by_owner() { + let mut table = NotificationTable::new(); + let a = table.create(ProcessId::from_raw(1)).unwrap(); + table.create(ProcessId::from_raw(2)).unwrap(); + let mut n = 0; + for id in table.ids_owned_by(ProcessId::from_raw(1)) { + assert_eq!(id, a); + n += 1; + } + assert_eq!(n, 1); +} diff --git a/crates/domain/src/proofs/thread.rs b/crates/domain/src/proofs/thread.rs index 75bf400..04cbc19 100644 --- a/crates/domain/src/proofs/thread.rs +++ b/crates/domain/src/proofs/thread.rs @@ -22,7 +22,7 @@ fn dead_thread_rejects_all_transitions() { assert!(matches!(thread.state(), ThreadState::Dead)); assert!(thread.set_running(CoreId::from_raw(0)).is_err()); - assert!(thread.block(BlockReason::WaitingAny).is_err()); + assert!(thread.block(BlockReason::AwaitingReply).is_err()); assert!(thread.unblock().is_err()); assert!(thread.preempt().is_err()); assert!(thread.exit().is_err()); @@ -36,7 +36,7 @@ fn only_running_thread_can_block() { let mut thread = test_thread(Priority::new(prio_val).unwrap()); assert!(matches!(thread.state(), ThreadState::Ready)); - assert!(thread.block(BlockReason::WaitingAny).is_err()); + assert!(thread.block(BlockReason::AwaitingReply).is_err()); } #[kani::proof] @@ -44,7 +44,7 @@ fn blocked_thread_cannot_run_directly() { let mut thread = test_thread(Priority::new(16).unwrap()); thread.set_running(CoreId::from_raw(0)).unwrap(); - thread.block(BlockReason::WaitingAny).unwrap(); + thread.block(BlockReason::AwaitingReply).unwrap(); assert!(thread.set_running(CoreId::from_raw(0)).is_err()); } diff --git a/crates/domain/src/resource.rs b/crates/domain/src/resource.rs index 394c5df..01d89df 100644 --- a/crates/domain/src/resource.rs +++ b/crates/domain/src/resource.rs @@ -1,9 +1,10 @@ -use types::id::{EndpointId, IrqId, ProcessId, RegionId}; +use types::id::{EndpointId, IrqId, NotificationId, ProcessId, RegionId}; #[derive(Clone)] pub enum Resource { Memory(RegionId), Endpoint(EndpointId), + Notification(NotificationId), Process(ProcessId), Interrupt(IrqId), ProcessCreate, diff --git a/crates/domain/src/scheduler/multi_core.rs b/crates/domain/src/scheduler/multi_core.rs index eeadf0c..9052d63 100644 --- a/crates/domain/src/scheduler/multi_core.rs +++ b/crates/domain/src/scheduler/multi_core.rs @@ -30,6 +30,13 @@ impl MultiCoreScheduler { } } + /// Whether `thread` waits in some core's run queue. + pub fn is_queued(&self, thread: ThreadId) -> bool { + self.cores[..self.num_cores as usize] + .iter() + .any(|c| c.is_queued(thread)) + } + pub fn current(&self, core: CoreId) -> Option { self.cores[core.raw() as usize].current() } diff --git a/crates/domain/src/scheduler/per_core.rs b/crates/domain/src/scheduler/per_core.rs index f93bde3..f5ebcdd 100644 --- a/crates/domain/src/scheduler/per_core.rs +++ b/crates/domain/src/scheduler/per_core.rs @@ -29,6 +29,10 @@ impl PerCoreScheduler { self.queue.add(thread, priority); } + pub fn is_queued(&self, thread: ThreadId) -> bool { + self.queue.contains(thread) + } + pub fn remove(&mut self, thread: ThreadId) { self.queue.remove(thread); } diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index da1066f..7dbd770 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -1,42 +1,12 @@ use types::{ address::{DataRef, EntryPoint, StackPointer, VirtualAddress}, + badge::Badge, bounded::{CapSlot, PageCount}, permission::PermissionSet, }; use crate::message::Message; -pub struct WaitSet { - endpoints: [Option; 4], - interrupts: [Option; 4], -} - -impl WaitSet { - pub fn new(endpoints: [Option; 4], interrupts: [Option; 4]) -> Self { - Self { - endpoints, - interrupts, - } - } - - pub fn endpoints(&self) -> &[Option; 4] { - &self.endpoints - } - - pub fn interrupts(&self) -> &[Option; 4] { - &self.interrupts - } - - pub fn is_empty(&self) -> bool { - self.endpoints.iter().all(|s| s.is_none()) && self.interrupts.iter().all(|s| s.is_none()) - } -} - -pub enum WakeReason { - MessageReady(CapSlot), - InterruptFired(CapSlot), -} - pub enum Syscall { Send { endpoint: CapSlot, @@ -50,15 +20,47 @@ pub enum Syscall { to_endpoint: CapSlot, permissions: PermissionSet, }, - WaitForInterrupt { + /// Send, then block for the reply (docs/ipc.md). + Call { + endpoint: CapSlot, + message: Message, + }, + /// Answer the thread in the caller's reply slot. + Reply { + message: Message, + }, + /// Reply (if a reply is pending), then receive. + ReplyRecv { + endpoint: CapSlot, + message: Message, + }, + /// Copy an unbadged endpoint/notification cap with a badge and + /// narrower permissions into the caller's own table. + Mint { + slot: CapSlot, + badge: Badge, + permissions: PermissionSet, + }, + CreateNotification, + Signal { + notification: CapSlot, + }, + Wait { + notification: CapSlot, + }, + Poll { + notification: CapSlot, + }, + BindNotification { + notification: CapSlot, + }, + SetIrqNotification { irq: CapSlot, + notification: CapSlot, }, AcknowledgeInterrupt { irq: CapSlot, }, - WaitAny { - sources: WaitSet, - }, CreateProcess { create_cap: CapSlot, image: DataRef, diff --git a/crates/domain/src/thread/mod.rs b/crates/domain/src/thread/mod.rs index b61c497..d8ec382 100644 --- a/crates/domain/src/thread/mod.rs +++ b/crates/domain/src/thread/mod.rs @@ -1,10 +1,11 @@ use types::{ - bounded::{CapSlot, Priority}, - id::{CoreId, ProcessId, ThreadId}, + bounded::Priority, + generation::Generation, + id::{CoreId, EndpointId, NotificationId, ProcessId, ThreadId}, time::TimeSlice, }; -use crate::error::KernelError; +use crate::{error::KernelError, generations::SlotGenerations}; #[cfg(test)] #[path = "tests.rs"] @@ -80,10 +81,16 @@ impl SavedContext { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BlockReason { - Sending(CapSlot), - Receiving(CapSlot), - WaitingForInterrupt(CapSlot), - WaitingAny, + /// Queued as a sender on the endpoint. + Sending(EndpointId), + /// Queued as a `Call`er on the endpoint; will then await the reply. + Calling(EndpointId), + /// Queued as a receiver on the endpoint. + Receiving(EndpointId), + /// Delivered a `Call`; waiting for the server's reply. + AwaitingReply, + /// Blocked in `Wait` on the notification. + WaitingNotification(NotificationId), } impl Thread { @@ -180,6 +187,17 @@ impl Thread { } } + /// Keeps a blocked thread blocked, waiting for something else. + pub fn rewait(&mut self, reason: BlockReason) -> Result<(), KernelError> { + match self.state { + ThreadState::Blocked(_) => { + self.state = ThreadState::Blocked(reason); + Ok(()) + } + _ => Err(KernelError::InvalidStateTransition(self.id)), + } + } + pub fn unblock(&mut self) -> Result<(), KernelError> { match self.state { ThreadState::Blocked(_) => { @@ -215,15 +233,28 @@ pub const MAX_THREADS: usize = 256; pub struct ThreadTable { threads: [Option; MAX_THREADS], + generations: SlotGenerations, } impl ThreadTable { pub fn new() -> Self { Self { threads: [const { None }; MAX_THREADS], + generations: SlotGenerations::new(), } } + /// Current generation of a live thread, so references to it (reply + /// slots) can detect that the id was reused. + pub fn generation(&self, id: ThreadId) -> Option { + self.get(id).ok()?; + self.generations.current(id.raw() as usize) + } + + fn free_slot(&self) -> Option { + (0..MAX_THREADS).find(|&i| self.threads[i].is_none() && self.generations.is_usable(i)) + } + pub fn spawn( &mut self, process: ProcessId, @@ -231,11 +262,7 @@ impl ThreadTable { time_slice: TimeSlice, core_affinity: Option, ) -> Result { - let idx = self - .threads - .iter() - .position(|s| s.is_none()) - .ok_or(KernelError::TooManyThreads)?; + let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?; let id = ThreadId::from_raw(idx as u16); self.threads[idx] = Some(Thread::new( @@ -256,11 +283,7 @@ impl ThreadTable { core_affinity: Option, context: SavedContext, ) -> Result { - let idx = self - .threads - .iter() - .position(|s| s.is_none()) - .ok_or(KernelError::TooManyThreads)?; + let idx = self.free_slot().ok_or(KernelError::TooManyThreads)?; let id = ThreadId::from_raw(idx as u16); self.threads[idx] = Some(Thread::with_context( @@ -292,10 +315,13 @@ impl ThreadTable { pub fn destroy(&mut self, id: ThreadId) -> Result { let idx = id.raw() as usize; - self.threads + let thread = self + .threads .get_mut(idx) .and_then(|s| s.take()) - .ok_or(KernelError::InvalidThread(id)) + .ok_or(KernelError::InvalidThread(id))?; + self.generations.advance(idx); + Ok(thread) } pub fn threads_for_process(&self, process: ProcessId) -> impl Iterator { diff --git a/crates/domain/src/thread/tests.rs b/crates/domain/src/thread/tests.rs index cae2363..1468ce8 100644 --- a/crates/domain/src/thread/tests.rs +++ b/crates/domain/src/thread/tests.rs @@ -1,6 +1,6 @@ use super::*; use types::bounded::Priority; -use types::id::{CoreId, ProcessId, ThreadId}; +use types::id::{CoreId, EndpointId, ProcessId, ThreadId}; use types::time::TimeSlice; fn thread(id: u16) -> ThreadId { @@ -65,10 +65,10 @@ fn ready_to_running() { fn running_to_blocked() { let mut t = test_thread(0, 0); t.set_running(core(0)).unwrap(); - t.block(BlockReason::WaitingAny).unwrap(); + t.block(BlockReason::AwaitingReply).unwrap(); assert!(matches!( t.state(), - ThreadState::Blocked(BlockReason::WaitingAny) + ThreadState::Blocked(BlockReason::AwaitingReply) )); } @@ -76,7 +76,7 @@ fn running_to_blocked() { fn blocked_to_ready() { let mut t = test_thread(0, 0); t.set_running(core(0)).unwrap(); - t.block(BlockReason::WaitingAny).unwrap(); + t.block(BlockReason::AwaitingReply).unwrap(); t.unblock().unwrap(); assert!(matches!(t.state(), ThreadState::Ready)); } @@ -105,7 +105,7 @@ fn dead_cannot_transition() { t.set_running(core(0)).unwrap(); t.exit().unwrap(); assert!(t.set_running(core(0)).is_err()); - assert!(t.block(BlockReason::WaitingAny).is_err()); + assert!(t.block(BlockReason::AwaitingReply).is_err()); assert!(t.unblock().is_err()); assert!(t.preempt().is_err()); assert!(t.exit().is_err()); @@ -114,14 +114,14 @@ fn dead_cannot_transition() { #[test] fn ready_cannot_block() { let mut t = test_thread(0, 0); - assert!(t.block(BlockReason::WaitingAny).is_err()); + assert!(t.block(BlockReason::AwaitingReply).is_err()); } #[test] fn blocked_cannot_run_directly() { let mut t = test_thread(0, 0); t.set_running(core(0)).unwrap(); - t.block(BlockReason::WaitingAny).unwrap(); + t.block(BlockReason::AwaitingReply).unwrap(); assert!(t.set_running(core(0)).is_err()); } @@ -141,7 +141,7 @@ fn ready_cannot_exit() { fn blocked_cannot_preempt() { let mut t = test_thread(0, 0); t.set_running(core(0)).unwrap(); - t.block(BlockReason::WaitingAny).unwrap(); + t.block(BlockReason::AwaitingReply).unwrap(); assert!(t.preempt().is_err()); } @@ -237,3 +237,32 @@ fn table_destroy_reuses_slot() { .unwrap(); assert_eq!(id, new_id); } + +#[test] +fn a_blocked_thread_can_change_what_it_waits_for() { + let mut t = Thread::new( + ThreadId::from_raw(1), + ProcessId::from_raw(0), + Priority::DEFAULT, + TimeSlice::DEFAULT, + None, + ); + t.set_running(CoreId::from_raw(0)).unwrap(); + t.block(BlockReason::Calling(EndpointId::from_raw(0))) + .unwrap(); + t.rewait(BlockReason::AwaitingReply).unwrap(); + assert_eq!(*t.state(), ThreadState::Blocked(BlockReason::AwaitingReply)); +} + +#[test] +fn only_blocked_threads_can_rewait() { + let mut t = Thread::new( + ThreadId::from_raw(1), + ProcessId::from_raw(0), + Priority::DEFAULT, + TimeSlice::DEFAULT, + None, + ); + assert!(t.rewait(BlockReason::AwaitingReply).is_err()); + assert_eq!(*t.state(), ThreadState::Ready); +} diff --git a/crates/kernel/src/interrupt.rs b/crates/kernel/src/interrupt.rs index 03ecebf..da83e24 100644 --- a/crates/kernel/src/interrupt.rs +++ b/crates/kernel/src/interrupt.rs @@ -1,6 +1,6 @@ use domain::scheduler::{Scheduler, SchedulerDecision}; use platform::Platform; -use types::id::{CoreId, IrqId, ThreadId}; +use types::id::CoreId; use crate::Kernel; @@ -53,22 +53,4 @@ impl Kernel

{ self.scheduler.tick(core, current_tid) } } - - pub fn handle_interrupt(&mut self, irq: IrqId) -> Option { - self.platform.mask_irq(irq); - - let tid = self.irqs.owner(irq)?; - let thread = self.threads.get_mut(tid).ok()?; - - if !matches!(thread.state(), domain::thread::ThreadState::Blocked(_)) { - return None; - } - - let priority = thread.priority(); - let core = self.platform.core_id(); - let _ = thread.unblock(); - self.scheduler.add(tid, priority, core); - - Some(tid) - } } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index e262262..45a46b0 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -18,11 +18,10 @@ use domain::{ error::KernelError, irq::IrqRoutingTable, memory::{RegionTable, frame::FrameTable, quota::Quota}, - message::Message, + notification::{MAX_NOTIFICATIONS, NotificationTable}, process::{MAX_PROCESSES, ProcessTable}, resource::Resource, scheduler::{Scheduler, multi_core::MultiCoreScheduler}, - syscall::WaitSet, thread::{MAX_THREADS, ThreadState, ThreadTable}, }; use platform::Platform; @@ -45,9 +44,9 @@ pub struct Kernel { quotas: [Quota; MAX_PROCESSES], regions: RegionTable, irqs: IrqRoutingTable, - pending_messages: [Option; MAX_THREADS], + notifications: NotificationTable, + ipc: [syscall::IpcState; MAX_THREADS], pending_results: [Option; MAX_THREADS], - wait_sets: [Option; MAX_THREADS], } impl Kernel

{ @@ -63,9 +62,9 @@ impl Kernel

{ quotas: [Quota::default(); MAX_PROCESSES], regions: RegionTable::new(), irqs: IrqRoutingTable::new(), - pending_messages: [const { None }; MAX_THREADS], + notifications: NotificationTable::new(), + ipc: [syscall::IpcState::default(); MAX_THREADS], pending_results: [const { None }; MAX_THREADS], - wait_sets: [const { None }; MAX_THREADS], } } @@ -95,9 +94,9 @@ impl Kernel

{ addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]); addr_of_mut!((*slot).regions).write(RegionTable::new()); addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new()); - addr_of_mut!((*slot).pending_messages).write([const { None }; MAX_THREADS]); + addr_of_mut!((*slot).notifications).write(NotificationTable::new()); + addr_of_mut!((*slot).ipc).write([syscall::IpcState::default(); MAX_THREADS]); addr_of_mut!((*slot).pending_results).write([const { None }; MAX_THREADS]); - addr_of_mut!((*slot).wait_sets).write([const { None }; MAX_THREADS]); } } @@ -280,6 +279,7 @@ impl Kernel

{ self.cleanup_threads(&thread_ids); self.destroy_endpoints_owned_by(pid); + self.destroy_notifications_owned_by(pid); self.teardown_memory(pid); let _ = self.processes.destroy(pid); @@ -289,10 +289,12 @@ impl Kernel

{ use domain::scheduler::Scheduler; 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.notifications.remove_thread_from_all(*tid); + let ipc = core::mem::take(&mut self.ipc[tid.raw() as usize]); + if let Some(caller) = ipc.reply_to { + self.release_caller(caller); + } self.scheduler.remove(*tid); - self.pending_messages[tid.raw() as usize] = None; self.pending_results[tid.raw() as usize] = None; let _ = self.threads.destroy(*tid); } @@ -315,28 +317,31 @@ impl Kernel

{ } } - /// 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; + /// Destroys every notification `owner` created, waking its waiters + /// with an error and unbinding its bound thread. + pub(crate) fn destroy_notifications_owned_by(&mut self, owner: ProcessId) { + let mut owned = [None; MAX_NOTIFICATIONS]; + for (slot, id) in owned.iter_mut().zip(self.notifications.ids_owned_by(owner)) { + *slot = Some(id); } - 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); + for id in owned.into_iter().flatten() { + let Some(mut n) = self.notifications.destroy(id) else { + continue; + }; + if let Some(bound) = n.bound() { + self.ipc[bound.raw() as usize].bound = None; + } + while let Some(tid) = n.pop_waiter() { + self.wake_with_error(tid, KernelError::NoSuchObject); + } } } - 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); - } + /// Makes a blocked thread runnable with `error` as its syscall result, + /// dropping whatever it was about to send. + fn wake_with_error(&mut self, tid: ThreadId, error: KernelError) { + self.ipc[tid.raw() as usize].outgoing = None; + self.wake(tid, SyscallResult::Error(error)); } fn check_cap( @@ -362,6 +367,7 @@ impl Kernel

{ match resource { Resource::Endpoint(id) => self.endpoints.generation(*id), Resource::Memory(id) => self.regions.generation(*id), + Resource::Notification(id) => self.notifications.generation(*id), Resource::Process(pid) => self.processes.generation(*pid), Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()), } @@ -371,6 +377,7 @@ impl Kernel

{ match resource { Resource::Endpoint(id) => self.endpoints.revoke(*id), Resource::Memory(id) => self.regions.revoke(*id), + Resource::Notification(id) => self.notifications.revoke(*id), _ => {} } } diff --git a/crates/kernel/src/syscall/capability.rs b/crates/kernel/src/syscall/capability.rs index a9f1721..46ed6ae 100644 --- a/crates/kernel/src/syscall/capability.rs +++ b/crates/kernel/src/syscall/capability.rs @@ -11,13 +11,17 @@ impl Kernel

{ to_endpoint: types::bounded::CapSlot, restrict: types::permission::PermissionSet, ) -> SyscallOutcome { - let (resource, permissions) = - match self.check_cap(caller_pid, slot, types::permission::Permission::Delegate) { - Ok(cap) => (cap.resource().clone(), *cap.permissions()), - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - - let restricted = permissions.restrict_to(&restrict); + // A copy of the capability itself, so its badge and generation + // travel with it; only the permissions narrow. + let copy = match self.check_cap(caller_pid, slot, types::permission::Permission::Delegate) { + Ok(cap) => domain::capability::Capability::new( + cap.resource().clone(), + cap.permissions().restrict_to(&restrict), + *cap.generation(), + ) + .with_badge(cap.badge()), + Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), + }; let target_pid = match self.check_cap( caller_pid, @@ -38,7 +42,11 @@ impl Kernel

{ Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; - if let Err(e) = self.grant_cap(target_pid, resource, restricted) { + let inserted = self + .processes + .get_mut(target_pid) + .and_then(|p| p.capabilities_mut().insert(copy)); + if let Err(e) = inserted { return SyscallOutcome::Complete(SyscallResult::Error(e)); } diff --git a/crates/kernel/src/syscall/fault.rs b/crates/kernel/src/syscall/fault.rs index d223822..b4a432c 100644 --- a/crates/kernel/src/syscall/fault.rs +++ b/crates/kernel/src/syscall/fault.rs @@ -1,47 +1,42 @@ +use domain::{fault::Fault, message::Message}; use platform::Platform; +use types::{id::ThreadId, permission::Permission}; +use super::{Delivery, DeliveryKind, SyscallResult}; use crate::Kernel; impl Kernel

{ - pub fn handle_fault( - &mut self, - tid: types::id::ThreadId, - fault: domain::fault::Fault, - fault_pc: usize, - ) { - let pid = match self.threads.get(tid) { - Ok(t) => t.process(), - Err(_) => return, + /// A thread faulted. Report it to the process's fault endpoint (if one + /// is set, valid and has a receiver waiting), then destroy the process. + pub fn handle_fault(&mut self, tid: ThreadId, fault: Fault, fault_pc: usize) { + let Some(pid) = self.thread_process(tid) else { + return; }; - - // Try to deliver fault to a fault handler endpoint (if configured) - if let Ok(process) = self.processes.get(pid) - && let Some(handler_slot) = process.fault_handler() - && let Ok(cap) = process.capabilities().get(handler_slot) - && let domain::resource::Resource::Endpoint(ep_id) = cap.resource() + let handler = self + .processes + .get(pid) + .ok() + .and_then(|p| p.fault_handler()) + .and_then(|slot| self.check_cap(pid, slot, Permission::Execute).ok()) + .and_then(|cap| match cap.resource() { + domain::resource::Resource::Endpoint(ep) => Some((*ep, cap.badge())), + _ => None, + }); + if let Some((ep, badge)) = handler + && let Some(receiver) = self + .endpoints + .get_mut(ep) + .and_then(|e| e.dequeue_receiver()) { - let msg = domain::message::Message::from_words([ - pid.raw() as usize, - fault.code(), - fault.address(), - fault_pc, - ]); - - if let Some(ep) = self.endpoints.get_mut(*ep_id) - && ep.has_waiting_receiver() - && let Some(receiver_tid) = ep.dequeue_receiver() - { - self.pending_results[receiver_tid.raw() as usize] = - Some(crate::SyscallResult::Message(msg)); - - if let Ok(receiver) = self.threads.get_mut(receiver_tid) { - let priority = receiver.priority(); - let _ = receiver.unblock(); - let core = self.platform.core_id(); - use domain::scheduler::Scheduler; - self.scheduler.add(receiver_tid, priority, core); - } - } + let report = + Message::from_words([pid.raw() as usize, fault.code(), fault.address(), fault_pc]); + let delivery = Delivery { + kind: DeliveryKind::Message, + badge: badge.raw(), + words: *report.words(), + cap: None, + }; + self.wake(receiver, SyscallResult::Received(delivery)); } self.destroy_process(pid); diff --git a/crates/kernel/src/syscall/ipc.rs b/crates/kernel/src/syscall/ipc.rs index 5b95781..a6b8fc9 100644 --- a/crates/kernel/src/syscall/ipc.rs +++ b/crates/kernel/src/syscall/ipc.rs @@ -1,346 +1,553 @@ -use platform::Platform; +//! Endpoint IPC, notifications and interrupt delivery. docs/ipc.md is the +//! specification; the comments below only point at the non-obvious rules. -use super::{SyscallOutcome, SyscallResult}; +use domain::{ + capability::Capability, + error::KernelError, + message::Message, + notification::SignalEffect, + resource::Resource, + scheduler::Scheduler, + thread::{BlockReason, ThreadState}, +}; +use platform::Platform; +use types::{ + badge::Badge, + bounded::CapSlot, + generation::Generation, + id::{EndpointId, IrqId, NotificationId, ProcessId, ThreadId}, + permission::{Permission, PermissionSet}, +}; + +use super::{Delivery, DeliveryKind, SyscallOutcome, SyscallResult}; use crate::Kernel; +/// A thread named by id and generation, so a stale reference can be told +/// apart from a new thread that reused the id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ThreadRef { + pub tid: ThreadId, + pub generation: Generation, +} + +/// A message a blocked sender is waiting to hand over. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Outgoing { + message: Message, + badge: Badge, + /// The sender's endpoint cap had `Delegate`, so a capability may travel. + grant: bool, +} + +/// Per-thread IPC bookkeeping. +#[derive(Debug, Clone, Copy, Default)] +pub struct IpcState { + pub(crate) outgoing: Option, + /// The caller this thread owes a reply to. + pub(crate) reply_to: Option, + pub(crate) bound: Option, +} + +fn rw_delegate() -> PermissionSet { + PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Write) + .with(Permission::Delegate) +} + impl Kernel

{ - pub(crate) fn handle_create_endpoint( + fn thread_ref(&self, tid: ThreadId) -> Option { + Some(ThreadRef { + tid, + generation: self.threads.generation(tid)?, + }) + } + + fn is_live(&self, r: ThreadRef) -> bool { + self.threads.generation(r.tid) == Some(r.generation) + } + + fn is_blocked_as(&self, tid: ThreadId, f: impl Fn(&BlockReason) -> bool) -> bool { + matches!(self.threads.get(tid).map(|t| *t.state()), Ok(ThreadState::Blocked(r)) if f(&r)) + } + + /// Blocks the running caller. + pub(crate) fn block_caller(&mut self, tid: ThreadId, reason: BlockReason) -> SyscallOutcome { + if let Ok(t) = self.threads.get_mut(tid) { + let _ = t.block(reason); + } + self.scheduler.remove(tid); + SyscallOutcome::BlockCaller(reason) + } + + /// Makes a blocked thread runnable with `result` as its syscall result. + pub(crate) fn wake(&mut self, tid: ThreadId, result: SyscallResult) { + let Ok(thread) = self.threads.get_mut(tid) else { + return; + }; + let priority = thread.priority(); + if thread.unblock().is_ok() { + self.pending_results[tid.raw() as usize] = Some(result); + let core = self.platform.core_id(); + self.scheduler.add(tid, priority, core); + } + } + + /// Releases a caller waiting on `reply` with `NoReply`, if it is still + /// that caller and still waiting. + pub(crate) fn release_caller(&mut self, reply: ThreadRef) { + if self.is_live(reply) + && self.is_blocked_as(reply.tid, |r| *r == BlockReason::AwaitingReply) + { + self.wake(reply.tid, SyscallResult::Error(KernelError::NoReply)); + } + } + + fn endpoint_cap( + &self, + pid: ProcessId, + slot: CapSlot, + needed: Permission, + ) -> Result<(EndpointId, Badge, bool), KernelError> { + let cap = self.check_cap(pid, slot, needed)?; + match cap.resource() { + Resource::Endpoint(id) => Ok(( + *id, + cap.badge(), + cap.permissions().allows(Permission::Delegate), + )), + _ => Err(KernelError::WrongObject), + } + } + + fn notification_cap( + &self, + pid: ProcessId, + slot: CapSlot, + needed: Permission, + ) -> Result<(NotificationId, Badge, Generation), KernelError> { + let cap = self.check_cap(pid, slot, needed)?; + match cap.resource() { + Resource::Notification(id) => Ok((*id, cap.badge(), *cap.generation())), + _ => Err(KernelError::WrongObject), + } + } + + /// Copies the capability in `slot` of `from` into `to`'s table, if the + /// transfer is allowed and still valid. Returns where it landed. + fn transfer_cap( &mut self, - caller_pid: types::id::ProcessId, + from: ProcessId, + slot: Option, + grant: bool, + to: ProcessId, + ) -> Option { + let slot = slot?; + if !grant { + return None; + } + let copy: Capability = self + .check_cap(from, slot, Permission::Delegate) + .ok()? + .clone(); + self.processes + .get_mut(to) + .ok()? + .capabilities_mut() + .insert(copy) + .ok() + } + + /// Hands `out` from `sender` to `receiver`. For a call, the receiver's + /// reply slot now names the sender. + fn deliver( + &mut self, + sender: ThreadId, + out: Outgoing, + receiver: ThreadId, + kind: DeliveryKind, + ) -> Delivery { + let from = self.thread_process(sender); + let to = self.thread_process(receiver); + let cap = match (from, to) { + (Some(from), Some(to)) => self.transfer_cap(from, out.message.cap(), out.grant, to), + _ => None, + }; + if kind == DeliveryKind::Call { + self.ipc[receiver.raw() as usize].reply_to = self.thread_ref(sender); + } + Delivery { + kind, + badge: out.badge.raw(), + words: *out.message.words(), + cap, + } + } + + // ---- endpoints ---- + + pub(crate) fn send( + &mut self, + tid: ThreadId, + pid: ProcessId, + slot: CapSlot, + message: Message, + is_call: bool, ) -> SyscallOutcome { - let ep_id = match self.create_endpoint(caller_pid) { + let (ep, badge, grant) = match self.endpoint_cap(pid, slot, Permission::Execute) { + Ok(v) => v, + Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), + }; + let out = Outgoing { + message, + badge, + grant, + }; + let kind = if is_call { + DeliveryKind::Call + } else { + DeliveryKind::Message + }; + + let receiver = self + .endpoints + .get_mut(ep) + .and_then(|e| e.dequeue_receiver()); + if let Some(receiver) = receiver { + let delivery = self.deliver(tid, out, receiver, kind); + self.wake(receiver, SyscallResult::Received(delivery)); + if !is_call { + return SyscallOutcome::Complete(SyscallResult::Ok); + } + self.block_caller(tid, BlockReason::AwaitingReply); + // Run the server straight away instead of waiting for its turn. + let core = self.platform.core_id(); + if self.scheduler.run_now(core, receiver) + && self + .threads + .get_mut(receiver) + .and_then(|t| t.set_running(core)) + .is_ok() + { + return SyscallOutcome::SwitchTo(receiver); + } + return SyscallOutcome::BlockCaller(BlockReason::AwaitingReply); + } + + let queued = self + .endpoints + .get_mut(ep) + .is_some_and(|e| e.enqueue_sender(tid).is_some()); + if !queued { + return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull)); + } + self.ipc[tid.raw() as usize].outgoing = Some(out); + let reason = if is_call { + BlockReason::Calling(ep) + } else { + BlockReason::Sending(ep) + }; + self.block_caller(tid, reason) + } + + /// `drop_reply`: a plain `Receive` abandons any pending reply. + pub(crate) fn receive( + &mut self, + tid: ThreadId, + pid: ProcessId, + slot: CapSlot, + drop_reply: bool, + ) -> SyscallOutcome { + let (ep, _, _) = match self.endpoint_cap(pid, slot, Permission::Read) { + Ok(v) => v, + Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), + }; + if drop_reply && let Some(pending) = self.ipc[tid.raw() as usize].reply_to.take() { + self.release_caller(pending); + } + + if let Some(n) = self.ipc[tid.raw() as usize].bound + && let Some(ntfn) = self.notifications.get_mut(n) + && ntfn.has_bits() + { + let bits = ntfn.take_bits(); + return SyscallOutcome::Complete(SyscallResult::Received(Delivery::signal(bits))); + } + + let sender = self.endpoints.get_mut(ep).and_then(|e| e.dequeue_sender()); + if let Some(sender) = sender { + let is_call = self.is_blocked_as(sender, |r| matches!(r, BlockReason::Calling(_))); + let Some(out) = self.ipc[sender.raw() as usize].outgoing.take() else { + // A queued sender always has a message; treat a missing one + // as a sender that vanished. + return SyscallOutcome::Complete(SyscallResult::Error(KernelError::WouldBlock)); + }; + let kind = if is_call { + DeliveryKind::Call + } else { + DeliveryKind::Message + }; + let delivery = self.deliver(sender, out, tid, kind); + if is_call { + if let Ok(t) = self.threads.get_mut(sender) { + let _ = t.rewait(BlockReason::AwaitingReply); + } + } else { + self.wake(sender, SyscallResult::Ok); + } + return SyscallOutcome::Complete(SyscallResult::Received(delivery)); + } + + let queued = self + .endpoints + .get_mut(ep) + .is_some_and(|e| e.enqueue_receiver(tid).is_some()); + if !queued { + return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull)); + } + self.block_caller(tid, BlockReason::Receiving(ep)) + } + + pub(crate) fn reply( + &mut self, + tid: ThreadId, + pid: ProcessId, + message: Message, + ) -> Result<(), KernelError> { + let caller = self.ipc[tid.raw() as usize] + .reply_to + .take() + .ok_or(KernelError::NoReply)?; + let waiting = self.is_live(caller) + && self.is_blocked_as(caller.tid, |r| *r == BlockReason::AwaitingReply); + if !waiting { + // The caller died; the reply has nowhere to go. + return Ok(()); + } + let to = self.thread_process(caller.tid); + // A reply may always carry a capability the server can delegate. + let cap = to.and_then(|to| self.transfer_cap(pid, message.cap(), true, to)); + let delivery = Delivery { + kind: DeliveryKind::Message, + badge: 0, + words: *message.words(), + cap, + }; + self.wake(caller.tid, SyscallResult::Received(delivery)); + Ok(()) + } + + pub(crate) fn reply_recv( + &mut self, + tid: ThreadId, + pid: ProcessId, + slot: CapSlot, + message: Message, + ) -> SyscallOutcome { + if let Err(e) = self.endpoint_cap(pid, slot, Permission::Read) { + return SyscallOutcome::Complete(SyscallResult::Error(e)); + } + if self.ipc[tid.raw() as usize].reply_to.is_some() { + let _ = self.reply(tid, pid, message); + } + self.receive(tid, pid, slot, false) + } + + pub(crate) fn mint( + &mut self, + pid: ProcessId, + slot: CapSlot, + badge: Badge, + permissions: PermissionSet, + ) -> Result { + let source = self.check_cap(pid, slot, Permission::Delegate)?; + if !matches!( + source.resource(), + Resource::Endpoint(_) | Resource::Notification(_) + ) { + return Err(KernelError::WrongObject); + } + if !source.badge().is_none() { + return Err(KernelError::InvalidArgument); + } + let minted = Capability::new( + source.resource().clone(), + source.permissions().restrict_to(&permissions), + *source.generation(), + ) + .with_badge(badge); + self.processes + .get_mut(pid)? + .capabilities_mut() + .insert(minted) + } + + pub(crate) fn handle_create_endpoint(&mut self, pid: ProcessId) -> SyscallOutcome { + let id = match self.create_endpoint(pid) { Ok(id) => id, Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; - - let all_perms = types::permission::PermissionSet::empty() - .with(types::permission::Permission::Read) - .with(types::permission::Permission::Write) - .with(types::permission::Permission::Execute) - .with(types::permission::Permission::Delegate); - - match self.grant_cap( - caller_pid, - domain::resource::Resource::Endpoint(ep_id), - all_perms, - ) { + let perms = PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Execute) + .with(Permission::Delegate); + match self.grant_cap(pid, Resource::Endpoint(id), perms) { Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)), Err(e) => { - let _ = self.endpoints.destroy(ep_id); + self.endpoints.destroy(id); SyscallOutcome::Complete(SyscallResult::Error(e)) } } } - pub(crate) fn handle_send( - &mut self, - caller_tid: types::id::ThreadId, - caller_pid: types::id::ProcessId, - endpoint_slot: types::bounded::CapSlot, - message: domain::message::Message, - ) -> SyscallOutcome { - use domain::scheduler::Scheduler; + // ---- notifications ---- - let ep_id = match self.check_cap( - caller_pid, - endpoint_slot, - types::permission::Permission::Execute, - ) { - Ok(cap) => match cap.resource() { - domain::resource::Resource::Endpoint(id) => *id, - _ => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint( - types::id::EndpointId::from_raw(0), - ), - )); - } - }, + pub fn create_notification(&mut self, owner: ProcessId) -> Result { + self.processes.get(owner)?; + self.notifications + .create(owner) + .ok_or(KernelError::NoFreeSlot) + } + + pub(crate) fn create_notification_cap( + &mut self, + pid: ProcessId, + ) -> Result { + let id = self.create_notification(pid)?; + self.grant_cap(pid, Resource::Notification(id), rw_delegate()) + .inspect_err(|_| { + self.notifications.destroy(id); + }) + } + + /// Signals `id` with `badge`; shared by `Signal` and interrupts. + fn signal_notification(&mut self, id: NotificationId, badge: Badge) { + let bound_receiving = self + .notifications + .get(id) + .and_then(|n| n.bound()) + .is_some_and(|t| self.is_blocked_as(t, |r| matches!(r, BlockReason::Receiving(_)))); + let Some(ntfn) = self.notifications.get_mut(id) else { + return; + }; + match ntfn.signal(badge, bound_receiving) { + SignalEffect::WakeWaiter(t, bits) => { + self.wake(t, SyscallResult::Received(Delivery::signal(bits))); + } + SignalEffect::WakeBound(t, bits) => { + self.endpoints.remove_thread_from_all(t); + self.wake(t, SyscallResult::Received(Delivery::signal(bits))); + } + SignalEffect::Stored => {} + } + } + + pub(crate) fn signal(&mut self, pid: ProcessId, slot: CapSlot) -> Result<(), KernelError> { + let (id, badge, _) = self.notification_cap(pid, slot, Permission::Write)?; + self.signal_notification(id, badge); + Ok(()) + } + + pub(crate) fn wait(&mut self, tid: ThreadId, pid: ProcessId, slot: CapSlot) -> SyscallOutcome { + let id = match self.notification_cap(pid, slot, Permission::Read) { + Ok((id, _, _)) => id, Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), }; - - let endpoint = match self.endpoints.get_mut(ep_id) { - Some(ep) => ep, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint(ep_id), - )); - } + let Some(ntfn) = self.notifications.get_mut(id) else { + return SyscallOutcome::Complete(SyscallResult::Error(KernelError::NoSuchObject)); }; + if ntfn.has_bits() { + let bits = ntfn.take_bits(); + return SyscallOutcome::Complete(SyscallResult::Received(Delivery::signal(bits))); + } + if !ntfn.add_waiter(tid) { + return SyscallOutcome::Complete(SyscallResult::Error(KernelError::EndpointFull)); + } + self.block_caller(tid, BlockReason::WaitingNotification(id)) + } - if endpoint.has_waiting_receiver() { - let receiver_tid = match endpoint.dequeue_receiver() { - Some(t) => t, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::WouldBlock, - )); - } - }; + pub(crate) fn poll(&mut self, pid: ProcessId, slot: CapSlot) -> Result { + let (id, _, _) = self.notification_cap(pid, slot, Permission::Read)?; + self.notifications + .get_mut(id) + .map(|n| n.take_bits()) + .ok_or(KernelError::NoSuchObject) + } - self.pending_results[receiver_tid.raw() as usize] = - Some(SyscallResult::Message(message)); + pub(crate) fn bind_notification( + &mut self, + tid: ThreadId, + pid: ProcessId, + slot: CapSlot, + ) -> Result<(), KernelError> { + let (id, _, _) = self.notification_cap(pid, slot, Permission::Read)?; + if self.ipc[tid.raw() as usize].bound.is_some() { + return Err(KernelError::InvalidArgument); + } + let ntfn = self + .notifications + .get_mut(id) + .ok_or(KernelError::NoSuchObject)?; + if !ntfn.bind(tid) { + return Err(KernelError::InvalidArgument); + } + self.ipc[tid.raw() as usize].bound = Some(id); + Ok(()) + } - let core = self.platform.core_id(); + // ---- interrupts ---- - let receiver = match self.threads.get_mut(receiver_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let _ = receiver.unblock(); - let _ = receiver.set_running(core); + fn interrupt_cap(&self, pid: ProcessId, slot: CapSlot) -> Result { + match self.check_cap(pid, slot, Permission::Execute)?.resource() { + Resource::Interrupt(irq) => Ok(*irq), + _ => Err(KernelError::WrongObject), + } + } - let caller = match self.threads.get_mut(caller_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let caller_priority = caller.priority(); - let _ = caller.preempt(); - self.scheduler.add(caller_tid, caller_priority, core); - - self.scheduler.set_current(core, receiver_tid); - - SyscallOutcome::SwitchTo(receiver_tid) + pub(crate) fn set_irq_notification( + &mut self, + pid: ProcessId, + irq_slot: CapSlot, + ntfn_slot: CapSlot, + ) -> Result<(), KernelError> { + let irq = self.interrupt_cap(pid, irq_slot)?; + let (notification, badge, generation) = + self.notification_cap(pid, ntfn_slot, Permission::Write)?; + let route = domain::irq::IrqRoute { + notification, + generation, + badge, + }; + if self.irqs.set(irq, route) { + Ok(()) } else { - self.pending_messages[caller_tid.raw() as usize] = Some(message); - - let endpoint = match self.endpoints.get_mut(ep_id) { - Some(ep) => ep, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint(ep_id), - )); - } - }; - if endpoint.enqueue_sender(caller_tid).is_none() { - self.pending_messages[caller_tid.raw() as usize] = None; - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::WouldBlock, - )); - } - - let caller = match self.threads.get_mut(caller_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let _ = caller.block(domain::thread::BlockReason::Sending(endpoint_slot)); - self.scheduler.remove(caller_tid); - - SyscallOutcome::BlockCaller(domain::thread::BlockReason::Sending(endpoint_slot)) + Err(KernelError::InvalidIrq(irq)) } } - pub(crate) fn handle_receive( + pub(crate) fn ack_interrupt( &mut self, - caller_tid: types::id::ThreadId, - caller_pid: types::id::ProcessId, - endpoint_slot: types::bounded::CapSlot, - ) -> SyscallOutcome { - use domain::scheduler::Scheduler; - - let ep_id = match self.check_cap( - caller_pid, - endpoint_slot, - types::permission::Permission::Read, - ) { - Ok(cap) => match cap.resource() { - domain::resource::Resource::Endpoint(id) => *id, - _ => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint( - types::id::EndpointId::from_raw(0), - ), - )); - } - }, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - - let endpoint = match self.endpoints.get_mut(ep_id) { - Some(ep) => ep, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint(ep_id), - )); - } - }; - - if endpoint.has_waiting_sender() { - let sender_tid = match endpoint.dequeue_sender() { - Some(t) => t, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::WouldBlock, - )); - } - }; - - let message = self.pending_messages[sender_tid.raw() as usize].take(); - debug_assert!( - message.is_some(), - "sender in wait queue had no pending message" - ); - let message = message.unwrap_or_else(domain::message::Message::empty); - - self.pending_results[sender_tid.raw() as usize] = Some(SyscallResult::Ok); - - let sender = match self.threads.get_mut(sender_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let priority = sender.priority(); - let _ = sender.unblock(); - let core = self.platform.core_id(); - self.scheduler.add(sender_tid, priority, core); - - SyscallOutcome::Complete(SyscallResult::Message(message)) - } else { - let endpoint = match self.endpoints.get_mut(ep_id) { - Some(ep) => ep, - None => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidEndpoint(ep_id), - )); - } - }; - if endpoint.enqueue_receiver(caller_tid).is_none() { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::WouldBlock, - )); - } - - let caller = match self.threads.get_mut(caller_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let _ = caller.block(domain::thread::BlockReason::Receiving(endpoint_slot)); - self.scheduler.remove(caller_tid); - - SyscallOutcome::BlockCaller(domain::thread::BlockReason::Receiving(endpoint_slot)) - } + pid: ProcessId, + slot: CapSlot, + ) -> Result<(), KernelError> { + let irq = self.interrupt_cap(pid, slot)?; + self.platform.unmask_irq(irq); + Ok(()) } - pub(crate) fn handle_wait_for_interrupt( - &mut self, - caller_tid: types::id::ThreadId, - caller_pid: types::id::ProcessId, - irq_slot: types::bounded::CapSlot, - ) -> SyscallOutcome { - use domain::scheduler::Scheduler; - - let irq_id = match self.check_cap(caller_pid, irq_slot, types::permission::Permission::Read) - { - Ok(cap) => match cap.resource() { - domain::resource::Resource::Interrupt(id) => *id, - _ => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidIrq(types::id::IrqId::from_raw(0)), - )); - } - }, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), + /// An interrupt fired: mask the line and signal its notification. The + /// line stays masked until the handler acknowledges it. + pub fn handle_interrupt(&mut self, irq: IrqId) { + self.platform.mask_irq(irq); + let Some(route) = self.irqs.route(irq) else { + return; }; - - let _ = self.irqs.register(irq_id, caller_tid); - - let caller = match self.threads.get_mut(caller_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let _ = caller.block(domain::thread::BlockReason::WaitingForInterrupt(irq_slot)); - self.scheduler.remove(caller_tid); - - SyscallOutcome::BlockCaller(domain::thread::BlockReason::WaitingForInterrupt(irq_slot)) - } - - pub(crate) fn handle_ack_interrupt( - &mut self, - caller_pid: types::id::ProcessId, - irq_slot: types::bounded::CapSlot, - ) -> SyscallOutcome { - let irq_id = - match self.check_cap(caller_pid, irq_slot, types::permission::Permission::Write) { - Ok(cap) => match cap.resource() { - domain::resource::Resource::Interrupt(id) => *id, - _ => { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::InvalidIrq(types::id::IrqId::from_raw(0)), - )); - } - }, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - - self.platform.unmask_irq(irq_id); - SyscallOutcome::Complete(SyscallResult::Ok) - } - - pub(crate) fn handle_wait_any( - &mut self, - caller_tid: types::id::ThreadId, - caller_pid: types::id::ProcessId, - sources: domain::syscall::WaitSet, - ) -> SyscallOutcome { - use domain::scheduler::Scheduler; - - if sources.is_empty() { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::WouldBlock, - )); + // A route set through a capability that has since been revoked, or + // to a notification whose slot was reused, is dead. + if self.notifications.generation(route.notification) != Some(route.generation) { + self.irqs.clear(irq); + return; } - - for slot in sources.endpoints().iter().flatten() { - let ep_id = match self.check_cap(caller_pid, *slot, types::permission::Permission::Read) - { - Ok(cap) => match cap.resource() { - domain::resource::Resource::Endpoint(id) => *id, - _ => continue, - }, - Err(_) => continue, - }; - - if let Some(ep) = self.endpoints.get_mut(ep_id) - && ep.has_waiting_sender() - { - return SyscallOutcome::Complete(SyscallResult::WakeReason( - domain::syscall::WakeReason::MessageReady(*slot), - )); - } - } - - for irq_slot in sources.interrupts() { - if let Some(slot) = irq_slot - && let Ok(cap) = - self.check_cap(caller_pid, *slot, types::permission::Permission::Read) - && let domain::resource::Resource::Interrupt(irq_id) = cap.resource() - && self.irqs.owner(*irq_id).is_some() - { - return SyscallOutcome::Complete(SyscallResult::WakeReason( - domain::syscall::WakeReason::InterruptFired(*slot), - )); - } - } - - for ep_slot in sources.endpoints() { - if let Some(slot) = ep_slot - && let Ok(cap) = - self.check_cap(caller_pid, *slot, types::permission::Permission::Read) - && let domain::resource::Resource::Endpoint(id) = cap.resource() - && let Some(ep) = self.endpoints.get_mut(*id) - { - let _ = ep.enqueue_receiver(caller_tid); - } - } - - self.wait_sets[caller_tid.raw() as usize] = Some(sources); - - let caller = match self.threads.get_mut(caller_tid) { - Ok(t) => t, - Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), - }; - let _ = caller.block(domain::thread::BlockReason::WaitingAny); - self.scheduler.remove(caller_tid); - - SyscallOutcome::BlockCaller(domain::thread::BlockReason::WaitingAny) + self.signal_notification(route.notification, route.badge); } } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 761fa2a..450b47c 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -5,28 +5,95 @@ mod lifecycle; mod memory; mod simple; -use domain::{error::KernelError, message::Message, syscall::Syscall}; +use domain::{error::KernelError, syscall::Syscall}; use platform::Platform; -use types::{bounded::CapSlot, id::ThreadId}; +use types::{bounded::CapSlot, id::ThreadId, syscall_abi::ErrorCode}; use crate::Kernel; +pub use ipc::{IpcState, ThreadRef}; + pub enum SyscallOutcome { Complete(SyscallResult), BlockCaller(domain::thread::BlockReason), + /// The caller blocked and `ThreadId` should run next. SwitchTo(ThreadId), TerminateCaller, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeliveryKind { + /// A `Send`; no reply expected. + Message, + /// A `Call`; the receiver's reply slot now names the caller. + Call, + /// Notification bits. + Signal, +} + +/// What a receiving syscall hands back (docs/ipc.md, register ABI). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Delivery { + pub kind: DeliveryKind, + /// The sender's badge, or the signal bits. + pub badge: usize, + pub words: [usize; 4], + /// Where a transferred capability landed in the receiver's table. + pub cap: Option, +} + +impl Delivery { + pub fn signal(bits: usize) -> Self { + Self { + kind: DeliveryKind::Signal, + badge: bits, + words: [0; 4], + cap: None, + } + } +} + +#[derive(Debug)] pub enum SyscallResult { Ok, Value(usize), Slot(CapSlot), - Message(Message), - WakeReason(domain::syscall::WakeReason), + Received(Delivery), Error(KernelError), } +/// The status code userspace sees for a kernel error. +pub fn error_code(error: &KernelError) -> ErrorCode { + use KernelError::*; + match error { + InvalidSlot(_) | RevokedCapability(_) | NoSuchObject | InvalidEndpoint(_) => { + ErrorCode::InvalidCap + } + PermissionDenied { .. } | ProcessCreateDenied => ErrorCode::PermissionDenied, + WrongObject => ErrorCode::WrongObject, + NoFreeSlot => ErrorCode::NoFreeSlot, + OutOfMemory => ErrorCode::OutOfMemory, + InvalidAddress => ErrorCode::InvalidAddress, + AddressInUse => ErrorCode::AddressInUse, + NoReply => ErrorCode::NoReply, + TooManyThreadsInProcess | TooManyProcesses | TooManyThreads | EndpointFull => { + ErrorCode::LimitReached + } + InvalidProcess(_) + | InvalidThread(_) + | InvalidStateTransition(_) + | InvalidIrq(_) + | InvalidRegion + | IrqAlreadyOwned(_) + | WouldBlock + | InvalidArgument => ErrorCode::InvalidArgument, + } +} + +fn complete(result: Result) -> SyscallOutcome { + SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error)) +} + impl Kernel

{ pub fn handle_syscall(&mut self, syscall: Syscall) -> SyscallOutcome { let (tid, pid) = match self.caller() { @@ -54,11 +121,45 @@ impl Kernel

{ to_endpoint, permissions, } => self.handle_delegate(pid, slot, to_endpoint, permissions), - Syscall::Send { endpoint, message } => self.handle_send(tid, pid, endpoint, message), - Syscall::Receive { endpoint } => self.handle_receive(tid, pid, endpoint), - Syscall::WaitForInterrupt { irq } => self.handle_wait_for_interrupt(tid, pid, irq), - Syscall::AcknowledgeInterrupt { irq } => self.handle_ack_interrupt(pid, irq), - Syscall::WaitAny { sources } => self.handle_wait_any(tid, pid, sources), + Syscall::Mint { + slot, + badge, + permissions, + } => complete( + self.mint(pid, slot, badge, permissions) + .map(SyscallResult::Slot), + ), + Syscall::Send { endpoint, message } => self.send(tid, pid, endpoint, message, false), + Syscall::Call { endpoint, message } => self.send(tid, pid, endpoint, message, true), + Syscall::Receive { endpoint } => self.receive(tid, pid, endpoint, true), + Syscall::Reply { message } => { + complete(self.reply(tid, pid, message).map(|()| SyscallResult::Ok)) + } + Syscall::ReplyRecv { endpoint, message } => { + self.reply_recv(tid, pid, endpoint, message) + } + Syscall::CreateNotification => { + complete(self.create_notification_cap(pid).map(SyscallResult::Slot)) + } + Syscall::Signal { notification } => { + complete(self.signal(pid, notification).map(|()| SyscallResult::Ok)) + } + Syscall::Wait { notification } => self.wait(tid, pid, notification), + Syscall::Poll { notification } => complete( + self.poll(pid, notification) + .map(|bits| SyscallResult::Received(Delivery::signal(bits))), + ), + Syscall::BindNotification { notification } => complete( + self.bind_notification(tid, pid, notification) + .map(|()| SyscallResult::Ok), + ), + Syscall::SetIrqNotification { irq, notification } => complete( + self.set_irq_notification(pid, irq, notification) + .map(|()| SyscallResult::Ok), + ), + Syscall::AcknowledgeInterrupt { irq } => { + complete(self.ack_interrupt(pid, irq).map(|()| SyscallResult::Ok)) + } Syscall::Halt => self.handle_halt(pid), Syscall::CreateEndpoint => self.handle_create_endpoint(pid), Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte), diff --git a/crates/kernel/src/tests/helpers.rs b/crates/kernel/src/tests/helpers.rs index 4f8332e..ca732b3 100644 --- a/crates/kernel/src/tests/helpers.rs +++ b/crates/kernel/src/tests/helpers.rs @@ -5,7 +5,7 @@ use platform::Platform; use types::{ address::PhysicalAddress, bounded::{CapSlot, Priority}, - id::{EndpointId, IrqId, ProcessId, ThreadId}, + id::{EndpointId, ProcessId, ThreadId}, permission::PermissionSet, time::TimeSlice, }; @@ -41,10 +41,6 @@ impl Kernel { tid } - pub fn register_irq_for_test(&mut self, irq: IrqId, thread: ThreadId) { - let _ = self.irqs.register(irq, thread); - } - pub fn block_thread_for_test(&mut self, tid: ThreadId, reason: domain::thread::BlockReason) { let core = self.platform.core_id(); if let Ok(t) = self.threads.get_mut(tid) { @@ -128,6 +124,10 @@ impl Kernel { } } + pub fn destroy_process_notifications_for_test(&mut self, owner: ProcessId) { + self.destroy_notifications_owned_by(owner); + } + pub fn thread_state_for_test(&self, tid: ThreadId) -> Option { self.threads.get(tid).ok().map(|t| *t.state()) } diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index 2602f96..5853a0c 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -1,227 +1,870 @@ +//! IPC semantics from docs/ipc.md: rendezvous, badges, Call/Reply, +//! capability transfer, notifications and interrupt delivery. + use super::*; +use crate::syscall::{Delivery, DeliveryKind}; use crate::{SyscallOutcome, SyscallResult}; +use domain::error::KernelError; use domain::message::Message; -use domain::syscall::{Syscall, WaitSet, WakeReason}; -use domain::thread::BlockReason; +use domain::resource::Resource; +use domain::syscall::Syscall; +use domain::thread::ThreadState; +use types::address::DataRef; +use types::badge::Badge; use types::bounded::CapSlot; -use types::id::{IrqId, ThreadId}; +use types::id::{EndpointId, IrqId, NotificationId, ThreadId}; use types::permission::{Permission, PermissionSet}; -fn setup_ipc_kernel() -> ( - crate::Kernel, - ProcessId, // process A - ProcessId, // process B - CapSlot, // endpoint cap for A (Execute perm) - CapSlot, // endpoint cap for B (Read perm) - ThreadId, // thread B id -) { - let (mut k, pid_a) = kernel_with_process(); +fn perms(ps: &[Permission]) -> PermissionSet { + ps.iter().fold(PermissionSet::empty(), |s, p| s.with(*p)) +} - // Create process B (CreateProcess no longer spawns threads) - let _proc_b_slot = match k.handle_syscall(Syscall::CreateProcess { - create_cap: CapSlot::new(0).unwrap(), - image: types::address::DataRef::empty(), - }) { +const SEND: &[Permission] = &[Permission::Execute]; +const RECV: &[Permission] = &[Permission::Read]; +const SEND_GRANT: &[Permission] = &[Permission::Execute, Permission::Delegate]; + +/// Client A (pid 0) and server B (pid 1), one thread each, and an endpoint. +struct Ipc { + k: Kernel, + a: ProcessId, + b: ProcessId, + ta: ThreadId, + tb: ThreadId, + ep: EndpointId, + a_send: CapSlot, + b_recv: CapSlot, +} + +impl Ipc { + fn new() -> Self { + let (mut k, a) = kernel_with_process(); + let ta = k.current_thread().unwrap(); + match k.handle_syscall(Syscall::CreateProcess { + create_cap: CapSlot::new(0).unwrap(), + image: DataRef::empty(), + }) { + SyscallOutcome::Complete(SyscallResult::Slot(_)) => {} + _ => panic!("create process"), + } + let b = ProcessId::from_raw(1); + let tb = k.spawn_thread_for_test(b, Priority::DEFAULT, TimeSlice::DEFAULT); + let ep = k.create_endpoint(b).unwrap(); + let a_send = k.grant_cap_for_test(a, Resource::Endpoint(ep), perms(SEND_GRANT)); + let b_recv = k.grant_cap_for_test(b, Resource::Endpoint(ep), perms(RECV)); + Self { + k, + a, + b, + ta, + tb, + ep, + a_send, + b_recv, + } + } + + fn run(&mut self, tid: ThreadId, call: Syscall) -> SyscallOutcome { + self.k.switch_to_thread_for_test(tid); + self.k.handle_syscall(call) + } + + fn state(&self, tid: ThreadId) -> Option { + self.k.thread_state_for_test(tid) + } + + fn woken_with(&self, tid: ThreadId) -> Option<&SyscallResult> { + self.k.pending_result(tid) + } +} + +fn msg(w: [usize; 4]) -> Message { + Message::from_words(w) +} + +fn delivered(outcome: &SyscallOutcome) -> Option { + match outcome { + SyscallOutcome::Complete(SyscallResult::Received(d)) => Some(*d), + _ => None, + } +} + +fn pending_delivery(r: Option<&SyscallResult>) -> Option { + match r { + Some(SyscallResult::Received(d)) => Some(*d), + _ => None, + } +} + +fn blocked(outcome: &SyscallOutcome) -> bool { + matches!( + outcome, + SyscallOutcome::BlockCaller(_) | SyscallOutcome::SwitchTo(_) + ) +} + +fn error(outcome: &SyscallOutcome) -> Option<&KernelError> { + match outcome { + SyscallOutcome::Complete(SyscallResult::Error(e)) => Some(e), + _ => None, + } +} + +// ---- rendezvous ---- + +#[test] +fn send_blocks_until_a_receiver_arrives() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + assert!(blocked(&t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([1, 2, 3, 4]) + } + ))); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.words, [1, 2, 3, 4]); + assert_eq!(d.kind, DeliveryKind::Message); + assert!(matches!(t.state(ta), Some(ThreadState::Ready))); + assert!(matches!(t.woken_with(ta), Some(SyscallResult::Ok))); +} + +#[test] +fn receive_blocks_until_a_sender_arrives() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + assert!(blocked(&t.run(tb, Syscall::Receive { endpoint: b_recv }))); + let out = t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([9, 8, 7, 6]), + }, + ); + assert!(!blocked(&out) && error(&out).is_none()); + let d = pending_delivery(t.woken_with(tb)).unwrap(); + assert_eq!(d.words, [9, 8, 7, 6]); +} + +#[test] +fn senders_are_served_in_arrival_order() { + let mut t = Ipc::new(); + let ta2 = + t.k.spawn_thread_for_test(t.a, Priority::DEFAULT, TimeSlice::DEFAULT); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([1, 0, 0, 0]), + }, + ); + t.run( + ta2, + Syscall::Send { + endpoint: a_send, + message: msg([2, 0, 0, 0]), + }, + ); + let first = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + let second = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!((first.words[0], second.words[0]), (1, 2)); +} + +#[test] +fn send_needs_execute_and_receive_needs_read() { + let mut t = Ipc::new(); + let (a, b, ep, ta, tb) = (t.a, t.b, t.ep, t.ta, t.tb); + let a_recv_only = + t.k.grant_cap_for_test(a, Resource::Endpoint(ep), perms(RECV)); + let b_send_only = + t.k.grant_cap_for_test(b, Resource::Endpoint(ep), perms(SEND)); + assert!(matches!( + error(&t.run( + ta, + Syscall::Send { + endpoint: a_recv_only, + message: msg([0; 4]) + } + )), + Some(KernelError::PermissionDenied { .. }) + )); + assert!(matches!( + error(&t.run( + tb, + Syscall::Receive { + endpoint: b_send_only + } + )), + Some(KernelError::PermissionDenied { .. }) + )); +} + +// ---- badges ---- + +#[test] +fn receiver_sees_the_badge_of_the_senders_cap() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + let badged = match t.run( + ta, + Syscall::Mint { + slot: a_send, + badge: Badge::new(42), + permissions: perms(SEND), + }, + ) { SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, - _ => panic!("expected slot"), + other => panic!("mint failed: {:?}", error(&other)), }; + t.run( + ta, + Syscall::Send { + endpoint: badged, + message: msg([0; 4]), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.badge, 42); +} - let pid_b = ProcessId::from_raw(1); - let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT); +#[test] +fn unbadged_senders_show_badge_zero() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.badge, 0); +} - // Create an endpoint - let ep_id = k.create_endpoint_for_test(); - - // Grant endpoint caps to both processes - let cap_a = k.grant_cap_for_test( - pid_a, - domain::resource::Resource::Endpoint(ep_id), - PermissionSet::empty() - .with(Permission::Execute) - .with(Permission::Read), +#[test] +fn mint_cannot_rebadge_or_widen_permissions() { + let mut t = Ipc::new(); + let (ta, a_send) = (t.ta, t.a_send); + let badged = match t.run( + ta, + Syscall::Mint { + slot: a_send, + badge: Badge::new(1), + permissions: perms(SEND_GRANT), + }, + ) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("mint"), + }; + assert!( + error(&t.run( + ta, + Syscall::Mint { + slot: badged, + badge: Badge::new(2), + permissions: perms(SEND) + }, + )) + .is_some() ); - let cap_b = k.grant_cap_for_test( - pid_b, - domain::resource::Resource::Endpoint(ep_id), - PermissionSet::empty() - .with(Permission::Execute) - .with(Permission::Read), + let narrow = match t.run( + ta, + Syscall::Mint { + slot: a_send, + badge: Badge::new(3), + permissions: perms(SEND), + }, + ) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("mint"), + }; + // Asking for Read (which the source lacks) must not grant it. + let widened = match t.run( + ta, + Syscall::Mint { + slot: a_send, + badge: Badge::new(4), + permissions: perms(&[Permission::Read, Permission::Execute]), + }, + ) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("mint"), + }; + assert!(error(&t.run(ta, Syscall::Receive { endpoint: widened })).is_some()); + let _ = narrow; +} + +#[test] +fn mint_needs_delegate() { + let mut t = Ipc::new(); + let (a, ep, ta) = (t.a, t.ep, t.ta); + let plain = + t.k.grant_cap_for_test(a, Resource::Endpoint(ep), perms(SEND)); + assert!( + error(&t.run( + ta, + Syscall::Mint { + slot: plain, + badge: Badge::new(1), + permissions: perms(SEND) + }, + )) + .is_some() ); - - (k, pid_a, pid_b, cap_a, cap_b, tid_b) } -#[test] -fn send_without_receiver_blocks_caller() { - let (mut k, _, _, cap_a, _, _) = setup_ipc_kernel(); - - let msg = Message::from_words([42, 0, 0, 0]); - let outcome = k.handle_syscall(Syscall::Send { - endpoint: cap_a, - message: msg, - }); - assert!(matches!( - outcome, - SyscallOutcome::BlockCaller(BlockReason::Sending(_)) - )); -} +// ---- call / reply ---- #[test] -fn receive_without_sender_blocks_caller() { - let (mut k, _, _pid_b, _, cap_b, tid_b) = setup_ipc_kernel(); +fn call_delivers_as_call_and_blocks_for_the_reply() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + assert!(blocked(&t.run(tb, Syscall::Receive { endpoint: b_recv }))); + assert!(blocked(&t.run( + ta, + Syscall::Call { + endpoint: a_send, + message: msg([5, 0, 0, 0]) + } + ))); + let d = pending_delivery(t.woken_with(tb)).unwrap(); + assert_eq!(d.kind, DeliveryKind::Call); + assert!(matches!(t.state(ta), Some(ThreadState::Blocked(_)))); - // Switch to thread B to call Receive - k.switch_to_thread_for_test(tid_b); - - let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_b }); - assert!(matches!( - outcome, - SyscallOutcome::BlockCaller(BlockReason::Receiving(_)) - )); -} - -#[test] -fn send_with_waiting_receiver_delivers_and_switches() { - let (mut k, _pid_a, _pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel(); - let tid_a = ThreadId::from_raw(0); - - // Thread B calls Receive first (blocks) - k.switch_to_thread_for_test(tid_b); - k.handle_syscall(Syscall::Receive { endpoint: cap_b }); - - // Thread A sends - k.switch_to_thread_for_test(tid_a); - let msg = Message::from_words([42, 0, 0, 0]); - let outcome = k.handle_syscall(Syscall::Send { - endpoint: cap_a, - message: msg, - }); - - // Should switch to the receiver - assert!(matches!(outcome, SyscallOutcome::SwitchTo(t) if t == tid_b)); - - // Receiver should have pending message - let result = k.pending_result(tid_b); - assert!(result.is_some()); -} - -#[test] -fn receive_with_waiting_sender_delivers_immediately() { - let (mut k, _pid_a, _pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel(); - let _tid_a = ThreadId::from_raw(0); - - // Thread A sends first (blocks) - let msg = Message::from_words([99, 0, 0, 0]); - k.handle_syscall(Syscall::Send { - endpoint: cap_a, - message: msg, - }); - - // Thread B calls Receive (should get message immediately) - k.switch_to_thread_for_test(tid_b); - let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_b }); - - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Message(m)) if m.word(0) == 99 - )); -} - -#[test] -fn send_checks_endpoint_capability() { - let (mut k, _, _, _, _, _) = setup_ipc_kernel(); - // slot 10 doesn't exist - let outcome = k.handle_syscall(Syscall::Send { - endpoint: CapSlot::new(10).unwrap(), - message: Message::empty(), - }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error(_)) - )); -} - -// --- WaitForInterrupt / AcknowledgeInterrupt --- - -#[test] -fn wait_for_interrupt_blocks_caller() { - let (mut k, pid_a, _, _, _, _) = setup_ipc_kernel(); - let irq = IrqId::from_raw(7); - let irq_slot = k.grant_cap_for_test( - pid_a, - domain::resource::Resource::Interrupt(irq), - PermissionSet::empty() - .with(Permission::Read) - .with(Permission::Write), + t.k.take_pending_result(tb); + let out = t.run( + tb, + Syscall::Reply { + message: msg([6, 0, 0, 0]), + }, ); + assert!(matches!(out, SyscallOutcome::Complete(SyscallResult::Ok))); + assert!(matches!(t.state(ta), Some(ThreadState::Ready))); + let reply = pending_delivery(t.woken_with(ta)).unwrap(); + assert_eq!(reply.words[0], 6); +} - let outcome = k.handle_syscall(Syscall::WaitForInterrupt { irq: irq_slot }); +#[test] +fn call_to_a_waiting_sender_queue_delivers_when_received() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + assert!(blocked(&t.run( + ta, + Syscall::Call { + endpoint: a_send, + message: msg([5, 0, 0, 0]) + } + ))); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.kind, DeliveryKind::Call); + // Still waiting for the reply, not woken by the receive. + assert!(matches!(t.state(ta), Some(ThreadState::Blocked(_)))); + assert!(t.woken_with(ta).is_none()); +} + +#[test] +fn reply_without_a_pending_call_is_an_error() { + let mut t = Ipc::new(); + let tb = t.tb; assert!(matches!( - outcome, - SyscallOutcome::BlockCaller(BlockReason::WaitingForInterrupt(_)) + error(&t.run( + tb, + Syscall::Reply { + message: msg([0; 4]) + } + )), + Some(KernelError::NoReply) )); } #[test] -fn ack_interrupt_returns_ok() { - let (mut k, pid_a, _, _, _, _) = setup_ipc_kernel(); - let irq = IrqId::from_raw(7); - let irq_slot = k.grant_cap_for_test( - pid_a, - domain::resource::Resource::Interrupt(irq), - PermissionSet::empty().with(Permission::Write), +fn a_reply_is_delivered_once() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + t.run( + ta, + Syscall::Call { + endpoint: a_send, + message: msg([0; 4]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + assert!( + error(&t.run( + tb, + Syscall::Reply { + message: msg([1, 0, 0, 0]) + } + )) + .is_none() ); - - let outcome = k.handle_syscall(Syscall::AcknowledgeInterrupt { irq: irq_slot }); assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Ok) - )); -} - -// --- WaitAny --- - -#[test] -fn wait_any_with_ready_source_returns_immediately() { - let (mut k, _pid_a, _, cap_a, cap_b, tid_b) = setup_ipc_kernel(); - let tid_a = ThreadId::from_raw(0); - - // Thread B sends a message (blocks since no receiver) - k.switch_to_thread_for_test(tid_b); - k.handle_syscall(Syscall::Send { - endpoint: cap_b, - message: Message::from_words([77, 0, 0, 0]), - }); - - // Thread A does WaitAny on the endpoint - k.switch_to_thread_for_test(tid_a); - let wait_set = WaitSet::new([Some(cap_a), None, None, None], [None, None, None, None]); - let outcome = k.handle_syscall(Syscall::WaitAny { sources: wait_set }); - assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::WakeReason(WakeReason::MessageReady(_))) + error(&t.run( + tb, + Syscall::Reply { + message: msg([2, 0, 0, 0]) + } + )), + Some(KernelError::NoReply) )); } #[test] -fn wait_any_with_no_ready_source_blocks() { - let (mut k, _pid_a, _, cap_a, _, _) = setup_ipc_kernel(); - let wait_set = WaitSet::new([Some(cap_a), None, None, None], [None, None, None, None]); - let outcome = k.handle_syscall(Syscall::WaitAny { sources: wait_set }); +fn reply_recv_answers_then_waits_for_the_next_request() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + t.run( + ta, + Syscall::Call { + endpoint: a_send, + message: msg([1, 0, 0, 0]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + let out = t.run( + tb, + Syscall::ReplyRecv { + endpoint: b_recv, + message: msg([10, 0, 0, 0]), + }, + ); + assert!(blocked(&out)); + assert_eq!(pending_delivery(t.woken_with(ta)).unwrap().words[0], 10); + assert!(matches!(t.state(tb), Some(ThreadState::Blocked(_)))); +} + +#[test] +fn receive_with_a_pending_reply_releases_the_caller_with_an_error() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv) = (t.ta, t.tb, t.a_send, t.b_recv); + t.run( + ta, + Syscall::Call { + endpoint: a_send, + message: msg([0; 4]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + assert!(matches!(t.state(ta), Some(ThreadState::Ready))); assert!(matches!( - outcome, - SyscallOutcome::BlockCaller(BlockReason::WaitingAny) + t.woken_with(ta), + Some(SyscallResult::Error(KernelError::NoReply)) )); } #[test] -fn wait_any_empty_sources_returns_error() { - let (mut k, _, _, _, _, _) = setup_ipc_kernel(); - let wait_set = WaitSet::new([None, None, None, None], [None, None, None, None]); - let outcome = k.handle_syscall(Syscall::WaitAny { sources: wait_set }); +fn server_death_releases_its_caller() { + let mut t = Ipc::new(); + let (ta, tb, a_send, b_recv, b) = (t.ta, t.tb, t.a_send, t.b_recv, t.b); + let ep2 = t.k.create_endpoint(t.a).unwrap(); + let a_send2 = + t.k.grant_cap_for_test(t.a, Resource::Endpoint(ep2), perms(SEND)); + let _ = (a_send, b_recv); + let b_recv2 = + t.k.grant_cap_for_test(b, Resource::Endpoint(ep2), perms(RECV)); + t.run( + ta, + Syscall::Call { + endpoint: a_send2, + message: msg([0; 4]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv2 }); + t.k.destroy_process(b); + assert!(matches!(t.state(ta), Some(ThreadState::Ready))); assert!(matches!( - outcome, - SyscallOutcome::Complete(SyscallResult::Error(_)) + t.woken_with(ta), + Some(SyscallResult::Error(KernelError::NoReply)) )); } + +#[test] +fn a_reply_never_reaches_a_thread_that_reused_the_callers_id() { + let mut t = Ipc::new(); + let (ta, tb, b, b_recv) = (t.ta, t.tb, t.b, t.b_recv); + // A second client process C calls, then dies while B holds the reply. + let c = t.k.create_process_internal().unwrap(); + let tc = + t.k.spawn_thread_for_test(c, Priority::DEFAULT, TimeSlice::DEFAULT); + let c_send = + t.k.grant_cap_for_test(c, Resource::Endpoint(t.ep), perms(SEND)); + t.run( + tc, + Syscall::Call { + endpoint: c_send, + message: msg([0; 4]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + t.k.destroy_process(c); + + // A new thread takes C's thread id and is itself waiting for a reply + // from a different server, exactly the state B's stale reply targets. + let reused = + t.k.spawn_thread_for_test(t.a, Priority::DEFAULT, TimeSlice::DEFAULT); + assert_eq!(reused, tc); + let other_server = + t.k.spawn_thread_for_test(t.a, Priority::DEFAULT, TimeSlice::DEFAULT); + let ep2 = t.k.create_endpoint(t.a).unwrap(); + let call2 = + t.k.grant_cap_for_test(t.a, Resource::Endpoint(ep2), perms(SEND)); + let recv2 = + t.k.grant_cap_for_test(t.a, Resource::Endpoint(ep2), perms(RECV)); + t.run(other_server, Syscall::Receive { endpoint: recv2 }); + t.run( + reused, + Syscall::Call { + endpoint: call2, + message: msg([0; 4]), + }, + ); + assert!(matches!( + t.state(reused), + Some(ThreadState::Blocked( + domain::thread::BlockReason::AwaitingReply + )) + )); + + let out = t.run( + tb, + Syscall::Reply { + message: msg([66, 0, 0, 0]), + }, + ); + assert!( + error(&out).is_none(), + "replying to a dead caller is a harmless no-op" + ); + assert!(matches!(t.state(reused), Some(ThreadState::Blocked(_)))); + assert!(t.woken_with(reused).is_none()); + let _ = (ta, b); +} + +// ---- capability transfer ---- + +#[test] +fn a_capability_travels_with_the_message_and_the_sender_keeps_it() { + let mut t = Ipc::new(); + let (a, ta, tb, a_send, b_recv, b) = (t.a, t.ta, t.tb, t.a_send, t.b_recv, t.b); + let other = t.k.create_endpoint(a).unwrap(); + let gift = t.k.grant_cap_for_test( + a, + Resource::Endpoint(other), + perms(&[Permission::Execute, Permission::Delegate]), + ); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]).with_cap(gift), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + let slot = d.cap.expect("cap transferred"); + assert_eq!(t.k.endpoint_of_cap_for_test(b, slot), other); + assert_eq!( + t.k.endpoint_of_cap_for_test(a, gift), + other, + "sender keeps its copy" + ); +} + +#[test] +fn transfer_needs_delegate_on_the_endpoint_cap() { + let mut t = Ipc::new(); + let (a, ep, ta, tb, b_recv) = (t.a, t.ep, t.ta, t.tb, t.b_recv); + let no_grant = + t.k.grant_cap_for_test(a, Resource::Endpoint(ep), perms(SEND)); + let other = t.k.create_endpoint(a).unwrap(); + let gift = + t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND_GRANT)); + t.run( + ta, + Syscall::Send { + endpoint: no_grant, + message: msg([7, 0, 0, 0]).with_cap(gift), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.words[0], 7, "message still delivered"); + assert_eq!(d.cap, None); +} + +#[test] +fn transfer_needs_delegate_on_the_transferred_cap() { + let mut t = Ipc::new(); + let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv); + let other = t.k.create_endpoint(a).unwrap(); + let not_delegable = + t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND)); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]).with_cap(not_delegable), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.cap, None); +} + +#[test] +fn a_cap_revoked_while_the_sender_waits_is_not_delivered() { + let mut t = Ipc::new(); + let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv); + let other = t.k.create_endpoint(a).unwrap(); + let gift = + t.k.grant_cap_for_test(a, Resource::Endpoint(other), perms(SEND_GRANT)); + t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]).with_cap(gift), + }, + ); + t.k.revoke_resource(&Resource::Endpoint(other)); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.cap, None); +} + +// ---- notifications ---- + +fn notification(t: &mut Ipc, owner: ProcessId) -> NotificationId { + t.k.create_notification(owner).unwrap() +} + +#[test] +fn signal_then_wait_returns_the_bits() { + let mut t = Ipc::new(); + let (a, b, ta, tb) = (t.a, t.b, t.ta, t.tb); + let n = notification(&mut t, b); + let sig = + t.k.grant_cap_for_test(a, Resource::Notification(n), perms(&[Permission::Write])); + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + t.run(ta, Syscall::Signal { notification: sig }); + let d = delivered(&t.run(tb, Syscall::Wait { notification: wait })).unwrap(); + assert_eq!(d.kind, DeliveryKind::Signal); + assert_eq!(d.badge, 1); +} + +#[test] +fn wait_blocks_until_signalled() { + let mut t = Ipc::new(); + let (a, b, ta, tb) = (t.a, t.b, t.ta, t.tb); + let n = notification(&mut t, b); + let sig = + t.k.grant_cap_for_test(a, Resource::Notification(n), perms(&[Permission::Write])); + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + assert!(blocked(&t.run(tb, Syscall::Wait { notification: wait }))); + t.run(ta, Syscall::Signal { notification: sig }); + assert_eq!(pending_delivery(t.woken_with(tb)).unwrap().badge, 1); +} + +#[test] +fn badged_signals_accumulate_until_waited() { + let mut t = Ipc::new(); + let (a, b, ta, tb) = (t.a, t.b, t.ta, t.tb); + let n = notification(&mut t, a); + let base = t.k.grant_cap_for_test( + a, + Resource::Notification(n), + perms(&[Permission::Write, Permission::Delegate]), + ); + let mut sigs = [base; 2]; + for (i, bit) in [0b010usize, 0b100].iter().enumerate() { + sigs[i] = match t.run( + ta, + Syscall::Mint { + slot: base, + badge: Badge::new(*bit), + permissions: perms(&[Permission::Write]), + }, + ) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("mint"), + }; + } + for s in sigs { + t.run(ta, Syscall::Signal { notification: s }); + } + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + let d = delivered(&t.run(tb, Syscall::Wait { notification: wait })).unwrap(); + assert_eq!(d.badge, 0b110); +} + +#[test] +fn poll_never_blocks() { + let mut t = Ipc::new(); + let (b, tb) = (t.b, t.tb); + let n = notification(&mut t, b); + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + let out = t.run(tb, Syscall::Poll { notification: wait }); + assert_eq!(delivered(&out).unwrap().badge, 0); +} + +#[test] +fn signal_needs_write_and_wait_needs_read() { + let mut t = Ipc::new(); + let (b, tb) = (t.b, t.tb); + let n = notification(&mut t, b); + let read_only = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + let write_only = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Write])); + assert!( + error(&t.run( + tb, + Syscall::Signal { + notification: read_only + } + )) + .is_some() + ); + assert!( + error(&t.run( + tb, + Syscall::Wait { + notification: write_only + } + )) + .is_some() + ); +} + +#[test] +fn a_bound_notification_interrupts_a_receive() { + let mut t = Ipc::new(); + let (a, b, ta, tb, a_send, b_recv) = (t.a, t.b, t.ta, t.tb, t.a_send, t.b_recv); + let n = notification(&mut t, b); + let bind = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + let sig = + t.k.grant_cap_for_test(a, Resource::Notification(n), perms(&[Permission::Write])); + assert!(error(&t.run(tb, Syscall::BindNotification { notification: bind })).is_none()); + assert!(blocked(&t.run(tb, Syscall::Receive { endpoint: b_recv }))); + + t.run(ta, Syscall::Signal { notification: sig }); + let d = pending_delivery(t.woken_with(tb)).unwrap(); + assert_eq!(d.kind, DeliveryKind::Signal); + + // B is no longer queued on the endpoint: a send must now block. + assert!(blocked(&t.run( + ta, + Syscall::Send { + endpoint: a_send, + message: msg([0; 4]) + } + ))); +} + +#[test] +fn receive_returns_pending_bound_bits_first() { + let mut t = Ipc::new(); + let (a, b, ta, tb, b_recv) = (t.a, t.b, t.ta, t.tb, t.b_recv); + let n = notification(&mut t, b); + let bind = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + let sig = + t.k.grant_cap_for_test(a, Resource::Notification(n), perms(&[Permission::Write])); + t.run(tb, Syscall::BindNotification { notification: bind }); + t.run(ta, Syscall::Signal { notification: sig }); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.kind, DeliveryKind::Signal); +} + +#[test] +fn destroying_a_notification_wakes_its_waiters() { + let mut t = Ipc::new(); + let (a, b, tb) = (t.a, t.b, t.tb); + let n = notification(&mut t, a); + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Read])); + t.run(tb, Syscall::Wait { notification: wait }); + t.k.destroy_process_notifications_for_test(a); + assert!(matches!(t.state(tb), Some(ThreadState::Ready))); + assert!(matches!(t.woken_with(tb), Some(SyscallResult::Error(_)))); +} + +// ---- interrupts ---- + +#[test] +fn an_interrupt_signals_its_notification() { + let mut t = Ipc::new(); + let (b, tb) = (t.b, t.tb); + let irq = IrqId::from_raw(10); + let irq_cap = + t.k.grant_cap_for_test(b, Resource::Interrupt(irq), perms(&[Permission::Execute])); + let n = notification(&mut t, b); + let base = t.k.grant_cap_for_test( + b, + Resource::Notification(n), + perms(&[Permission::Read, Permission::Write, Permission::Delegate]), + ); + let badged = match t.run( + tb, + Syscall::Mint { + slot: base, + badge: Badge::new(0x40), + permissions: perms(&[Permission::Write]), + }, + ) { + SyscallOutcome::Complete(SyscallResult::Slot(s)) => s, + _ => panic!("mint"), + }; + assert!( + error(&t.run( + tb, + Syscall::SetIrqNotification { + irq: irq_cap, + notification: badged + } + )) + .is_none() + ); + assert!(blocked(&t.run(tb, Syscall::Wait { notification: base }))); + + t.k.handle_interrupt(irq); + assert_eq!(pending_delivery(t.woken_with(tb)).unwrap().badge, 0x40); +} + +#[test] +fn an_interrupt_routed_to_a_dead_notification_is_dropped() { + let mut t = Ipc::new(); + let (a, b, tb) = (t.a, t.b, t.tb); + let irq = IrqId::from_raw(11); + let irq_cap = + t.k.grant_cap_for_test(b, Resource::Interrupt(irq), perms(&[Permission::Execute])); + let n = notification(&mut t, a); + let sig = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Write])); + t.run( + tb, + Syscall::SetIrqNotification { + irq: irq_cap, + notification: sig, + }, + ); + t.k.destroy_process_notifications_for_test(a); + // A new notification reuses the slot; the stale route must not reach it. + let n2 = notification(&mut t, b); + assert_eq!(n2, n); + let wait = + t.k.grant_cap_for_test(b, Resource::Notification(n2), perms(&[Permission::Read])); + t.run(tb, Syscall::Wait { notification: wait }); + t.k.handle_interrupt(irq); + assert!(matches!(t.state(tb), Some(ThreadState::Blocked(_)))); +} + +#[test] +fn interrupts_beyond_the_routing_table_are_ignored() { + let mut t = Ipc::new(); + t.k.handle_interrupt(IrqId::from_raw(1000)); +} diff --git a/crates/kernel/src/tests/ipc_invariants.rs b/crates/kernel/src/tests/ipc_invariants.rs new file mode 100644 index 0000000..4cd5c40 --- /dev/null +++ b/crates/kernel/src/tests/ipc_invariants.rs @@ -0,0 +1,350 @@ +//! Randomized check of the IPC state machine. Six threads in three +//! processes send, call, receive, reply and signal at random over two +//! endpoints and a notification; one process is periodically killed and +//! re-created. After every step the kernel's queues, block reasons, reply +//! slots and run queue must agree, and no message may be duplicated. + +use super::*; +use crate::syscall::{Delivery, DeliveryKind}; +use crate::{SyscallOutcome, SyscallResult}; +use domain::message::Message; +use domain::resource::Resource; +use domain::syscall::Syscall; +use domain::thread::{BlockReason, ThreadState}; +use types::bounded::CapSlot; +use types::id::{EndpointId, NotificationId, ThreadId}; +use types::permission::{Permission, PermissionSet}; + +const STEPS: usize = 4000; +const THREADS_PER_PROC: usize = 2; +const PROCS: usize = 3; + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } +} + +fn all_perms() -> PermissionSet { + PermissionSet::empty() + .with(Permission::Read) + .with(Permission::Write) + .with(Permission::Execute) + .with(Permission::Delegate) +} + +struct Proc { + pid: ProcessId, + threads: [ThreadId; THREADS_PER_PROC], + eps: [CapSlot; 2], + ntfn: CapSlot, +} + +struct World { + k: Kernel, + procs: [Proc; PROCS], + eps: [EndpointId; 2], + ntfn: NotificationId, + next_id: usize, + delivered: [bool; 2 * STEPS + 16], + sent: [bool; 2 * STEPS + 16], + calls_answered: usize, + signals_seen: usize, + ran: usize, + rebirths: usize, +} + +impl World { + fn new() -> Self { + let (mut k, p0) = kernel_with_process(); + let t0 = k.current_thread().unwrap(); + let p1 = k.create_process_internal().unwrap(); + let p2 = k.create_process_internal().unwrap(); + // Endpoints and the notification belong to init, which never dies. + let eps = [ + k.create_endpoint(p0).unwrap(), + k.create_endpoint(p0).unwrap(), + ]; + let ntfn = k.create_notification(p0).unwrap(); + let mut procs = [p0, p1, p2].map(|pid| Proc { + pid, + threads: [t0; THREADS_PER_PROC], + eps: [CapSlot::new(0).unwrap(); 2], + ntfn: CapSlot::new(0).unwrap(), + }); + for (i, p) in procs.iter_mut().enumerate() { + Self::equip(&mut k, p, eps, ntfn, i == 0, t0); + } + Self { + k, + procs, + eps, + ntfn, + next_id: 1, + delivered: [false; 2 * STEPS + 16], + sent: [false; 2 * STEPS + 16], + calls_answered: 0, + signals_seen: 0, + ran: 0, + rebirths: 0, + } + } + + fn equip( + k: &mut Kernel, + p: &mut Proc, + eps: [EndpointId; 2], + ntfn: NotificationId, + is_init: bool, + init_thread: ThreadId, + ) { + for j in 0..THREADS_PER_PROC { + p.threads[j] = if is_init && j == 0 { + init_thread + } else { + k.spawn_thread_for_test(p.pid, Priority::DEFAULT, TimeSlice::DEFAULT) + }; + } + for (e, ep) in eps.iter().enumerate() { + p.eps[e] = k.grant_cap_for_test(p.pid, Resource::Endpoint(*ep), all_perms()); + } + p.ntfn = k.grant_cap_for_test(p.pid, Resource::Notification(ntfn), all_perms()); + // The second thread of every process gets the notification bound. + k.switch_to_thread_for_test(p.threads[1]); + let _ = k.handle_syscall(Syscall::BindNotification { + notification: p.ntfn, + }); + } + + fn fresh_id(&mut self) -> usize { + let id = self.next_id; + self.next_id += 1; + self.sent[id] = true; + id + } + + fn record(&mut self, d: &Delivery) { + match d.kind { + DeliveryKind::Signal => self.signals_seen += 1, + _ => { + let id = d.words[0]; + assert!( + id < self.sent.len() && self.sent[id], + "delivered id {id} was never sent" + ); + assert!(!self.delivered[id], "message {id} delivered twice"); + self.delivered[id] = true; + } + } + } + + fn record_outcome(&mut self, outcome: &SyscallOutcome) { + if let SyscallOutcome::Complete(SyscallResult::Received(d)) = outcome { + let d = *d; + self.record(&d); + } + } + + fn runnable(&self) -> impl Iterator + '_ { + self.procs.iter().enumerate().flat_map(move |(pi, p)| { + p.threads + .into_iter() + .filter(|&t| { + matches!( + self.k.thread_state_for_test(t), + Some(ThreadState::Ready | ThreadState::Running(_)) + ) + }) + .map(move |t| (pi, t)) + }) + } + + /// Kills process `pi` and brings it back with fresh threads and caps. + fn reincarnate(&mut self, pi: usize) { + self.k.destroy_process(self.procs[pi].pid); + self.procs[pi].pid = self.k.create_process_internal().unwrap(); + let t0 = self.procs[0].threads[0]; + Self::equip( + &mut self.k, + &mut self.procs[pi], + self.eps, + self.ntfn, + false, + t0, + ); + self.rebirths += 1; + } + + fn step(&mut self, rng: &mut Rng) { + let count = self.runnable().count(); + if count == 0 || rng.below(100) == 0 { + // Random clients deadlock; killing someone is what breaks it. + self.reincarnate(1 + rng.below(PROCS - 1)); + return; + } + let pick = rng.below(count); + let (pi, tid) = self.runnable().nth(pick).unwrap(); + self.ran += 1; + self.k.switch_to_thread_for_test(tid); + // What the adapter would hand the thread when it resumes. + if let Some(SyscallResult::Received(d)) = self.k.take_pending_result(tid) { + self.record(&d); + } + let ep = self.procs[pi].eps[rng.below(2)]; + let ntfn = self.procs[pi].ntfn; + + let call = match rng.below(9) { + 0 => Syscall::Send { + endpoint: ep, + message: Message::from_words([self.fresh_id(), 0, 0, 0]), + }, + 1 | 2 => Syscall::Call { + endpoint: ep, + message: Message::from_words([self.fresh_id(), 1, 0, 0]), + }, + 3 | 4 => Syscall::Receive { endpoint: ep }, + 5 => Syscall::Reply { + message: Message::from_words([self.fresh_id(), 2, 0, 0]), + }, + 6 => Syscall::ReplyRecv { + endpoint: ep, + message: Message::from_words([self.fresh_id(), 2, 0, 0]), + }, + 7 => Syscall::Signal { notification: ntfn }, + _ => match rng.below(2) { + 0 => Syscall::Wait { notification: ntfn }, + _ => Syscall::Poll { notification: ntfn }, + }, + }; + let is_reply = matches!(call, Syscall::Reply { .. } | Syscall::ReplyRecv { .. }); + let had_reply = self.k.ipc[tid.raw() as usize].reply_to.is_some(); + let outcome = self.k.handle_syscall(call); + if is_reply && had_reply { + self.calls_answered += 1; + } + self.record_outcome(&outcome); + } + + fn check(&self) { + let k = &self.k; + for p in &self.procs { + for &t in &p.threads { + let Some(state) = k.thread_state_for_test(t) else { + continue; + }; + let ipc = &k.ipc[t.raw() as usize]; + let queued_on = |e: EndpointId| { + let ep = k.endpoints.get(e).unwrap(); + (ep.is_sender(t), ep.is_receiver(t)) + }; + let in_any_queue = self.eps.iter().any(|&e| { + let (s, r) = queued_on(e); + s || r + }) || k.notifications.get(self.ntfn).unwrap().is_waiter(t); + + match state { + ThreadState::Blocked(BlockReason::Sending(e) | BlockReason::Calling(e)) => { + assert_eq!( + queued_on(e), + (true, false), + "{t:?} blocked sending but not queued" + ); + assert!( + ipc.outgoing.is_some(), + "{t:?} queued sender without a message" + ); + } + ThreadState::Blocked(BlockReason::Receiving(e)) => { + assert_eq!( + queued_on(e), + (false, true), + "{t:?} blocked receiving but not queued" + ); + } + ThreadState::Blocked(BlockReason::WaitingNotification(n)) => { + assert!(k.notifications.get(n).unwrap().is_waiter(t)); + } + ThreadState::Blocked(BlockReason::AwaitingReply) => { + assert!(!in_any_queue, "{t:?} awaiting reply but still queued"); + let me = crate::syscall::ThreadRef { + tid: t, + generation: k.threads.generation(t).unwrap(), + }; + let holders = self + .procs + .iter() + .flat_map(|p| p.threads) + .filter(|&s| k.thread_state_for_test(s).is_some()) + .filter(|s| k.ipc[s.raw() as usize].reply_to == Some(me)) + .count(); + assert_eq!(holders, 1, "{t:?} awaits a reply nobody can send"); + } + ThreadState::Ready | ThreadState::Running(_) | ThreadState::Dead => { + assert!(!in_any_queue, "{t:?} runnable but queued on an IPC object"); + assert!(ipc.outgoing.is_none()); + } + } + let blocked = matches!(state, ThreadState::Blocked(_)); + assert_eq!( + k.scheduler.is_queued(t), + matches!(state, ThreadState::Ready), + "{t:?} in state {state:?} has wrong run-queue membership" + ); + if blocked { + assert_ne!(k.current_thread(), Some(t)); + } + + // A live reply slot always points at a thread waiting for it. + if let Some(r) = ipc.reply_to + && k.threads.generation(r.tid) == Some(r.generation) + { + assert_eq!( + k.thread_state_for_test(r.tid), + Some(ThreadState::Blocked(BlockReason::AwaitingReply)), + "{t:?} holds a reply for {:?} which is not waiting", + r.tid + ); + } + } + } + for &e in &self.eps { + let ep = k.endpoints.get(e).unwrap(); + assert!(!(ep.has_waiting_sender() && ep.has_waiting_receiver())); + } + } +} + +#[test] +fn ipc_state_machine_holds_under_random_operations() { + for seed in [3u64, 0xC0FFEE, 0xFEED_FACE, 99, 123_456_789] { + let mut world = World::new(); + let mut rng = Rng(seed); + for _ in 0..STEPS { + world.step(&mut rng); + world.check(); + } + assert!( + world.calls_answered > 20, + "seed {seed:#x}: too few replies: answered={} signals={} delivered={} ran={}", + world.calls_answered, + world.signals_seen, + world.delivered.iter().filter(|d| **d).count(), + world.ran + ); + assert!( + world.rebirths > 5, + "seed {seed:#x}: kill/re-create barely exercised" + ); + assert!(world.signals_seen > 20, "seed {seed:#x}: too few signals"); + assert!(world.delivered.iter().filter(|d| **d).count() > 200); + } +} diff --git a/crates/kernel/src/tests/lifetimes.rs b/crates/kernel/src/tests/lifetimes.rs index a487a51..27040be 100644 --- a/crates/kernel/src/tests/lifetimes.rs +++ b/crates/kernel/src/tests/lifetimes.rs @@ -6,10 +6,10 @@ use crate::{SyscallOutcome, SyscallResult}; use domain::message::Message; use domain::resource::Resource; use domain::syscall::Syscall; -use domain::thread::{BlockReason, ThreadState}; +use domain::thread::ThreadState; use types::address::DataRef; use types::bounded::CapSlot; -use types::id::{IrqId, ThreadId}; +use types::id::ThreadId; use types::permission::{Permission, PermissionSet}; fn create_process(k: &mut Kernel) -> CapSlot { @@ -177,57 +177,6 @@ fn endpoint_slots_are_reclaimed_when_processes_die() { } } -#[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(); @@ -253,3 +202,43 @@ fn killing_yourself_terminates_the_caller() { 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" + ); +} diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index 11de71d..b62466a 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -5,6 +5,7 @@ mod capability; mod exit; mod invariants; mod ipc; +mod ipc_invariants; mod isolation; mod lifecycle; mod lifetimes; diff --git a/crates/kernel/src/tests/tick.rs b/crates/kernel/src/tests/tick.rs index aff9720..69220fd 100644 --- a/crates/kernel/src/tests/tick.rs +++ b/crates/kernel/src/tests/tick.rs @@ -1,6 +1,6 @@ use super::*; use domain::scheduler::SchedulerDecision; -use types::id::{CoreId, IrqId, ThreadId}; +use types::id::CoreId; use types::time::TimeSlice; #[test] @@ -30,26 +30,3 @@ fn tick_preempts_when_time_expired_and_other_thread_ready() { let d2 = k.handle_tick(CoreId::from_raw(0)); assert!(matches!(d2, SchedulerDecision::Preempt(_))); } - -#[test] -fn interrupt_unknown_irq_returns_none() { - let (mut k, _) = kernel_with_process(); - assert!(k.handle_interrupt(IrqId::from_raw(99)).is_none()); -} - -#[test] -fn interrupt_unblocks_registered_thread() { - let (mut k, _) = kernel_with_process(); - let tid = ThreadId::from_raw(0); - let irq = IrqId::from_raw(5); - - // Register and block the thread on this IRQ - k.register_irq_for_test(irq, tid); - k.block_thread_for_test( - tid, - domain::thread::BlockReason::WaitingForInterrupt(types::bounded::CapSlot::new(0).unwrap()), - ); - - let result = k.handle_interrupt(irq); - assert_eq!(result, Some(tid)); -} diff --git a/crates/libkos/src/alloc_impl.rs b/crates/libkos/src/alloc_impl.rs index db98bac..8c16124 100644 --- a/crates/libkos/src/alloc_impl.rs +++ b/crates/libkos/src/alloc_impl.rs @@ -80,13 +80,17 @@ impl AllocatorInner { if prev.is_null() { self.free_list = new_block; } else { - unsafe { (*prev).next = new_block; } + unsafe { + (*prev).next = new_block; + } } } else { if prev.is_null() { self.free_list = block.next; } else { - unsafe { (*prev).next = block.next; } + unsafe { + (*prev).next = block.next; + } } } diff --git a/crates/libkos/src/arch/mock.rs b/crates/libkos/src/arch/mock.rs index 4aef986..6eea7e1 100644 --- a/crates/libkos/src/arch/mock.rs +++ b/crates/libkos/src/arch/mock.rs @@ -1,27 +1,4 @@ -pub fn syscall0(_nr: usize) -> usize { - 0 -} - -pub fn syscall1(_nr: usize, _a0: usize) -> usize { - 0 -} - -pub fn syscall2(_nr: usize, _a0: usize, _a1: usize) -> usize { - 0 -} - -pub fn syscall3(_nr: usize, _a0: usize, _a1: usize, _a2: usize) -> usize { - 0 -} - -pub fn syscall4(_nr: usize, _a0: usize, _a1: usize, _a2: usize, _a3: usize) -> usize { - 0 -} - -pub fn syscall5(_nr: usize, _a0: usize, _a1: usize, _a2: usize, _a3: usize, _a4: usize) -> usize { - 0 -} - -pub fn syscall_receive(_nr: usize, _a0: usize) -> (usize, usize, usize, usize) { - (0, 0, 0, 0) +/// Host builds have no kernel; every call reports success with zeroes. +pub fn syscall(_nr: usize, _args: [usize; 6]) -> [usize; 8] { + [0; 8] } diff --git a/crates/libkos/src/arch/riscv.rs b/crates/libkos/src/arch/riscv.rs index 60ba1b2..cac01a7 100644 --- a/crates/libkos/src/arch/riscv.rs +++ b/crates/libkos/src/arch/riscv.rs @@ -1,106 +1,20 @@ +/// One `ecall`: syscall number in `a7`, arguments in `a0..a5`. Returns the +/// eight result registers `a0..a7` (docs/ipc.md, register ABI). #[inline(always)] -pub fn syscall0(nr: usize) -> usize { - let ret: usize; +pub fn syscall(nr: usize, args: [usize; 6]) -> [usize; 8] { + let mut r = [0usize; 8]; unsafe { core::arch::asm!( "ecall", - in("a7") nr, - lateout("a0") ret, + inlateout("a0") args[0] => r[0], + inlateout("a1") args[1] => r[1], + inlateout("a2") args[2] => r[2], + inlateout("a3") args[3] => r[3], + inlateout("a4") args[4] => r[4], + inlateout("a5") args[5] => r[5], + lateout("a6") r[6], + inlateout("a7") nr => r[7], ); } - ret -} - -#[inline(always)] -pub fn syscall1(nr: usize, a0: usize) -> usize { - let ret: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => ret, - ); - } - ret -} - -#[inline(always)] -pub fn syscall2(nr: usize, a0: usize, a1: usize) -> usize { - let ret: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => ret, - in("a1") a1, - ); - } - ret -} - -#[inline(always)] -pub fn syscall3(nr: usize, a0: usize, a1: usize, a2: usize) -> usize { - let ret: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => ret, - in("a1") a1, - in("a2") a2, - ); - } - ret -} - -#[inline(always)] -pub fn syscall4(nr: usize, a0: usize, a1: usize, a2: usize, a3: usize) -> usize { - let ret: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => ret, - in("a1") a1, - in("a2") a2, - in("a3") a3, - ); - } - ret -} - -#[inline(always)] -pub fn syscall5(nr: usize, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) -> usize { - let ret: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => ret, - in("a1") a1, - in("a2") a2, - in("a3") a3, - in("a4") a4, - ); - } - ret -} - -#[inline(always)] -pub fn syscall_receive(nr: usize, a0: usize) -> (usize, usize, usize, usize) { - let r0: usize; - let r1: usize; - let r2: usize; - let r3: usize; - unsafe { - core::arch::asm!( - "ecall", - in("a7") nr, - inlateout("a0") a0 => r0, - lateout("a1") r1, - lateout("a2") r2, - lateout("a3") r3, - ); - } - (r0, r1, r2, r3) + r } diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 1230f98..6f827f0 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -1,13 +1,20 @@ +use crate::syscall; use types::bounded::CapSlot; use types::display::{Color, Dimension, ScreenCoord}; -use crate::syscall; fn ipc(ep: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> (usize, usize, usize, usize) { syscall::send(ep, w0, w1, w2, w3); syscall::receive(ep) } -pub fn draw_rect(ep: CapSlot, x: ScreenCoord, y: ScreenCoord, w: Dimension, h: Dimension, color: Color) { +pub fn draw_rect( + ep: CapSlot, + x: ScreenCoord, + y: ScreenCoord, + w: Dimension, + h: Dimension, + color: Color, +) { let xy = (x.raw() as usize) | ((y.raw() as usize) << 16); let wh = (w.raw() as usize) | ((h.raw() as usize) << 16); ipc(ep, 0, xy, wh, color.raw() as usize); diff --git a/crates/libkos/src/fs.rs b/crates/libkos/src/fs.rs index f8d08f7..c0dd432 100644 --- a/crates/libkos/src/fs.rs +++ b/crates/libkos/src/fs.rs @@ -1,19 +1,24 @@ -use types::bounded::CapSlot; use crate::syscall; +use types::bounded::CapSlot; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FileHandle(u16); impl FileHandle { - pub const fn raw(&self) -> u16 { self.0 } + pub const fn raw(&self) -> u16 { + self.0 + } } fn pack_path(path: &[u8]) -> (usize, usize) { let mut w0 = [0u8; 8]; let mut w1 = [0u8; 8]; for (i, &b) in path.iter().enumerate() { - if i < 8 { w0[i] = b; } - else if i < 16 { w1[i - 8] = b; } + if i < 8 { + w0[i] = b; + } else if i < 16 { + w1[i - 8] = b; + } } (usize::from_le_bytes(w0), usize::from_le_bytes(w1)) } @@ -26,7 +31,11 @@ fn ipc(ep: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> (usize, usize pub fn open(ep: CapSlot, path: &[u8]) -> Option { let (p0, p1) = pack_path(path); let (tag, handle, _, _) = ipc(ep, 0, p0, p1, 0); - if tag == 0 { Some(FileHandle(handle as u16)) } else { None } + if tag == 0 { + Some(FileHandle(handle as u16)) + } else { + None + } } pub fn read(ep: CapSlot, handle: FileHandle, max_bytes: usize) -> Option { @@ -52,7 +61,11 @@ pub fn list_dir(ep: CapSlot, path: &[u8]) -> Option { pub fn create(ep: CapSlot, path: &[u8]) -> Option { let (p0, p1) = pack_path(path); let (tag, handle, _, _) = ipc(ep, 5, p0, p1, 0); - if tag == 0 { Some(FileHandle(handle as u16)) } else { None } + if tag == 0 { + Some(FileHandle(handle as u16)) + } else { + None + } } pub fn delete(ep: CapSlot, path: &[u8]) -> bool { diff --git a/crates/libkos/src/input.rs b/crates/libkos/src/input.rs index d6ff72c..64e759f 100644 --- a/crates/libkos/src/input.rs +++ b/crates/libkos/src/input.rs @@ -1,5 +1,5 @@ -use types::bounded::CapSlot; use crate::syscall; +use types::bounded::CapSlot; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Key { diff --git a/crates/libkos/src/lib.rs b/crates/libkos/src/lib.rs index 29c8614..62b483e 100644 --- a/crates/libkos/src/lib.rs +++ b/crates/libkos/src/lib.rs @@ -2,14 +2,14 @@ extern crate alloc; -mod arch; -pub mod syscall; pub mod alloc_impl; -pub mod io; -pub mod fs; +mod arch; pub mod display; +pub mod fs; pub mod input; +pub mod io; mod rt; +pub mod syscall; #[cfg(target_os = "none")] #[global_allocator] diff --git a/crates/libkos/src/syscall.rs b/crates/libkos/src/syscall.rs index 9d7b657..9dcfb27 100644 --- a/crates/libkos/src/syscall.rs +++ b/crates/libkos/src/syscall.rs @@ -1,90 +1,335 @@ +//! Thin wrappers over the kernel's register ABI (docs/ipc.md). Every call +//! returns a status in `a0`; results follow in `a1..`. + use crate::arch; -use types::syscall_abi::*; use types::address::{EntryPoint, StackPointer, VirtualAddress}; +use types::badge::Badge; use types::bounded::{CapSlot, PageCount}; use types::id::ProcessId; use types::permission::PermissionSet; +use types::syscall_abi::*; use types::time::Ticks; +pub use types::syscall_abi::ErrorCode as Error; + +pub type Result = core::result::Result; + +fn raw(nr: usize, args: [usize; 6]) -> Result<[usize; 8]> { + let r = arch::syscall(nr, args); + match r[0] { + 0 => Ok(r), + code => Err(Error::from_raw(code).unwrap_or(Error::InvalidArgument)), + } +} + +fn call0(nr: usize) -> Result<[usize; 8]> { + raw(nr, [0; 6]) +} + +fn slot_arg(slot: Option) -> usize { + slot.map_or(NONE, |s| s.value() as usize) +} + +fn slot_result(r: usize) -> Option { + if r == NONE { + None + } else { + CapSlot::new(r as u8) + } +} + +fn slot_of(r: Result<[usize; 8]>) -> Result { + r.and_then(|r| slot_result(r[1]).ok_or(Error::InvalidArgument)) +} + +/// What a receiving call got. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Received { + /// A `Send`. `badge` identifies the sender's capability. + Message { + badge: usize, + words: [usize; 4], + cap: Option, + }, + /// A `Call`; answer it with `reply` or `reply_recv`. + Call { + badge: usize, + words: [usize; 4], + cap: Option, + }, + /// Bits from a notification. + Signal { bits: usize }, +} + +impl Received { + fn decode(r: [usize; 8]) -> Self { + let words = [r[2], r[3], r[4], r[5]]; + let cap = slot_result(r[6]); + match r[7] { + KIND_CALL => Received::Call { + badge: r[1], + words, + cap, + }, + KIND_SIGNAL => Received::Signal { bits: r[1] }, + _ => Received::Message { + badge: r[1], + words, + cap, + }, + } + } + + pub fn words(&self) -> [usize; 4] { + match self { + Received::Message { words, .. } | Received::Call { words, .. } => *words, + Received::Signal { .. } => [0; 4], + } + } +} + +fn message_args(ep: usize, words: [usize; 4], cap: Option) -> [usize; 6] { + [ep, words[0], words[1], words[2], words[3], slot_arg(cap)] +} + +// ---- threads, processes, time ---- + pub fn exit() -> ! { - arch::syscall0(SYS_EXIT); + let _ = call0(SYS_EXIT); loop {} } pub fn exit_thread() -> ! { - arch::syscall0(SYS_EXIT_THREAD); + let _ = call0(SYS_EXIT_THREAD); loop {} } pub fn yield_now() { - arch::syscall0(SYS_YIELD); + let _ = call0(SYS_YIELD); } pub fn halt() -> ! { - arch::syscall0(SYS_HALT); + let _ = call0(SYS_HALT); loop {} } pub fn get_time() -> Ticks { - Ticks::from_raw(arch::syscall0(SYS_GET_TIME) as u64) + Ticks::from_raw(call0(SYS_GET_TIME).map_or(0, |r| r[1]) as u64) } pub fn get_process_id() -> ProcessId { - ProcessId::from_raw(arch::syscall0(SYS_GET_PROCESS_ID) as u16) + ProcessId::from_raw(call0(SYS_GET_PROCESS_ID).map_or(0, |r| r[1]) as u16) } pub fn create_process(create_cap: CapSlot) -> Option { - let r = arch::syscall1(SYS_CREATE_PROCESS, create_cap.value() as usize); - if r == usize::MAX { None } else { CapSlot::new(r as u8) } -} - -pub fn create_endpoint() -> Option { - let r = arch::syscall0(SYS_CREATE_ENDPOINT); - if r == usize::MAX { None } else { CapSlot::new(r as u8) } + slot_of(raw( + SYS_CREATE_PROCESS, + [create_cap.value() as usize, 0, 0, 0, 0, 0], + )) + .ok() } pub fn spawn_thread(entry: EntryPoint, stack: StackPointer) -> usize { - arch::syscall2(SYS_SPAWN_THREAD, entry.raw(), stack.raw()) + raw(SYS_SPAWN_THREAD, [entry.raw(), stack.raw(), 0, 0, 0, 0]).map_or(NONE, |r| r[1]) } pub fn spawn_thread_in(process_cap: CapSlot, entry: EntryPoint, stack: StackPointer) -> usize { - arch::syscall3(SYS_SPAWN_THREAD_IN, process_cap.value() as usize, entry.raw(), stack.raw()) -} - -pub fn request_pages(count: PageCount) -> Option { - let r = arch::syscall1(SYS_REQUEST_PAGES, count.value() as usize); - if r == usize::MAX { None } else { Some(VirtualAddress::from_raw(r)) } -} - -pub fn map_page(process_cap: CapSlot, src_va: VirtualAddress, dst_va: VirtualAddress, executable: bool) -> bool { - arch::syscall4(SYS_MAP_PAGE, process_cap.value() as usize, src_va.raw(), dst_va.raw(), executable as usize) == 0 -} - -pub fn send(endpoint: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> usize { - arch::syscall5(SYS_SEND, endpoint.value() as usize, w0, w1, w2, w3) -} - -pub fn receive(endpoint: CapSlot) -> (usize, usize, usize, usize) { - arch::syscall_receive(SYS_RECEIVE, endpoint.value() as usize) -} - -pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool { - arch::syscall3(SYS_DELEGATE, slot.value() as usize, to.value() as usize, permissions.bits() as usize) == 0 + raw( + SYS_SPAWN_THREAD_IN, + [ + process_cap.value() as usize, + entry.raw(), + stack.raw(), + 0, + 0, + 0, + ], + ) + .map_or(NONE, |r| r[1]) } pub fn kill(process_cap: CapSlot) -> bool { - arch::syscall1(SYS_KILL, process_cap.value() as usize) == 0 + raw(SYS_KILL, [process_cap.value() as usize, 0, 0, 0, 0, 0]).is_ok() +} + +// ---- memory ---- + +pub fn request_pages(count: PageCount) -> Option { + raw(SYS_REQUEST_PAGES, [count.value() as usize, 0, 0, 0, 0, 0]) + .ok() + .map(|r| VirtualAddress::from_raw(r[1])) +} + +pub fn map_page( + process_cap: CapSlot, + src_va: VirtualAddress, + dst_va: VirtualAddress, + executable: bool, +) -> bool { + raw( + SYS_MAP_PAGE, + [ + process_cap.value() as usize, + src_va.raw(), + dst_va.raw(), + executable as usize, + 0, + 0, + ], + ) + .is_ok() +} + +// ---- capabilities ---- + +pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool { + raw( + SYS_DELEGATE, + [ + slot.value() as usize, + to.value() as usize, + permissions.bits() as usize, + 0, + 0, + 0, + ], + ) + .is_ok() +} + +/// Copies an unbadged endpoint or notification capability with `badge` +/// and at most `permissions`. +pub fn mint(slot: CapSlot, badge: Badge, permissions: PermissionSet) -> Result { + slot_of(raw( + SYS_MINT, + [ + slot.value() as usize, + badge.raw(), + permissions.bits() as usize, + 0, + 0, + 0, + ], + )) } pub fn revoke(slot: CapSlot) -> bool { - arch::syscall1(SYS_REVOKE, slot.value() as usize) == 0 + raw(SYS_REVOKE, [slot.value() as usize, 0, 0, 0, 0, 0]).is_ok() } /// Removes a capability from this process's own table, freeing the slot. pub fn drop_cap(slot: CapSlot) -> bool { - arch::syscall1(SYS_DROP_CAP, slot.value() as usize) == 0 + raw(SYS_DROP_CAP, [slot.value() as usize, 0, 0, 0, 0, 0]).is_ok() +} + +// ---- endpoints ---- + +pub fn create_endpoint() -> Option { + slot_of(call0(SYS_CREATE_ENDPOINT)).ok() +} + +/// Sends four words; returns 0 on success (kept for existing callers). +pub fn send(endpoint: CapSlot, w0: usize, w1: usize, w2: usize, w3: usize) -> usize { + match send_msg(endpoint, [w0, w1, w2, w3], None) { + Ok(()) => 0, + Err(e) => e as usize, + } +} + +pub fn send_msg(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result<()> { + raw( + SYS_SEND, + message_args(endpoint.value() as usize, words, cap), + ) + .map(|_| ()) +} + +/// Receives and returns just the words (kept for existing callers). +pub fn receive(endpoint: CapSlot) -> (usize, usize, usize, usize) { + let w = receive_msg(endpoint).map_or([0; 4], |r| r.words()); + (w[0], w[1], w[2], w[3]) +} + +pub fn receive_msg(endpoint: CapSlot) -> Result { + raw(SYS_RECEIVE, [endpoint.value() as usize, 0, 0, 0, 0, 0]).map(Received::decode) +} + +/// Sends and waits for the reply. +pub fn call(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { + raw( + SYS_CALL, + message_args(endpoint.value() as usize, words, cap), + ) + .map(Received::decode) +} + +/// Answers the last `Call` this thread received. +pub fn reply(words: [usize; 4], cap: Option) -> Result<()> { + raw(SYS_REPLY, message_args(0, words, cap)).map(|_| ()) +} + +/// Replies to the pending `Call` (if any), then receives the next message. +pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { + raw( + SYS_REPLY_RECV, + message_args(endpoint.value() as usize, words, cap), + ) + .map(Received::decode) +} + +// ---- notifications and interrupts ---- + +pub fn create_notification() -> Result { + slot_of(call0(SYS_CREATE_NOTIFICATION)) +} + +pub fn signal(notification: CapSlot) -> Result<()> { + raw(SYS_SIGNAL, [notification.value() as usize, 0, 0, 0, 0, 0]).map(|_| ()) +} + +/// Blocks until the notification has bits, then returns and clears them. +pub fn wait(notification: CapSlot) -> Result { + raw(SYS_WAIT, [notification.value() as usize, 0, 0, 0, 0, 0]).map(|r| r[1]) +} + +/// Returns and clears the bits without blocking. +pub fn poll(notification: CapSlot) -> Result { + raw(SYS_POLL, [notification.value() as usize, 0, 0, 0, 0, 0]).map(|r| r[1]) +} + +/// After this, `receive` also returns when the notification is signalled. +pub fn bind_notification(notification: CapSlot) -> Result<()> { + raw( + SYS_BIND_NOTIFICATION, + [notification.value() as usize, 0, 0, 0, 0, 0], + ) + .map(|_| ()) +} + +/// Delivers interrupt `irq` as a signal on `notification`. +pub fn set_irq_notification(irq: CapSlot, notification: CapSlot) -> Result<()> { + raw( + SYS_SET_IRQ_NOTIFICATION, + [ + irq.value() as usize, + notification.value() as usize, + 0, + 0, + 0, + 0, + ], + ) + .map(|_| ()) +} + +pub fn ack_interrupt(irq: CapSlot) -> Result<()> { + raw( + SYS_ACKNOWLEDGE_INTERRUPT, + [irq.value() as usize, 0, 0, 0, 0, 0], + ) + .map(|_| ()) } pub fn debug_putchar(byte: u8) { - arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize); + let _ = raw(SYS_DEBUG_PUTCHAR, [byte as usize, 0, 0, 0, 0, 0]); } diff --git a/crates/types/src/badge.rs b/crates/types/src/badge.rs new file mode 100644 index 0000000..3f32b26 --- /dev/null +++ b/crates/types/src/badge.rs @@ -0,0 +1,21 @@ +/// A word stamped on an endpoint or notification capability by `Mint`. +/// Receivers see the badge of the capability a sender used, which is how a +/// server tells its clients apart. Zero means "unbadged". +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Badge(usize); + +impl Badge { + pub const NONE: Self = Self(0); + + pub const fn new(value: usize) -> Self { + Self(value) + } + + pub const fn raw(&self) -> usize { + self.0 + } + + pub const fn is_none(&self) -> bool { + self.0 == 0 + } +} diff --git a/crates/types/src/id.rs b/crates/types/src/id.rs index d93e6b9..7562407 100644 --- a/crates/types/src/id.rs +++ b/crates/types/src/id.rs @@ -4,5 +4,6 @@ kernel_id!(ProcessId, u16); kernel_id!(ThreadId, u16); kernel_id!(EndpointId, u16); kernel_id!(RegionId, u16); +kernel_id!(NotificationId, u16); kernel_id!(IrqId, u16); kernel_id!(CoreId, u8); diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 2009857..7d4daca 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] pub mod address; +pub mod badge; pub mod bounded; pub mod display; pub mod generation; diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index b2f9bb0..aa6f9f4 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -1,9 +1,12 @@ +//! Register-level syscall contract shared by the kernel and libkos. +//! See docs/ipc.md for the meaning of each call. + pub const SYS_SEND: usize = 0; pub const SYS_RECEIVE: usize = 1; pub const SYS_DELEGATE: usize = 2; -pub const SYS_WAIT_FOR_INTERRUPT: usize = 3; +// 3 was WaitForInterrupt; interrupts are delivered through notifications. pub const SYS_ACKNOWLEDGE_INTERRUPT: usize = 4; -pub const SYS_WAIT_ANY: usize = 5; +// 5 was WaitAny; superseded by notifications. pub const SYS_CREATE_PROCESS: usize = 6; pub const SYS_KILL: usize = 7; pub const SYS_REVOKE: usize = 8; @@ -21,3 +24,63 @@ pub const SYS_DEBUG_PUTCHAR: usize = 19; pub const SYS_MAP_PAGE: usize = 20; pub const SYS_SPAWN_THREAD_IN: usize = 21; pub const SYS_DROP_CAP: usize = 22; +pub const SYS_CALL: usize = 23; +pub const SYS_REPLY: usize = 24; +pub const SYS_REPLY_RECV: usize = 25; +pub const SYS_MINT: usize = 26; +pub const SYS_CREATE_NOTIFICATION: usize = 27; +pub const SYS_SIGNAL: usize = 28; +pub const SYS_WAIT: usize = 29; +pub const SYS_POLL: usize = 30; +pub const SYS_BIND_NOTIFICATION: usize = 31; +pub const SYS_SET_IRQ_NOTIFICATION: usize = 32; + +/// "No capability" in a capability argument or result register. +pub const NONE: usize = usize::MAX; + +/// What a receiving syscall got, returned in `a7`. +pub const KIND_MESSAGE: usize = 0; +pub const KIND_CALL: usize = 1; +pub const KIND_SIGNAL: usize = 2; + +/// Status returned in `a0`; `0` is success. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(usize)] +pub enum ErrorCode { + /// No capability in that slot, or it names a dead or revoked object. + InvalidCap = 1, + /// The capability lacks the permission the operation needs. + PermissionDenied = 2, + /// The capability names the wrong kind of object. + WrongObject = 3, + NoFreeSlot = 4, + OutOfMemory = 5, + InvalidAddress = 6, + AddressInUse = 7, + InvalidArgument = 8, + /// `Reply` with nothing to reply to, or a call whose reply was dropped. + NoReply = 9, + /// A table (threads, processes, queue) is full. + LimitReached = 10, + UnknownSyscall = 11, +} + +impl ErrorCode { + pub fn from_raw(code: usize) -> Option { + use ErrorCode::*; + Some(match code { + 1 => InvalidCap, + 2 => PermissionDenied, + 3 => WrongObject, + 4 => NoFreeSlot, + 5 => OutOfMemory, + 6 => InvalidAddress, + 7 => AddressInUse, + 8 => InvalidArgument, + 9 => NoReply, + 10 => LimitReached, + 11 => UnknownSyscall, + _ => return None, + }) + } +} diff --git a/docs/ipc.md b/docs/ipc.md new file mode 100644 index 0000000..9083f7a --- /dev/null +++ b/docs/ipc.md @@ -0,0 +1,105 @@ +# IPC + +Synchronous message passing modelled on classic (non-MCS) seL4: endpoints +for rendezvous messages, notifications for asynchronous signals, an +implicit per-thread reply slot for `Call`. This document is the contract the +kernel tests and proofs check. + +## Objects + +**Endpoint.** A rendezvous point. Holds a FIFO of blocked senders and a +FIFO of blocked receivers; never both non-empty. Owned by its creator and +destroyed with it. + +**Notification.** A word of signal bits plus a FIFO of blocked waiters and +at most one bound thread. Owned by its creator and destroyed with it. + +Both are named by capabilities whose generation must match (see P2). + +## Capabilities + +A capability to an endpoint or notification carries a **badge** (a word; +`0` means unbadged). `Mint` copies an unbadged capability with a badge and +narrower permissions. A badge cannot be changed once set. + +| Object | Permission | Allows | +|--------------|------------|--------------------------------------------| +| Endpoint | Execute | `Send`, `Call` | +| Endpoint | Read | `Receive`, `ReplyRecv` | +| Endpoint | Delegate | transfer a capability through it; `Mint` | +| Notification | Write | `Signal` | +| Notification | Read | `Wait`, `Poll`, `BindNotification` | +| Notification | Delegate | `Mint` | +| Interrupt | Execute | `SetIrqNotification`, `AckInterrupt` | + +## Messages + +A message is 4 words and at most one capability. The capability is +transferred only if the sender's endpoint capability has `Delegate` and the +transferred capability itself has `Delegate`; it is **copied** (the sender +keeps its own) with its permissions and badge unchanged. If the receiver's +table is full, or the capability is no longer valid at delivery time, the +message is delivered without it. + +The receiver learns the badge of the capability the sender used. The kernel +never reveals the sender's process or thread id. + +## Operations + +- `Send(ep, msg)`: if a receiver waits, deliver and continue; otherwise block + until one arrives. +- `Receive(ep)`: if the caller has a bound notification with bits set, + return those bits (and clear them). Else if a sender waits, take its + message. Else block until a sender arrives or the bound notification is + signalled. +- `Call(ep, msg)`: like `Send`, but the caller then blocks awaiting a reply. + The receiving thread's reply slot now names the caller. +- `Reply(msg)`: deliver `msg` to the thread in the caller's reply slot and + clear the slot. Error if the slot is empty. Never blocks. +- `ReplyRecv(ep, msg)`: `Reply` if the slot is full, then `Receive(ep)`. +- `Signal(ntfn)`: let `b` be the capability's badge, or `1` if unbadged. If + a waiter is blocked, wake the first with `b`. Else if the bound thread is + blocked in `Receive`, wake it with `b`. Else `bits |= b`. +- `Wait(ntfn)`: if `bits != 0` return and clear them; else block. +- `Poll(ntfn)`: return and clear `bits` (possibly 0); never blocks. +- `BindNotification(ntfn)`: bind to the calling thread. One binding per + thread and per notification. +- `SetIrqNotification(irq, ntfn)`: deliver interrupt `irq` as `Signal` + through the given (badged) notification capability. +- `AckInterrupt(irq)`: unmask the line after handling. + +An interrupt masks its line and signals its notification. If no route is +set it stays masked. + +## Reply slot rules + +- A server thread has at most one pending reply. `Receive` (not + `ReplyRecv`) with a pending reply drops it: the caller wakes with + `NoReply` instead of blocking forever. +- A reply slot names the caller by thread id **and generation**, so a reply + can never reach a different thread that reused the id. +- If the server thread dies, its pending caller wakes with `NoReply`. +- If the caller dies, the reply slot is simply stale and `Reply` is a + successful no-op. + +## Death + +Destroying an endpoint or notification wakes every thread blocked on it with +`InvalidCap`. Destroying a thread removes it from every queue, binding, +reply slot and IRQ route. + +## Register ABI (RISC-V) + +Inputs: `a7` = syscall number, arguments in `a0..a5`. A capability argument +of `NONE` (`usize::MAX`) means "no capability". + +Outputs: `a0` = status (`0` = ok, else an `ErrorCode`), `a1..` = results. +Receiving operations (`Receive`, `Call`, `ReplyRecv`, `Wait`) return: + +| Register | Value | +|----------|--------------------------------------------------------| +| a0 | status | +| a1 | badge (message) or signal bits (notification) | +| a2..a5 | message words 0..3 | +| a6 | received capability slot, or `NONE` | +| a7 | kind: 0 = message, 1 = call (reply expected), 2 = signal |