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

@@ -1,7 +1,7 @@
use types::{ use types::{
badge::Badge, badge::Badge,
generation::Generation, generation::Generation,
id::{IrqId, NotificationId}, id::{IrqId, NotificationId, ProcessId},
}; };
#[cfg(test)] #[cfg(test)]
@@ -17,6 +17,8 @@ pub struct IrqRoute {
pub notification: NotificationId, pub notification: NotificationId,
pub generation: Generation, pub generation: Generation,
pub badge: Badge, pub badge: Badge,
/// The process that set the route, and so owns the line.
pub owner: ProcessId,
} }
pub struct IrqRoutingTable { 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. /// The route for `irq`; `None` for unknown or unrouted lines.
pub fn route(&self, irq: IrqId) -> Option<IrqRoute> { pub fn route(&self, irq: IrqId) -> Option<IrqRoute> {
*self.routes.get(irq.raw() as usize)? *self.routes.get(irq.raw() as usize)?

View File

@@ -8,6 +8,7 @@ fn route(n: u16) -> IrqRoute {
notification: NotificationId::from_raw(n), notification: NotificationId::from_raw(n),
generation: Generation::initial(), generation: Generation::initial(),
badge: Badge::new(1 << n), badge: Badge::new(1 << n),
owner: types::id::ProcessId::from_raw(0),
} }
} }

View File

@@ -46,10 +46,15 @@ impl<P: Platform> Kernel<P> {
if !grant { if !grant {
return None; return None;
} }
let copy: Capability = self // The receiver gets the capability but not the right to pass it
.check_cap(from, slot, Permission::Delegate) // on; a sender that wants that uses Delegate with explicit rights.
.ok()? let source = self.check_cap(from, slot, Permission::Delegate).ok()?;
.clone(); let copy = Capability::new(
*source.resource(),
source.permissions().without(Permission::Delegate),
*source.generation(),
)
.with_badge(source.badge());
let parent = self.cap_ref(from, slot); let parent = self.cap_ref(from, slot);
self.insert_derived(to, copy, parent).ok() 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 irq = self.interrupt_cap(pid, irq_slot)?;
let (notification, badge, generation) = let (notification, badge, generation) =
self.notification_cap(pid, ntfn_slot, Permission::Write)?; 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 { let route = domain::irq::IrqRoute {
notification, notification,
generation, generation,
badge, badge,
owner: pid,
}; };
if self.irqs.set(irq, route) { if self.irqs.set(irq, route) {
Ok(()) Ok(())
@@ -45,10 +49,22 @@ impl<P: Platform> Kernel<P> {
slot: CapSlot, slot: CapSlot,
) -> Result<(), KernelError> { ) -> Result<(), KernelError> {
let irq = self.interrupt_cap(pid, slot)?; 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); self.platform.unmask_irq(irq);
Ok(()) 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 /// An interrupt fired: mask the line and signal its notification. The
/// line stays masked until the handler acknowledges it. /// line stays masked until the handler acknowledges it.
pub fn handle_interrupt(&mut self, irq: IrqId) { 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.delete_all_caps(pid);
self.destroy_endpoints_owned_by(pid); self.destroy_endpoints_owned_by(pid);
self.destroy_notifications_owned_by(pid); self.destroy_notifications_owned_by(pid);
self.irqs.clear_owned_by(pid);
self.teardown_memory(pid); self.teardown_memory(pid);
let _ = self.processes.destroy(pid); let _ = self.processes.destroy(pid);

View File

@@ -12,7 +12,7 @@ use domain::thread::ThreadState;
use types::address::DataRef; use types::address::DataRef;
use types::badge::Badge; use types::badge::Badge;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::id::{EndpointId, IrqId, NotificationId, ThreadId}; use types::id::{EndpointId, IrqId, NotificationId, ProcessId, ThreadId};
use types::permission::{Permission, PermissionSet}; use types::permission::{Permission, PermissionSet};
fn perms(ps: &[Permission]) -> PermissionSet { fn perms(ps: &[Permission]) -> PermissionSet {
@@ -869,3 +869,129 @@ fn interrupts_beyond_the_routing_table_are_ignored() {
let mut t = Ipc::new(); let mut t = Ipc::new();
t.k.handle_interrupt(IrqId::from_raw(1000)); 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());
}

View File

@@ -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 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 only if the sender's endpoint capability has `Delegate` and the
transferred capability itself has `Delegate`; it is **copied** (the sender 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 table is full, or the capability is no longer valid at delivery time, the
message is delivered without it. 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 - `BindNotification(ntfn)`: bind to the calling thread. One binding per
thread and per notification. thread and per notification.
- `SetIrqNotification(irq, ntfn)`: deliver interrupt `irq` as `Signal` - `SetIrqNotification(irq, ntfn)`: deliver interrupt `irq` as `Signal`
through the given (badged) notification capability. through the given (badged) notification capability. The first process to
- `AckInterrupt(irq)`: unmask the line after handling. 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 An interrupt masks its line and signals its notification. If no route is
set it stays masked. set it stays masked.
@@ -148,8 +152,8 @@ set it stays masked.
## Death ## Death
Destroying an endpoint or notification wakes every thread blocked on it with Destroying an endpoint or notification wakes every thread blocked on it with
`InvalidCap`. Destroying a thread removes it from every queue, binding, `InvalidCap`. Destroying a thread removes it from every queue, binding and
reply slot and IRQ route. reply slot; a dying process loses every IRQ route it set.
## Register ABI (RISC-V) ## Register ABI (RISC-V)