Files
k-os/docs/ipc.md
Gabriel Kaszewski 154599e3af feat(kernel): derivation-tree revocation, region caps
- caps record parent (mint/delegate/ipc), CapRef w/ pid+slot gens
- Revoke = delete subtree, keep self; drop/death re-parents children
- CreateRegion/MapRegion; mapping lives only as long as its cap
- drop Permission::Revoke + object-wide revoke
- mint any cap (badge only ep/ntfn), process caps delegable
- kani: descendants == subtree; model-based fuzz vs spec
2026-10-26 20:41:03 +01:00

123 lines
5.6 KiB
Markdown

# 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 a capability with narrower permissions
and, for endpoints and notifications, a badge. A badge cannot be changed
once set: a badged capability cannot be minted again.
## Derivation and revocation
Every capability made from another (by `Mint`, `Delegate` or an IPC
transfer) records it as its **parent**; capabilities the kernel creates
directly have none. `Revoke(cap)` deletes every capability derived from
`cap`, transitively and across processes, and keeps `cap` itself. Any
holder may revoke, but only its own subtree.
Deleting a capability (`DropCap`, or its process dying) makes its parent
adopt its children, so stepping out of the chain never shields anyone
from a revocation further up.
A region mapping made with `MapRegion(cap, va, perms)` lasts only as long
as `cap`: deleting or revoking `cap` unmaps it. `perms` needs `Read`, plus
`Write` for read-write or `Execute` for read-execute.
| 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 |