Privilege is the organizing idea
The ARM processor runs code at exception levels. Ouroboros uses
two: EL1 for the kernel, EL0 for
everything else. EL0 code cannot touch kernel memory, device
registers, or privileged system registers. The only way from EL0
into EL1 is a trap — a system call
(svc), a fault, or the timer interrupt —
caught by the exception vector table. That single crossing is where
the whole design lives. Firmware decides which level the kernel is
entered at: QEMU and Parallels hand off at EL1, the Raspberry Pi at
EL2, and there the kernel drops to EL1 before it relies on any EL1
register, with its tables and vectors prepared from EL2 and an
eret into a running MMU.
Inside the kernel
The kernel is deliberately small. It owns the things that require privilege — trap handling, memory mapping, the interrupt controller, raw devices — and the mechanisms tasks use to talk to each other. It does not contain policy it can push outward.
Exception vectors
A 16-entry VBAR_EL1 table catches every syscall, fault, and interrupt. Kernel faults report and halt; EL0 faults are contained — only the faulting task dies.
Round-robin scheduler
Preemptive, no priorities. A 20 ms timer tick switches tasks; a blocking syscall switches immediately. States: unused, runnable, blocked, zombie.
Syscall dispatch
Number in x8, arguments in x0–x4, return in x0. A shared syscall-abi crate keeps both sides from drifting.
Per-task page tables
Each task has its own L0–L3 view. Kernel and device mappings are shared; EL0 access is granted to that task's region only. Switching tasks switches TTBR0, and the slot it switches to is a TaskIndex: a value below NUM_TASKS by construction, so a slot with no view cannot be spelled.
IPC · grant/safecopy
Fixed-size messages copied task-to-task through bounded mailboxes, plus a capability for moving bulk data directly between two isolated regions.
Timer + interrupts
The ARM generic timer drives preemption; the GIC (v2 or v3, discovered from ACPI's MADT) routes its interrupt. Real IRQ delivery on QEMU and Parallels.
Console: discovery + emergency
Five discovery paths tried in order (a GOP framebuffer wins on Apple Silicon), a minimal emergency console for boot and fault reporting, and dumb framebuffer pixel primitives. The rendering itself moved out — see below.
Raw hardware
A from-scratch xHCI USB stack (keyboard and mass storage) and a virtio transport with block, network, console and entropy drivers — the parts that genuinely touch MMIO and DMA.
Block-device custody
The kernel holds the raw disk and hands out one 512-byte sector at a time — but only to the filesystem server. Any other caller is denied.
The filesystem → userland
FAT32, exFAT and ext2 parsing, partition tables, directories, paths, permissions — none of it lives in the kernel. It moved out to a task (fsd), reached only over IPC.
The console → userland
Font, cursor, line wrap, scrolling, ANSI parsing — the console rendering moved out to a task (cond) too, driving the kernel's dumb pixel primitives over gated syscalls.
The network → userland
ARP, IPv4, ICMP, UDP, DNS and TCP are a userland task (netd) — the whole protocol stack, plus an HTTP server and the cluster's 9P gateway. The kernel keeps only the DMA-owning NIC driver, gated to that one task.
Accounts → userland
Password changes go to a task (accountd) rather than a setuid binary — which could not work here, since the loader is an ordinary program and "this is setuid" would be a claim by a task the capability model distrusts.
The tasks
Eleven scheduler slots. The first six have fixed roles — the shell, idle, and the four servers; the remaining five are filled at runtime, when the shell runs a command or builds a pipeline. Each is an ordinary EL0 program — the shell is not special kernel code, it is just the program loaded into slot 0, and neither is any server.
| Slot | Task | Responsibility | Status |
|---|---|---|---|
| 0 | Shell | The interactive program loaded from disk at boot. Owns the keyboard. Every disk command is an IPC request to the filesystem server. | own view |
| 1 | Idle | A bare busy-spin — runs when nothing else can. (A real spin, not wfe: EL0 wfe hangs under the Apple hypervisor.) |
protected |
| 2 | FS server | Owns FAT32, exFAT, ext2, a synthetic /proc, partition discovery and permission enforcement. The only task allowed the block-device syscalls. Boot-loaded and supervised — the kernel restarts it from a kept image if it crashes or wedges. |
protected |
| 3 | Console server | Owns the steady-state console. Userland output flows to it over IPC; it renders glyphs and a cursor to the framebuffer (or forwards to a UART). Boot-loaded, protected, and supervised like the FS server: restarted if it crashes or wedges (confirmed live on real Parallels). | protected |
| 4 | Network server | The entire protocol stack in userland — ARP, IPv4, ICMP, UDP, DNS, TCP — plus an HTTP server, the cluster's 9P export gateway, and remote execution. The only task allowed the NIC syscalls; the DMA-owning driver stays in the kernel because there is no IOMMU. | protected |
| 5 | Account server | Changes passwords for callers who may not write /etc/shadow themselves. It holds no device capability at all — its privilege is policy, and it authorizes on the sender credential the kernel bound when the message was sent, never on who occupies a slot now. |
protected |
| 6–10 | Spawnable | Five slots filled by running a command — a /bin program, a background job, or the stages of a pipeline running concurrently. Own memory, own view, reaped when they exit. Job control (fg, kill, wait) and IPC let them cooperate. |
runtime |
exit/kill; 6–10 cycle as programs come and go. The guard derives from one constant, so a new server is protected the moment it is added.How the tasks are kept apart
Isolation here is enforced by the hardware MMU, not by good behavior. Three mechanisms compose: separate address-space views, message passing by copy, and a capability for the cases where copying through a message is too small.
Per-task page tables
Every task runs under its own translation view. From inside any task, another's memory is simply not mapped for EL0 — touching it faults, and the fault kills only the toucher. Proven by injection: the same read succeeds under a shared map and faults under views. The same mechanism guards a task from itself: a page mapped EL1-only sits between its heap and its stack, so a stack overflow faults cleanly instead of quietly corrupting what is below it — it has caught real overflows three times.
Messages by copy
Tasks never share memory. A message is copied — out of the sender, into the kernel, into the destination's mailbox. Small, bounded, safe by construction: no pointer from one task is ever dereferenced by another. And a task may only message the endpoints its per-slot capability mask allows — a spawned program can reach the servers it legitimately needs and the shell, not the device drivers' hardware and not arbitrary tasks — so isolation is topological, not just memory-level. The mask is a baseline: a task holding a capability may hand it onward at runtime, which is how the shell wires one program's output straight into the next, and how any command gets to speak to the network server at all.
Grant & safecopy
To move file contents, a client grants the server an exact buffer, in one direction. The server may copy only those bytes, only while the client's call is in flight, never a third task's — a capability the kernel checks, not a trust it assumes.
at EL1
between them
One file read, end to end
Because the filesystem is a separate process, a plain
cat crosses the privilege boundary
several times. This is the cost the microkernel split pays — and,
right beside it, exactly what it buys.
cat /notes.txt. The shell grants the server a read buffer, then calls it.cat as a sequence of traps. Light cards are EL0; dark panels are the kernel.An EL0 fault is contained, not fatal. A wild pointer in any userland program used to halt the whole system. Now the kernel reports the fault, tears down just that task, and resumes the rest. If the dead task is the filesystem server, it is reloaded from an image kept at boot and remounts from disk — because all of its state was always derivable from the disk. That is the whole reason the filesystem was worth moving out.
From firmware to EL0
There is no separate bootloader — the kernel is a UEFI application, the only boot path that works on both the QEMU dev loop and the real Parallels target. It sets itself up while still inside firmware, then leaves that world for good.
VBAR_EL1, before anything can fault.tasks::start — and never returns.Go deeper
This page is the overview. The reference documents in the repository go all the way down: architecture.md for the full mechanics, processes.md for the userland-program model, and the postmortems for the debugging that found this design on real hardware.