From 0d5a7394efe61620060e162e903968b7c318f417 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:26:53 +0100 Subject: [PATCH] =?UTF-8?q?docs:=20establishment=20planes=20=E2=80=94=20th?= =?UTF-8?q?e=20design=20and=20the=20two-seam=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/establishment-planes-plan.md | 122 +++++++++++++++++++++++++++ docs/os-development/communication.md | 35 +++++++- 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/establishment-planes-plan.md diff --git a/docs/establishment-planes-plan.md b/docs/establishment-planes-plan.md new file mode 100644 index 0000000..6b14c7d --- /dev/null +++ b/docs/establishment-planes-plan.md @@ -0,0 +1,122 @@ +# Establishment planes: the plan + +*2026-08-09. The design is settled in +[communication.md](os-development/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 `PI` 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. diff --git a/docs/os-development/communication.md b/docs/os-development/communication.md index 2e241b8..c2b5774 100644 --- a/docs/os-development/communication.md +++ b/docs/os-development/communication.md @@ -55,7 +55,40 @@ source/destination addressing internally at L0 — the way IP runs under TCP — and none of it surfaces into the packet header. Protocols stay ignorant of distance. -## The transport (L0): a buffer and a doorbell +## Establishment: two planes, one namespace + +`target` routes objects *within* a provider. It cannot route *between* provider +processes, because "who am I talking to" was decided at establishment — so when a +protocol's objects live in several processes, something must resolve establishment +to the right one. A real machine forced the question: three xHCI controllers, +three `usb-xhci-bus` processes, one `usb-transfer` name, and whichever instance +bound first owned every class driver's discovery — a mouse on the second +controller was unreachable (`could not open device 50`). The registry's exclusive +bind was built for parties that are genuinely singular; per-device driver +processes are deliberately *not* singular — one process per device found is what +makes a single driver recompilable and restartable while the machine runs. + +So establishment has two planes, and the namespace stays instance-free on both: + +- **Applications establish by name.** `/protocol/input`, `/protocol/display`, + `/protocol/ufs` — served by *services*, the layer that aggregates + (driver-model.md: services face applications). These are genuinely singular, + the registry's exclusive bind is correct for them, and multiplicity behind + them is `target`'s job. Ten mice still merge into one `/protocol/input`. +- **Drivers establish by topology.** A driver instance *creates its channel* — + its own endpoint — and hands it up in the `hello` it already sends, as a + capability. The device manager is the establishment router for the device + tree because it already owns the routing fact: it knows which instance + reported which child. A consumer's `hello` for device N returns a capability + to N's provider. The name `usb-transfer` names the *contract*; no process + binds it. + +Establishment is always a synchronous call, and always consumer-initiated: the +kernel carries one capability in each direction of a call (request and reply), +and deliberately none on a push — a provider cannot foist a channel on anyone. +Re-establishment after a provider restart is therefore also consumer-initiated: +the consumer's channel dies with the provider, and a fresh `hello` fetches the +successor's. Strip any transport to its skeleton and the same two parts remain: