establishment: the two-controller proof, and the docs catch up

P4 of docs/establishment-planes-plan.md. The new usb-two-controllers case is
the Ryzen mouse bug pinned in the suite: a second xHCI controller with its
own keyboard while the boot controller keeps the default one — both must
come up, on different device ids, which requires each class driver to reach
ITS OWN controller. Discrimination: at 72807c2 (name-based establishment)
the case fails — one keyboard is unreachable, exactly the bench failure —
verified against a checkout of that merge; with lineage routing it passes.
The existing second-controller cases could not prove this: the boot bus
always carries a keyboard, so their expects were satisfiable by it.

Docs updated with the code: device-manager.md (hello moves the channels,
paged enumerate, delegation as built, reap-and-rebuild in the restart
sequence), device-authority.md (the fourth as-built decision: driver-layer
channels ride the hello; hello is no longer only the liveness handshake).

Full suite: 118/119, the one failure being iommu-fault's fixed 200 ms
fault window under end-of-run host load — 3/3 green standalone, deflake
flagged separately. usb-two-controllers passed inside the full run.
This commit is contained in:
Daniel Samson
2026-08-09 12:48:33 +01:00
parent 8710944a92
commit 3c9f454398
3 changed files with 67 additions and 13 deletions
@@ -74,13 +74,26 @@ one world.
| Direction | Packet | Purpose |
|---|---|---|
| driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock |
| driver → manager | `hello { role, wants_channel, version }` @ the assigned device | confirms the assignment, starts the deadline clock — **and moves the channels** (below) |
| bus → manager | `child_added { parent, bus_address, identity, bus, vendor, device, subsystem, hid }` @ the registered device id | one node the bus discovered |
| bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
| app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail |
| app → manager | `enumerate` (reserved verb 1) @ start index | one page of the tree, one `ChildEntry` per record in the reply's tail; a short page is the end |
| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability |
| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called |
**`hello` is also the establishment plane for driver-layer channels**
([communication.md](../os-development/communication.md) "Establishment: two
planes, one namespace"). A provider's hello carries its serving endpoint UP as
the call's capability (the xHCI bus its transfer endpoint, usb-storage its
block endpoint — several processes provide those contracts, so neither is ever
a registry name). A hello with `wants_channel` set is answered with a
capability DOWN: for a `.device`-role driver, the channel of its device's
*reporter* (a class driver reaching its own controller); for a `.consumer`
(not a spawned driver at all — fat looking for its volume), the channel of the
driver *bound to* the target device. No channel in an acked reply means the
provider is mid-restart and its re-report is on the way — retryable, never a
verdict.
The watcher table behind those last two rows is the **service harness's**
(`service.Subscribers`, shared with input and power), not the manager's: it
answers `subscribe`/`unsubscribe`, frames each event once for the fan-out, and
@@ -113,10 +126,11 @@ the stop sequence and the restart policy. Everything else lifecycle-shaped
(terminate, the common `ping` liveness call, exit reasons) arrives through
[process-lifecycle.md](../os-development/process-lifecycle.md)'s vocabulary, not this protocol.
Assignment stays argv (`usb-xhci-bus <device id>`) for now — simple, and it works.
The step after `hello` exists is delegation: the manager claims (or is granted) the
devices and passes the claim to the driver over IPC (the M13 capability-transfer
mechanism), replacing first-come-first-served `device_claim` with policy. Identity in
Delegation is BUILT (docs/os-development/device-authority.md "As built"): the
device arrives WITH the spawn — `system_spawn`'s sixth argument moves the
claim before the child's first instruction — and the id still rides argv for
the driver to name itself by. A given device is a loan that returns to the
manager on the holder's death. Identity in
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
@@ -133,13 +147,19 @@ On a death notification:
Clean exit → it meant to; don't restart. Fault or missed `hello` deadline →
restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed,
stop respawning, log loudly; a later `reload` to the manager can retry).
2. **Prune the subtree** the dead bus driver reported. Its children describe
protocol state (xHCI slot ids, transfer rings) that died with the process;
keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the
input service losing, then regaining, a keyboard is the *honest* description of
what happened. The restarted instance rediscovers and re-reports.
3. **The claim is already free** because the kernel released it at death — the
restarted instance claims the same controller and comes up.
2. **Prune the subtree** the dead bus driver reported, **reaping the class
drivers bound to its children**. Its children describe protocol state (xHCI
slot ids, transfer rings) that died with the process; keeping the nodes
would be keeping a lie — and the class drivers hold channels into the dead
process they cannot observe dying (an HID driver blocks on reports that
simply never come). Each is killed and its entry cleared, so the matcher
can respawn a fresh generation when the re-report arrives; their hellos
fetch the successor's channel. Watchers receive `child_removed` — the input
service losing, then regaining, a keyboard is the *honest* description of
what happened.
3. **The device is already back** because the loan returned at death — the
manager re-delegates the same controller with the respawn, and the kernel
confines it afresh.
Who supervises the supervisor: **init** (PID 1), which already supervises the
services it starts. If the manager dies, drivers keep running (they hold their
+12
View File
@@ -173,3 +173,15 @@ Three details settled differently, or beyond, what the sections above say:
unconfined forever. Both give paths (`device_transfer` and spawn's give) share one
body in [process.zig](../../system/kernel/process.zig) (`giveDeviceLocked`), and the
`iommu` kernel test drives the death-and-respawn sequence against it directly.
A fourth followed on 2026-08-09, when a real three-controller machine broke the last
name-shaped assumption ([communication.md](communication.md) "Establishment: two
planes, one namespace"): **driver-layer channels ride the same hello.** A driver's
serving endpoint goes up as the hello's capability; consumers get their provider's
channel down in the reply, routed by the manager's lineage — the reporter for a
class driver, the bound driver for a `.consumer`. `usb-transfer` and `block`
stopped being registry names, and a reporter's death now reaps its class-driver
subtree so the re-report can rebuild it against the successor — the manager half of
the loan story above. One correction to the earlier bullet: `hello` is no longer
*only* the liveness handshake; it is also where establishment happens. The device
itself still arrives with the spawn.