From 33e1c849d1e226b691cbf625a3e2b50e3ce12dc1 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 14:42:28 +0100 Subject: [PATCH] feat(kernel): ipc cap transfer drops Delegate; one owner per irq line, freed when owner dies --- crates/domain/src/irq/mod.rs | 13 ++- crates/domain/src/irq/tests.rs | 1 + crates/kernel/src/syscall/ipc/endpoint.rs | 13 ++- crates/kernel/src/syscall/ipc/irq.rs | 16 +++ crates/kernel/src/teardown.rs | 1 + crates/kernel/src/tests/ipc.rs | 128 +++++++++++++++++++++- docs/ipc.md | 14 ++- 7 files changed, 175 insertions(+), 11 deletions(-) diff --git a/crates/domain/src/irq/mod.rs b/crates/domain/src/irq/mod.rs index 9a23f69..c8636bb 100644 --- a/crates/domain/src/irq/mod.rs +++ b/crates/domain/src/irq/mod.rs @@ -1,7 +1,7 @@ use types::{ badge::Badge, generation::Generation, - id::{IrqId, NotificationId}, + id::{IrqId, NotificationId, ProcessId}, }; #[cfg(test)] @@ -17,6 +17,8 @@ pub struct IrqRoute { pub notification: NotificationId, pub generation: Generation, pub badge: Badge, + /// The process that set the route, and so owns the line. + pub owner: ProcessId, } pub struct IrqRoutingTable { @@ -53,6 +55,15 @@ impl IrqRoutingTable { } } + /// Drops every route `owner` set, when it dies. + pub fn clear_owned_by(&mut self, owner: ProcessId) { + for slot in self.routes.iter_mut() { + if slot.is_some_and(|r| r.owner == owner) { + *slot = None; + } + } + } + /// 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 554a720..69c54a6 100644 --- a/crates/domain/src/irq/tests.rs +++ b/crates/domain/src/irq/tests.rs @@ -8,6 +8,7 @@ fn route(n: u16) -> IrqRoute { notification: NotificationId::from_raw(n), generation: Generation::initial(), badge: Badge::new(1 << n), + owner: types::id::ProcessId::from_raw(0), } } diff --git a/crates/kernel/src/syscall/ipc/endpoint.rs b/crates/kernel/src/syscall/ipc/endpoint.rs index 9cab477..a3bd1de 100644 --- a/crates/kernel/src/syscall/ipc/endpoint.rs +++ b/crates/kernel/src/syscall/ipc/endpoint.rs @@ -46,10 +46,15 @@ impl Kernel

