Files
danos/docs/establishment-planes-plan.md
Daniel Samson 0d5a7394ef docs: establishment planes — the design and the two-seam plan
The namespace holds protocols, never instances; what multiplies is provider
processes, and their establishment routes through the device manager, which
owns the topology. communication.md gets the model; the plan converts the
usb-transfer and block seams in one flag-day, closes the restart-zombie hole
the scoping found, and pins the three-controller mouse bug as a QEMU case.
2026-08-09 11:26:53 +01:00

7.6 KiB

Establishment planes: the plan

2026-08-09. The design is settled in communication.md ("Establishment: two planes, one namespace"): driver-layer channels stop being registry names and are routed at establishment by the device manager, which owns the topology. This converts both live seams — usb-transfer and block — in one flag-day, and fixes the restart hole the scoping audit found on the way. Found by a real machine: three xHCI controllers, one name, a mouse nobody could reach.

Non-goals, stated up front. fat stays single-volume (multi-volume mounting, volume identity/content-probing, and fat's own reconnection after a storage restart are the M21 remount track). The input service's last-writer-wins button mask across two mice is a known quirk, not part of this change. Nothing here touches the app-facing names — input, display, ufs keep their exclusive registry binds.

What the scoping reads established

  • Capabilities already travel both directions of a synchronous call, and only there. Request-direction via ipc.callCap (r9 → shareCapability at server dequeue, a refcounted copy); reply-direction via ipc_reply_wait's send_cap (surfaces as the client's Reply.cap) — init's registry is the working in-tree precedent (pending_capability). Pushes structurally cannot carry one. No kernel change is needed.
  • The one mechanism gap is the service harness: service.run hardcodes the reply capability to null and Answer has no capability slot; the device manager runs on that harness. It needs a reply-cap out-slot following init's pattern, with null staying the untouched default for every other service.
  • The manager already stores the routing fact: Child{parent, bus_address, device_id, reporter} — consumer's device id → children[].reporter → driverByProcess(reporter) → the (new) stored endpoint. Hello.role (.bus vs .device) already exists on the wire to distinguish provider hellos (cap up) from consumer hellos (cap down); the manager currently never reads it.
  • The restart hole (new finding): today, when a bus instance dies, its class drivers are zombies — HID drivers block forever on interrupt reports that will never come, usb-storage answers fat with errors forever; the manager respawns neither, and the matcher's dedupe actively prevents a respawn on re-report. The user's core goal is restart-a-driver-live; this plan closes the hole.
  • Blast radius: every QEMU case boots from USB storage, so this seam failing fails the whole suite, and the load-bearing serial markers (fat: mounted /volumes/usb, usb-storage: ready, fat-test: ok) must not change.

P0 — mechanics (library + manager, no behavior change yet)

  1. Harness reply-capability out-slot. A handler nominates a capability for its reply (init's pending_capability idiom moved into service.run's loop); default null; the existing Arrival take/release ownership discipline is unchanged. Every other service compiles and behaves identically.
  2. driver.hello grows both directions. Provider form: carry the caller's serving endpoint up (ipc.callCap). Consumer form: return Reply.cap. One call can do both (a storage driver hands its block endpoint up and receives its bus channel down in the same hello).
  3. Manager stores and routes. Driver gains an endpoint field; the cap is claimed at onMessage level (the capability_claimed pattern usb-xhci-bus already uses — the harness's private claim flag composes, verified). onDriverExit must ipc.close the stored handle (a dead owner's endpoint survives as a refcounted slot; repeated restarts would exhaust the manager's 32-slot table — boot-fatal). Consumer hello with an unknown device id (bus died, not yet re-reported) gets a retryable refusal, never a permanent one.

P1 — the usb-transfer seam

  • Bus: delete the bindPatiently("usb-transfer") block and its serve-unnamed fallback; hello carries the endpoint up.
  • usb.open(device_id) takes the channel from the class driver's hello instead of the name lookup; retry patience stays ≥ today's window (the provider's hello precedes the consumer's spawn in the normal order — the retry covers respawn races).
  • usb-storage's consumer side uses the same lineage (its own device id).
  • protocol.csv: delete the bind row (67) and the three open rows (119/120/122) in the same commit as the code — grants and code move together or the old path dies silently first.
  • Fixtures: conformance table row, the usb-report kernel-side registry spawn.

P2 — the block seam

  • usb-storage: drop .service = "block" (the field is optional; the harness serves nameless — pci-bus and logger already do); its initialise keeps the endpoint it currently discards and hellos it up. Exit-code contract preserved: device absent = clean exit (no restart), device-present failure = exit(1) (restart with backoff) — establishment failure is the latter, never a silent pre-hello death like today's second-stick -EBUSY.
  • fat: block.tryOpen's name lookup becomes manager-routed — enumerate, find the block-class provider, hello for its channel. One volume: unambiguous. Two volumes: fat takes the first by device-id order — deterministic within a boot, and no worse than today's bind race; choosing the boot volume by content is deferred to M21 (risk noted: /system/configuration and /system/logs mounts ride this choice).
  • fat gets a protocol.csv open grant for device-manager; rows 68/99 die.
  • Delete block.open (zero callers, dead code).

P3 — restart integrity (the zombie fix)

  • A reporter's death reaps its subtree. When a bus/storage instance dies, the manager already prunes its children; now it also kills the class drivers spawned for those children and clears their entries, so the matcher's dedupe stops blocking the respawn. On re-report (ids are stable — the P<port>I<iface> registration identity), the matcher spawns fresh class drivers, which hello and receive the successor's channel. Restart a bus driver live: the subtree rebuilds itself.
  • usb-storage on -EPEER from its bus: exit(1) → supervisor restart with backoff → fresh hello. (fat's own reconnection: M21.)
  • Extend the existing bus-restart drill (test-usb-restart, the usb-report case) to assert the subtree works after the restart — a post-restart keyboard event, not just a re-report. Discrimination: today that assertion fails (zombie).

P4 — the multi-instance proof

  • New QEMU case: two xHCI controllers, keyboard on one, storage on the other; assert both class drivers come up and function. Discrimination: without the conversion this case fails exactly like the Ryzen mouse (second controller's child unreachable). This is the machine's bug, pinned in the suite forever.
  • Full suite green; the four existing second-controller cases (usb-hub and friends) re-checked against their expectations.
  • Docs updated with the code: device-manager.md, drivers.md (hello), the usb-transfer and block protocol docblocks, device-authority.md as-built appendix.

Order and verification discipline

P0 → P1 → P2 → P3 → P4, one commit per coherent step, suite green at every phase boundary (P0 and P1 may share a verification run — P0 alone changes no behavior). Every new test must be shown to fail against the old behavior before it counts (the discrimination rule). One QEMU suite at a time. The Ryzen re-test at the end is the acceptance run: mouse moves, and killing one bus instance from a shell one day only blinks the devices on that controller.