Sprint 5 — Capabilities & IPC
The security model — unforgeable tokens and message passing.
✅ Complete
Table of contents
Overview #
Sprint 5 implements the two defining features of MinimalOS: capability-based access control and IPC (Inter-Process Communication). Together, they replace traditional Unix permissions with a mathematically sound security model where every resource access requires an explicit, unforgeable token.
Capability System #
What is a Capability?
A capability is a kernel-managed token that represents the right to perform specific operations on a specific resource. Capabilities are:
- Unforgeable — only the kernel creates them; userspace cannot fabricate them
- Typed — each capability refers to a specific kind of resource
- Rights-bearing — capabilities carry a bitmask of permitted operations
- Transferable — processes can send capabilities to other processes via IPC
- Revocable — the creator can invalidate a capability
Capability Table (CNode)
Each process has a CNode — a 64-slot array, each slot containing a Capability (typed object + rights bitmask):
graph LR
subgraph CT["Init Process CNode (64 slots)"]
S0["Slot 0: Empty"]
S1["Slot 1: PmmAllocator\nRights: ALL"]
S2["Slot 2: IoPort\nbase=0x3F8, size=8\nRights: ALL"]
S3["Slot 3: Process\npid=1 (self)\nRights: ALL"]
S4["Slot 4: IoPort\nbase=0xC000, size=128\nRights: ALL (Virtio-Blk)"]
S5["Slot 5–63: Empty"]
end
Syscalls reference capabilities by slot index: sys_map_memory(proc_slot=3, frame_slot=10, vaddr, flags).
Capability Types (Actual)
| Variant | Fields | Used By |
|---|---|---|
| Empty | — | Unused slot |
| Endpoint | id: u64 | SYS_SEND, SYS_RECV |
| MemoryFrame | phys: u64, order: u8 | SYS_MAP_MEMORY, SYS_ALLOC_MEMORY |
| Interrupt | irq: u32 | SYS_WAIT_IRQ |
| IoPort | base: u16, size: u16 | SYS_PORT_IN, SYS_PORT_OUT |
| ThreadControl | tid: u64 | (reserved) |
| Process | pid: u64 | SYS_MAP_MEMORY, SYS_DELEGATE, SYS_SPAWN_THREAD |
| PmmAllocator | — | SYS_ALLOC_MEMORY (grants right to allocate frames) |
Rights Bitmask (Actual)
pub struct CapRights(u8); // #[repr(transparent)]
const NONE = 0x00;
const READ = 0x01;
const WRITE = 0x02;
const EXEC = 0x04;
const GRANT = 0x08;
const REVOKE = 0x10;
const ALL = 0x1F;
Methods: from_raw(u8), bits(), contains(other), restrict(mask) — rights can only be reduced when delegated, never increased.
CNode Operations (Actual)
| Method / Syscall | Description |
|---|---|
CNode::lookup(slot) | Kernel-internal: read capability at slot index |
CNode::insert(cap) | Kernel-internal: insert into first empty slot |
CNode::insert_at(slot, cap) | Kernel-internal: insert at specific slot (fails if occupied) |
CNode::remove(slot) | Kernel-internal: remove and return capability |
SYS_DELEGATE(proc, src, dst) | Copy capability from caller's CNode to child process's CNode |
SYS_DROP_CAP(slot) | Remove capability from caller's CNode slot (frees for reuse) |
SYS_ALLOC_MEMORY(alloc, target) | Kernel creates MemoryFrame cap and places it in target_slot |
Monotonic Rights Reduction
When transferring a capability, rights can only be reduced, never increased. This ensures that delegation cannot escalate privileges:
graph LR
A["Process A\nMemory 0x1000\nRead+Write+Grant"] -->|"transfer with reduced rights"| B["Process B\nMemory 0x1000\nRead only"]
IPC (Inter-Process Communication) #
Design Principles
- Synchronous — send blocks until the receiver is ready; receive blocks until a message arrives
- Capability-mediated — both sender and receiver must hold an Endpoint capability
- Small message — 3-word payload (label, data0, data1) passed in registers
- Per-thread IPC buffer — each Thread has an
IpcMessagestruct for message storage
Message Format (Actual)
pub struct IpcMessage {
pub label: u64, // Message type/opcode
pub data0: u64, // Payload word 0
pub data1: u64, // Payload word 1
}
On SYS_RECV, the kernel copies the sender's IpcMessage into the receiver's IpcMessage buffer and returns the values in registers (RDI=label, RSI=data0, RDX=data1).
Communication Pattern (Actual)
// Sender (process A) — blocks until B calls SYS_RECV
sys_send(endpoint_slot, label, data0, data1);
// Receiver (process B) — blocks until A calls SYS_SEND
let (label, data0, data1) = sys_recv(endpoint_slot);
Thread State During IPC
When a thread calls SYS_SEND and no receiver is waiting, it transitions to ThreadState::BlockedSend and its Box<Thread> is owned by the IPC endpoint. When the partner calls SYS_RECV, the sender is woken, message is copied, and both threads return to Ready.
Security Guarantees #
| Property | Mechanism |
|---|---|
| No ambient authority | All resources accessed via capability slots |
| Information hiding | Processes only see capabilities they were granted |
| Controlled delegation | Capabilities transfer with monotonically decreasing rights |
| Complete mediation | Every resource access goes through the kernel's capability check |
| Revocation | Creator can revoke derived capabilities instantly |
Dependencies #
- Requires: Sprint 4 (process/thread structures, scheduler)
- Enables: Sprint 6 (syscall dispatch validates capabilities), Sprint 7–9 (init process delegates capabilities to children), Sprint 10 (Ring 3 allocator uses PmmAllocator cap)