feat(kernel): ipc cap transfer drops Delegate; one owner per irq line, freed when owner dies

This commit is contained in:
2026-10-27 14:42:28 +01:00
parent 37c482c36c
commit 33e1c849d1
7 changed files with 175 additions and 11 deletions

View File

@@ -46,10 +46,15 @@ impl<P: Platform> Kernel<P> {
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()
}

View File

@@ -27,10 +27,14 @@ impl<P: Platform> Kernel<P> {
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<P: Platform> Kernel<P> {
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<ProcessId> {
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) {

View File

@@ -26,6 +26,7 @@ impl<P: Platform> Kernel<P> {
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);

View File

@@ -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());
}