docs: device authority — the how, and the run that deletes the ceilings

device-authority.md is rewritten as an implementation design rather than a
rival to device-manager.md. The what was already settled there in 2026-07:
structure in the manager, authority in the kernel, and delegation as the
step after hello. This is the how, plus the two decisions that paragraph
leaves open.

Decision 1: the manager claims, it is not granted. device-manager.md says
"claims (or is granted)"; claiming wins because the manager runs before any
driver exists and takes the seeded devices unopposed, leaving nothing unheld
to race for. One new call, device_transfer(device_id, task_id), checks only
that the caller holds the device — no names in the kernel, no attestation.
The alternative put a binary path inside the kernel, and the kernel should
hold only what cannot safely live in user space. The residual is stated
rather than hidden: authority rests on the manager claiming first, which
init.csv makes an operator-visible ordering rather than an attacker-
controlled one, and the enforced version arrives with the spawn capability
drivers.md already names as missing.

Decision 2: the kernel stops holding inventory. It reads three things out of
a descriptor — physical ranges, interrupt numbers, one PCI BDF — and stores
the rest only so device_enumerate can hand it back. Devices with no
resources leave the kernel entirely: a USB device conveys no mapping
authority, so there is nothing to enforce. That is also the case which
sidesteps containment, and therefore the reason a shared cap existed.

Decision 3: no shared ceiling. The table becomes dynamic — it is built after
heap.init, so nothing ever prevented it — and the two invented numbers go. A
per-holder quota replaces them, because dynamic storage with no bound moves
the ceiling to the kernel heap, which is shared and fatal rather than
partial. A bound charged to whoever caused it is isolation.

Two earlier drafts of this document are gone: one gave init the root grants,
the other proposed extracting a firmware-framebuffer driver. Both were wrong
and both are recorded as wrong in the run plan's settled list — the
framebuffer is not a device, it is where pixels go until a real display
driver announces itself.

Run 2 is nine steps, ordered so the suite stays green throughout: build and
prove the transfer mechanism, move the five claimants across one at a time,
then the flag day, then the inventory, then the ceilings.
This commit is contained in:
Daniel Samson
2026-08-08 17:01:25 +01:00
parent 5ad42ceac3
commit 547d0ec46b
2 changed files with 181 additions and 175 deletions
+58 -1
View File
@@ -19,11 +19,68 @@ next one starts.*
| L5 | USB: interfaces from the descriptor, and the misattributed-endpoint bug | **done** — fix is by construction; no direct test, see open question 5 |
| L6 | xHCI: a failed `allocateDevice` stops leaking an enabled slot | **done** — path forced and verified; no regression test, see open question 5 |
**Run complete.** L1 stopped (the step was wrong), L2–L6 landed. Suite 115 → 116.
**Run 1 complete.** L1 stopped (the step was wrong), L2–L6 landed. Suite 115 → 116.
Allowlist 278 → 269. Two steps ship without a permanent regression test, both because
QEMU's USB devices are too small to reach the paths — see open question 5, which is the
audit's own lesson recurring: the test rig is smaller than a real machine.
---
## Run 2 — device authority: delete the invented ceilings
*Design: [device-authority.md](os-development/device-authority.md), which is the **how**
for the delegation step [device-manager.md](device-driver-development/device-manager.md)
already settled. Read both before starting; the second is authoritative where they
differ.*
The goal, in the project owner's words: **remove the maximum values we set arbitrarily,
move the responsibility to the device manager, and keep in the kernel only the parts
that cannot safely run in user space.**
| Step | What | State |
|---|---|---|
| D1 | `device_transfer(device_id, task_id)` — the holder gives a device away | not started |
| D2 | Adversarial case: a process handed nothing is refused, on a held device and a free one | not started |
| D3 | The manager claims the seeded devices at boot, before any driver is spawned | not started |
| D4 | `usb-xhci-bus` receives its controller in the `hello` reply instead of claiming argv[1] | not started |
| D5 | The other four claimants converted: `pci-bus`, `ps2-bus`, `virtio-gpu`, `acpi` | not started |
| D6 | `device_claim` refuses a device the caller was not handed; the hole is closed | not started |
| D7 | Zero-resource devices stop being kernel objects — inventory moves to the manager | not started |
| D8 | **`maximum_children_per_parent` deleted** — the authorisation it stood in for exists | not started |
| D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | not started |
Ordering is load-bearing. D1–D2 build and prove the mechanism with nothing depending on
it. D3–D5 move each claimant across one at a time, so the suite stays green throughout
and a regression names the driver that caused it. D6 is the flag day. D7 must precede
D9, because zero-resource children are the case that sidesteps containment and so the
reason a shared cap was needed at all.
### Settled, so the run does not re-litigate them
- **The manager claims, it is not granted.** No binary names in the kernel; the rule is
"you may give away what you hold". The residual — it rests on the manager claiming
first — is stated in the design and is closed later by the spawn capability
[drivers.md](device-driver-development/drivers.md) already names as missing.
- **The framebuffer is not a device.** It is where pixels go, handed over by the loader,
and the compositor uses it as the boot floor until a real display driver announces
itself. Nothing in this run touches the display service or its GOP path.
- **`maximum_endpoints_per_interface` and the wire structs stay.** Widening them is a
protocol change, out of scope.
- **A per-holder quota is not a retreat.** Dynamic storage with no bound moves the
ceiling to the kernel heap, which is shared and fatal rather than partial. A bound
charged to the task that caused it is isolation, and it is declared through
[bounds.md](os-development/bounds.md) like anything else.
### Working rules
As Run 1, unchanged: work in `/Users/danielsamson/Gitea/daniel/danos` on
`claude/bounds-track`; every step lands with a test that fails before the fix, verified
by restoring the old behaviour; full suite green before each commit; never two suites at
once (`pgrep -f qemu_test.py`); 60 GiB free; `git commit -F` with no `Co-Authored-By`;
update this table before starting the next step. **If a step needs a decision that is
not written down, stop it, add the question below, and move on** — Run 1's first step
was wrong and stopping was the right call.
**Suite:** 115/115 at the start of the run.
**Branch:** `claude/bounds-track`.