{ if !grant { return None; } - let copy: Capability = self - .check_cap(from, slot, Permission::Delegate) - .ok()? - .clone(); + // The receiver gets the capability but not the right to pass it + // on; a sender that wants that uses Delegate with explicit rights. + let source = self.check_cap(from, slot, Permission::Delegate).ok()?; + let copy = Capability::new( + *source.resource(), + source.permissions().without(Permission::Delegate), + *source.generation(), + ) + .with_badge(source.badge()); let parent = self.cap_ref(from, slot); self.insert_derived(to, copy, parent).ok() } diff --git a/crates/kernel/src/syscall/ipc/irq.rs b/crates/kernel/src/syscall/ipc/irq.rs index 29b6f8a..e8e3dbc 100644 --- a/crates/kernel/src/syscall/ipc/irq.rs +++ b/crates/kernel/src/syscall/ipc/irq.rs @@ -27,10 +27,14 @@ impl Kernel

{ let irq = self.interrupt_cap(pid, irq_slot)?; let (notification, badge, generation) = self.notification_cap(pid, ntfn_slot, Permission::Write)?; + if self.irq_owner(irq).is_some_and(|owner| owner != pid) { + return Err(KernelError::IrqAlreadyOwned(irq)); + } let route = domain::irq::IrqRoute { notification, generation, badge, + owner: pid, }; if self.irqs.set(irq, route) { Ok(()) @@ -45,10 +49,22 @@ impl Kernel

{ slot: CapSlot, ) -> Result<(), KernelError> { let irq = self.interrupt_cap(pid, slot)?; + if self.irq_owner(irq).is_some_and(|owner| owner != pid) { + return Err(KernelError::IrqAlreadyOwned(irq)); + } self.platform.unmask_irq(irq); Ok(()) } + /// The process that routed `irq`, while its route still leads to a + /// live notification. One owner per line: other holders of the IRQ + /// capability cannot take it over. + fn irq_owner(&self, irq: IrqId) -> Option { + let route = self.irqs.route(irq)?; + (self.notifications.generation(route.notification) == Some(route.generation)) + .then_some(route.owner) + } + /// 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) { diff --git a/crates/kernel/src/teardown.rs b/crates/kernel/src/teardown.rs index 37ef20e..a963c98 100644 --- a/crates/kernel/src/teardown.rs +++ b/crates/kernel/src/teardown.rs @@ -26,6 +26,7 @@ impl Kernel

{ self.delete_all_caps(pid); self.destroy_endpoints_owned_by(pid); self.destroy_notifications_owned_by(pid); + self.irqs.clear_owned_by(pid); self.teardown_memory(pid); let _ = self.processes.destroy(pid); diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index 4ed2bd6..cb1839d 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -12,7 +12,7 @@ use domain::thread::ThreadState; use types::address::DataRef; use types::badge::Badge; use types::bounded::CapSlot; -use types::id::{EndpointId, IrqId, NotificationId, ThreadId}; +use types::id::{EndpointId, IrqId, NotificationId, ProcessId, ThreadId}; use types::permission::{Permission, PermissionSet}; fn perms(ps: &[Permission]) -> PermissionSet { @@ -869,3 +869,129 @@ fn interrupts_beyond_the_routing_table_are_ignored() { let mut t = Ipc::new(); t.k.handle_interrupt(IrqId::from_raw(1000)); } + +#[test] +fn a_received_cap_cannot_be_passed_on() { + 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 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), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + let got = d.cap.expect("the cap arrives"); + let rights = + *t.k.processes + .get(b) + .unwrap() + .capabilities() + .get(got) + .unwrap() + .permissions(); + assert!( + rights.allows(Permission::Execute), + "the right itself arrives" + ); + assert!( + !rights.allows(Permission::Delegate), + "but not the right to pass it on" + ); + let mint = t.run( + tb, + Syscall::Mint { + slot: got, + badge: Badge::new(1), + permissions: perms(SEND), + }, + ); + assert!(matches!( + error(&mint), + Some(KernelError::PermissionDenied { .. }) + )); +} + +#[test] +fn an_irq_has_one_owner_until_it_dies() { + let mut t = Ipc::new(); + let (a, b, ta, tb) = (t.a, t.b, t.ta, t.tb); + let irq = IrqId::from_raw(12); + let route = |t: &mut Ipc, pid: ProcessId, tid: ThreadId| { + let irq_cap = + t.k.grant_cap_for_test(pid, Resource::Interrupt(irq), perms(&[Permission::Execute])); + let n = notification(t, pid); + let sig = + t.k.grant_cap_for_test(pid, Resource::Notification(n), perms(&[Permission::Write])); + ( + irq_cap, + t.run( + tid, + Syscall::SetIrqNotification { + irq: irq_cap, + notification: sig, + }, + ), + ) + }; + let (_, first) = route(&mut t, b, tb); + assert!(error(&first).is_none()); + let (a_irq, stolen) = route(&mut t, a, ta); + assert!(matches!( + error(&stolen), + Some(KernelError::IrqAlreadyOwned(_)) + )); + let ack = t.run(ta, Syscall::AcknowledgeInterrupt { irq: a_irq }); + assert!( + matches!(error(&ack), Some(KernelError::IrqAlreadyOwned(_))), + "nor unmask it" + ); + let (_, again) = route(&mut t, b, tb); + assert!( + error(&again).is_none(), + "the owner may re-route its own line" + ); + + t.k.destroy_process(b); + let (_, claimed) = route(&mut t, a, ta); + assert!(error(&claimed).is_none(), "a dead owner's line is free"); +} + +#[test] +fn a_dead_owner_frees_its_line_even_if_the_notification_lives_on() { + let mut t = Ipc::new(); + let (a, b, ta, tb) = (t.a, t.b, t.ta, t.tb); + let irq = IrqId::from_raw(13); + // B routes the line to a notification A owns, then dies. + let b_irq = + t.k.grant_cap_for_test(b, Resource::Interrupt(irq), perms(&[Permission::Execute])); + let n = notification(&mut t, a); + let b_sig = + t.k.grant_cap_for_test(b, Resource::Notification(n), perms(&[Permission::Write])); + let routed = t.run( + tb, + Syscall::SetIrqNotification { + irq: b_irq, + notification: b_sig, + }, + ); + assert!(error(&routed).is_none()); + t.k.destroy_process(b); + + let a_irq = + t.k.grant_cap_for_test(a, Resource::Interrupt(irq), perms(&[Permission::Execute])); + let a_sig = + t.k.grant_cap_for_test(a, Resource::Notification(n), perms(&[Permission::Write])); + let claimed = t.run( + ta, + Syscall::SetIrqNotification { + irq: a_irq, + notification: a_sig, + }, + ); + assert!(error(&claimed).is_none()); +} diff --git a/docs/ipc.md b/docs/ipc.md index e42054b..e30030d 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -100,7 +100,9 @@ the UART) to their drivers before anything else (`libkos::device`): 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 +keeps its own) with the same badge and permissions minus `Delegate`: the +receiver can use it but not pass it on. To let a process pass a capability +on, give it with the `Delegate` syscall and explicit rights. If the receiver's table is full, or the capability is no longer valid at delivery time, the message is delivered without it. @@ -128,8 +130,10 @@ never reveals the sender's process or thread id. - `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. + through the given (badged) notification capability. The first process to + route a line owns it; other holders of the IRQ capability get + `IrqAlreadyOwned` until the owner dies or its notification is destroyed. +- `AckInterrupt(irq)`: unmask the line after handling (owner only). An interrupt masks its line and signals its notification. If no route is set it stays masked. @@ -148,8 +152,8 @@ set it stays masked. ## 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. +`InvalidCap`. Destroying a thread removes it from every queue, binding and +reply slot; a dying process loses every IRQ route it set. ## Register ABI (RISC-V)