The services, and how they reach each other

Everything above the kernel is a service: an isolated address space, a contract, a capability table, and one receive endpoint. Nothing here shares memory with anything else. Every arrow on every diagram below is an IPC endpoint - a message copied by the kernel from one address space into another, under a capability that had to be granted (§8, §7).

This page is the map. It is written from the supervisor's own IMAGES table, which is the single place a service's name, image, core, peers, privileges and device class are declared. The rows for the worked examples under examples/, which the test suites start, are left out.


How a connection is made at all

A service does not "find" another service. It is wired to it, or it asks for it by name and the kernel mints a capability - and nothing else works.

   SPAWN TIME (the supervisor wires it)
   ┌────────────┐                              ┌────────────┐
   │ supervisor │──Spawn(image, peers:["fs"])─▶│   kernel   │
   └────────────┘                              └─────┬──────┘
                                                     │ mints a SEND cap to
                                                     │ fs's endpoint, into
                                                     ▼ the new task's table
                                               ┌────────────┐
                                               │   shell    │ holds cap → fs
                                               └────────────┘

   LATER (fs restarts; the shell's cap is now stale)
   ┌────────────┐   send ──▶ EndpointDead      ┌────────────┐
   │   shell    │──AcquireSendCap("fs")───────▶│   kernel   │ name directory
   └────────────┘   ◀── fresh cap, new gen ────└────────────┘ fs → EndpointId

Two properties follow, and they are the reason the rest of this page is shaped the way it is:

  • Identity is stable, location is not (invariant 11). A restarted fs is a different task on a possibly different core with a new endpoint generation. Clients see EndpointDead, reacquire by name, and carry on. Nobody tracks where anything runs.
  • Authority is explicit (invariant 1, 3). A service can only reach a peer it was granted, or one it was spawned declaring (the peer list in its spawn request, not its contract) and reacquired by name. ACQUIRE_ANY lifts that for the operator instruments - the shell, the supervisor, the probes. There is no ambient directory to browse.

The whole system

Arrows point from the caller to the service it sends to.

                       ┌──────────────┐
                       │    kernel    │  spawns exactly one thing
                       └──────┬───────┘
                              │
                       ┌──────▼───────┐
             ┌─────────│  supervisor  │─────────┐  spawns everything else,
             │         └──────┬───────┘         │  restarts what dies
             │                │                 │
     ┌───────▼──────┐  ┌──────▼──────┐   ┌──────▼──────┐
     │   control    │  │    shell    │   │   events    │
     │ (COM2 ops)   │  │  (the user) │   │             │
     └──────────────┘  └──┬───┬───┬──┘   └─────────────┘
                          │   │   │
        ┌─────────────────┘   │   └──────────────┐
        │                     │                  │
  ┌─────▼─────┐         ┌─────▼─────┐      ┌─────▼─────┐
  │    fs     │         │  console  │      │   time    │
  └─────┬─────┘         │ (display) │      └──┬────────┘
        │               └───────────┘         │
  ┌─────▼────────┐                      ┌─────▼──────┐
  │ block-driver │                      │ net-stack  │
  └─────┬────────┘                      └─────┬──────┘
        │  (USB disk)                         │
  ┌─────▼────────┐                      ┌─────▼──────┐
  │ dwc2 / xhci  │                      │ nic-driver │
  │  USB host    │                      └─────┬──────┘
  └──────────────┘                            │ (Pi 2: through USB)
                                        ┌─────▼──────┐
                                        │   dwc2     │
                                        └────────────┘

On x86 block-driver talks to an AHCI controller through its own MMIO capability and has no USB peer at all; on the Pi 2, the Pi 4 and the VisionFive 2 the disk is a USB device, so it goes through the USB host service. Same service, same block protocol, different peer - which is the point of naming peers rather than devices.


The services

supervisor - restart authority and the image catalogue

   kernel ──spawn (the ONE direct spawn)──▶ supervisor
                                              │
        ┌─────────────────────────────────────┼──────────────────────────┐
        │ IMAGES[]: name, ELF, core, peers,   │  death notification      │
        │           privileges, device class  │  ◀───────────────────────┤
        ▼                                     ▼                          │
     Spawn(image, class, bdf) ──▶ kernel   respawn ──▶ kernel ───────────┘

Trusted, and restartable: when it dies the kernel respawns it, unconditionally and forever, and it reconciles by adopting the services still running rather than duplicating them. The only unkillable thing in the system is the kernel (§6.2, §6.3).

Peers: none. Everything reaches it, not the other way round.

shell - the user's interface, and a capability broker

   keyboard ──▶ kernel console ring ──▶ shell ──▶ console  (what you see)
                                          │
                     ┌────────────────────┼──────────────┬──────────┐
                     ▼                    ▼              ▼          ▼
                    fs              block-driver       time     supervisor
              (files, pipes)         (drives)        (clock)    (spawn/kill)

There is no stdin and no fork. A pipe A | B is the shell carrying each stage's output to the next: in-process between built-ins, and for a service stage a SEND cap to the shell's own endpoint, granted at spawn (Appendix D.3, docs/pipes.md). A killed shell respawns as a fresh prompt - the in-flight command is lost, the session is not.

Peers: fs, block-driver, time, console, events, supervisor.

fs - the filesystem, and files as capabilities

   client ──Open("/doc.txt", READ)──▶ fs ──resource_mint──▶ kernel
          ◀──────── a real kernel capability ──────────────┘
                                     │
   client ──send ON THE FILE CAP──▶ kernel ──badged (resource_id, right)──▶ fs
                                     │
                                     ▼
                              block-driver (512-byte blocks, tagged req/reply)

A file is a capability (§7.10): unforgeable, non-escalating, revocable by generation bump. fs commits metadata through a crash-consistent redo journal, so its death is a restart rather than a reboot - which is what took it out of the trusted computing base.

Peers: block-driver, events.

block-driver - blocks, and nothing above them

   fs ──[op, lba, data?]──▶ block-driver ──▶ AHCI  (x86: own MMIO cap + DMA)
      ◀──[status, data?]───              └──▶ dwc2 / xhci  (USB: over IPC)

It knows sectors, not files. Where the disk is a USB device it sits behind the USB host service, so a peer restart makes its capacity temporarily unknowable - and unknowable is answered with an error, never with "no disk". Publishing zero sectors during a peer restart is how a filesystem gets mounted against nothing.

Peers: events, plus the USB host where the disk is on USB - dwc2 on the Pi 2, xhci on the Pi 4 and the VisionFive 2.

xhci / ehci / dwc2 - the USB host controllers

   kernel ──MMIO cap + DMA arena + IRQ route──▶ xhci
                                                 │
                  ┌──────────────────────────────┼────────────────┐
                  ▼                              ▼                ▼
             keyboard ──CONSOLE_PUSH──▶ shell   hub          mass storage
                                                              │
                                                    block-driver ◀┘

Ring-0 code parsing descriptors supplied by whatever was plugged in is exactly what §4.4 forbids, so these are ordinary services holding only what their contract grants. They cannot reboot the machine

  • REBOOT lives with the shell alone (SEC-2). Where an IOMMU confines one, a compromise is bounded to its granted DMA arena and it leaves the trusted computing base (§6.4). Today that is xhci on x86 alone: ehci runs in IOMMU passthrough even where there is one, and the Pis and the VisionFive have no IOMMU at all, so there dwc2 and xhci can point their DMA anywhere and stay in the trusted computing base.

A host also serves an installed USB driver package: when a device the package names by vendor and product is plugged in, the host binds it and carries the package's transfers to it (gs::driver::bus), so the package needs no hardware authority of its own (§14.1, the 2026-10-23 amendment).

nic-driver and net-stack - the network

   net-stack ──frames──▶ nic-driver ──▶ e1000 / RTL8168 (x86, own MMIO cap)
       │                            └──▶ dwc2 (Pi 2, USB ethernet)
       │                            └──▶ GENET (Pi 4, on the SoC; the radio when the cable is out)
       │                            └──▶ dwmac (VisionFive 2, on the SoC)
       ▼
   ARP · IPv4 · ICMP · UDP · DHCP · DNS · TCP
       │
       └──▶ a socket is a capability (the same mechanism as a file)

Where a board has a radio - wifi-driver (Pi 4, VisionFive 2) or wifi-usb (a USB dongle) - nic-driver bridges frames to it when the cable is out (not on QEMU's e1000, which has no bridge), and the cable always wins.

The kernel gains nothing from networking: it routes messages, and a socket is a delegated resource capability owned by net-stack. There is no ambient network any more than there is an ambient filesystem.

The clock is not the network's: net-stack never calls the clock service, which fetches its own NTP answer through net-stack's op 12, a UDP datagram answered when the reply arrives.

Peers: net-stack → nic-driver.

console - the terminal

   any service ──text──▶ console ──▶ framebuffer (grid, cursor, scroll, ANSI/CSI)
                                       ▲
   kernel bootcon ────────────────────┘  boot + panic only, then hands over

1,172 lines of terminal emulation that used to live in the kernel. What the kernel kept is a blit that cannot format, position or scroll - because a panic halts every core including this service, so it cannot ask a service to report it (§11.4).

Peers: events only - everything else writes to it.

wifi-driver - the radio

   shell (wifi) ──▶ wifi-driver ──▶ SDIO host ──▶ the radio chip (its own firmware, uploaded at start)
   nic-driver   ──▶ wifi-driver     (frames, once the cable is out - the cable always wins)
                      ├──▶ fs     (/system/shared/wifi/keys: the keys a join earned)
                      └──▶ power  (a lease on the Arm clock while it loads the chip)

The Pi 4's onboard radio, and the VisionFive 2 Lite's AIC8800 (docs/wifi-aic8800.md: scan, WPA2 join, DHCP and ping over it, the cable taking the link back). The chip-independent half - the handshake, the key file, the wire protocol, the SDIO protocol and the Station a serve loop drives - is a library, sdk/wifi, shared with the radios to come. A respawn adopts a firmware still running; a stopped one is power-cycled cold.

Peers: fs, power.

wifi-usb - a USB WiFi dongle

   wifi-usb ──usbfn──▶ USB host (dwc2 or xhci) ──▶ the dongle it bound (a Realtek RTL8188CUS)

The driver for a WiFi dongle, separate from wifi-driver so a board with an onboard radio can run both. It holds no hardware: the USB host that enumerated the dongle binds it as the radio and answers godspeed_wifi::usbfn for that one device - who it is, its control transfers and its frames. Behind dwc2 on the Pi 2 and xhci everywhere else, it scans, joins WPA2 and carries the machine's traffic through nic-driver when the cable is out; the supervisor starts it when the host reports the dongle and stops it when the dongle leaves (docs/wifi-usb.md).

Peers: its USB host (dwc2 or xhci), and fs for /system/shared/wifi/keys.

power - the machine's power policy

   wifi-driver ──lease──▶ power ──CpuClock──▶ kernel ──▶ firmware mailbox (the Arm clock)

Holds CPU_CLOCK alone. Leases of up to 30 s keep the Arm clock at its maximum while any is open; a lease nobody returns expires on its own, so a holder that dies cannot pin the machine fast. Without a lease a governor sets the clock by load, as Raspberry Pi OS's ondemand does: every 100 ms, a core over 50% busy holds it at its maximum for 5 s. It reads each core's counters with INTROSPECT, and nothing more. It also gives a shutdown's or a reboot's warnings and its tidy-up three seconds before the deadline, reading the deadline the kernel holds; it cannot shut the machine down itself - only the shell can.

Peers: fs and copier, for a shutdown's or a reboot's tidy-up only; the leases still only answer.

audio-driver - sound

   HD Audio controller ──MSI──▶ audio-driver ──▶ CORB/RIRB ──▶ codec (power, amps, pin, converter)
                                     │
                                     └──▶ a ring of sound in its DMA arena, refilled per period

Intel High Definition Audio, x86 only, heard on the Dell Wyse 5070 and the HP T630, speaker and headphones on both. The stream interrupts as each period of the ring is played and the driver refills it then, where the controller has MSI; where it has none (the T630) the driver polls. Its DMA is confined behind the IOMMU where there is one. On its death the kernel stops the controller's DMA and releases the confinement, keyed on the device it was given rather than its name.

Peers: fs, for its settings file /system/services/audio-driver/settings.

pwm-audio - sound on the Pis

   shell (audio) ──▶ pwm-audio ──▶ DMA engine ──▶ PWM FIFO ──▶ the 3.5 mm jack
                         └──▶ fs (its settings file)

The same protocol as audio-driver, for a different device: the Pis have no codec, so each sample is a PWM duty cycle, moved by the DMA engine as the PWM asks. The kernel routes the jack's pins and starts the PWM clock when it grants the device, because both live in blocks shared with every other pin and clock.

Peers: fs, for its settings file /system/services/pwm-audio/settings.

time - the wall clock

   shell ──▶ time ──▶ net-stack (op 12: one NTP datagram, answered when it arrives)
                 └──▶ fs (persist a clock floor across boots)

Peers: fs, net-stack.

events - the observability sink

Logging does NOT go through it, and that is the design. ctx.log() is a syscall: it writes the kernel's 16 KiB ring buffer and the serial console DIRECTLY, for every service, for the life of the machine. So a chaos storm that kills this service loses no log output, and ctx.log() never blocks on it and never fails because of it.

   ctx.log()  ──syscall 5──▶  kernel ring + serial          (always, never via events)
                     └──────▶  events  (a best-effort COPY, offered after the fact)

   gs::trace::metric() / IPC trace ──▶ events ──▶ three VOLATILE stores:
                                                    192-event IPC trace ring
                                                    64-slot metric table, keyed (owner, name)
                                                    8 KiB log window

That ordering is the whole point and must not be inverted: re-pointing logs AT this service would make observing a failure depend on a service that can fail.

Everything it holds is volatile and starts empty after a restart - a re-init, not a resume. The publishers are what make that work: they resolve lazily, ask the kernel for a cap when they hold none, retry a sample after reacquiring, and republish their liveness row on the first message after the sink returns.

Read it with: events ipc, events metrics, events log, events status. Peers: thirteen services declare it as a send peer; it publishes to nobody.

recorder - the capture that outlives the screen

Spawned on demand by events persist start, never at boot, and deliberately not restarted.

Writing to disk blocks, and a blocked single-threaded loop stops draining its endpoint - so if events did this itself it would drop the very events worth capturing, on a sick disk, exactly when they matter. recorder has the identical problem and it does not matter, because nothing depends on it: the dependency points the right way.

It is not restarted on purpose. A respawned recorder would not know its target path, so it would be alive and writing nothing while status said "running" - worse than dead, because dead is visible. Instead a capture opens with a header and closes with a footer, so a file with no footer says plainly that it died.

Peers: events, fs.

ask, godspeed-pm, copier, chaos, observe - programs started on demand

None of these is started at boot and none is in MANAGED, so nothing restarts them: each is started for one job and ends, or is ended, when the job is done.

  • ask answers one question about the machine (ask "is my system healthy?"). The shell starts it, hands it the question and reads back a record table of facts, inferences and unknowns. It only reads: INTROSPECT for the task table, and requests to the supervisor, fs, the storage chain and the radio drivers on this board. A network question starts it under a second row, ask-net, whose peers are net-stack and the radio drivers, because no one row may hold more than six.
  • godspeed-pm is the machine's package manager, started by the shell for one godspeed command. It sends to the shell for the answer, fs for its settings and trust roots, net-stack for the registry's connection, time for the date a certificate is checked against, and the supervisor, which starts and stops an installed package for it. It holds no privilege.
  • copier runs one detached job without owning the prompt - background copy, a tree delete, a check, a scrub - so a long job cannot take the shell down with it. It is not restarted, because a copier that came back would not know what it was copying. It sends to fs, and to the board's audio driver for the "done" sound a finished job makes.
  • chaos is chaos max-carnage's loop. It cannot live in the shell, because the shell is one of its targets. It holds SPAWN, INTROSPECT, SERVICE_CONTROL and ACQUIRE_ANY, and the console's input so that q reaches it, and it never kills itself. Its spawn-storm ammunition, and the victim of chaos mem-pressure, is mem-pressure: it allocates up to its memory limit, checks that a refused allocation stays refused, and waits to be killed, which is how its memory comes back.
  • observe is the live task view: every task's core, state, memory, restarts, queue depth and CPU use. It holds INTROSPECT and nothing else. It runs under three rows, observe, observe-now and observe-live, one binary that each row starts in a different mode.

hw-enumerator - hardware discovery in userspace

   supervisor ──"which device is class 0x0C0330?"──▶ hw-enumerator
              ◀──────────── BDF ─────────────────────    │
                                                    PCI_CFG: read ONE
                                                    config register,
                                                    select+fetch atomic.
                                                    No write. Ever.

It holds the narrowest hardware capability in the system. It cannot write configuration space at all - that would be write access to every BAR of every device on the bus.

Peers: events only - the supervisor asks it, never the other way round.

control - the operator channel

   host ──COM2──▶ control ──▶ supervisor   (KILL / RESTART, for tests and operators)

Peers: supervisor.


What a restart looks like from the outside

The single most important behaviour to understand, because every service depends on it:

   t0   shell ──send──▶ fs                         works
   t1   fs dies. kernel bumps its endpoint generation, drains the queue,
        clears its name from the directory
   t2   shell ──send──▶ EndpointDead               the stale cap fails loudly
   t3   supervisor respawns fs (possibly on a different core), which
        re-registers its name
   t4   shell ──AcquireSendCap("fs")──▶ fresh cap  and carries on

Between t1 and t3 the name does not resolve. A client that treats that window as "the peer is gone forever" rather than "ask again" is the shape of most of the bugs this system has had.

Reacquiring the endpoint is necessary but not sufficient (§14.3). Anything derived from the dead instance - an open-file capability, a socket, a cached capacity - was issued by an instance that no longer exists. It must be re-derived, not remembered.