library: the harness keeps the subscribers, and an id belongs to whoever opened it

Three services had each written the same thing and got it three different
ways: input polled the process list to notice a dead subscriber, and only
when someone else subscribed; the power service never noticed at all; the
device manager noticed drivers but not subscribers. The harness owns the
table now, driven by the events a protocol declares — it registers on the
reserved verb, frames each event once, posts to everyone interested without
waiting on any of them, and reclaims a slot when the kernel says its owner
died. Interest masks moved to the envelope, so a subscriber that wants only
mice asks the same way everywhere.

Two consequences the plan had not foreseen. The device manager now hears a
supervised child's death twice, once as its supervisor and once as a
subscriber, so restart backoff counted every crash twice and gave up after
half as many; it retires the id before counting. And the kernel's published
exit table had eight slots for what is now six subscriptions in a plain
boot, so it holds sixteen.

The other half is a hole the design named early and left standing: a
backend handed out a small integer and then honoured it from anyone. A
process that guessed a file's node id read another client's file; a display
layer had no owner at all, so any client could reconfigure or destroy any
layer; a USB device token was never checked against the client that opened
it. Each is now bound to the task that opened it, and a wrong owner gets
exactly what an unknown id gets — the refusal must not become the oracle
the identical answers elsewhere were designed to remove. Closing a file
changed with it: it used to succeed unconditionally, which would have told
a caller which ids existed.

Suite 111/111, with a new case in which one process holds a file and a
layer, hands both ids to a second process, and finds them untouched after
that process has tried everything with them.
This commit is contained in:
Daniel Samson
2026-08-01 09:05:26 +01:00
parent 2719b93530
commit 1b1c587c14
29 changed files with 1072 additions and 335 deletions
+45 -7
View File
@@ -36,11 +36,11 @@ plain `main` checkout always tells the truth about where the work is.**
| | |
|---|---|
| Working on | **P4c** — harness subscriber lift + badge-scoped client ids |
| Branch carrying it | `feat/security-group-3` (pushed to origin) |
| On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged |
| Awaiting merge | P4a, P4b — land with the group 3 merge |
| Suite | 110 cases, all passing |
| Working on | **H2** — SMEP (group 4) |
| Branch carrying it | `feat/security-group-4` (cut next) |
| On `main` | everything through P4c — groups 1, 2 and 3 merged |
| Awaiting merge | nothing |
| Suite | 111 cases, all passing |
| Last updated | 2026-08-01 |
A checkbox below means the phase met its definition of green and was
@@ -67,8 +67,19 @@ group boundary.
- [x] **merge** group 2 → main, push
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
- [x] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer; every leading operation byte folded into the header, and with it the `device_id`/`device_token` that followed it — `Header.target` now carries the device in all three. device-manager's own `enumerate`/`subscribe` became the reserved verbs and its three `{status, reserved}` reply structs the envelope's `Status`; `ChildAdded` is one struct under two numbers, a call and an event, landing exactly on the 64-byte push floor. power's kinds became one declared event each, the input protocol's shape, so init reads *what happened* from the header; usb-transfer's control data stage moved to the packet tail in both directions, which made `Status.len` the transferred length and `actual_length` redundant. The two silent-breakage sites — init's byte-offset power parse and acpi's `message[0]` dispatch — are gone, the shutdown badge gate unchanged; three more rows in the conformance table. Suite 110/110)
- [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers
- [ ] **merge** group 3 → main, push
- [x] **P4c** — harness subscriber lift + badge-scoped per-client integers (the
subscriber table, the reserved subscribe/unsubscribe verbs, the fan-out and the
dead-subscriber sweep are `service.Subscribers` now; input, acpi and
device-manager deleted three hand-rolled variants and their three different
ideas of when a subscriber goes away, standardizing on published exit
notifications — acpi had no sweep at all and input polled the process list on
every subscribe. The three guessable-id namespaces are scoped to the opening
badge: FAT node ids on every verb that names one, xHCI device tokens on open,
control, bulk and interrupt_subscribe, display layers on configure, fill, blit,
damage and destroy — each refusing a wrong owner with the *same* answer as an id
nobody holds. New `badge-scope` case, two processes of one fixture, every
refusal paired with a control; suite 111/111)
- [x] **merge** group 3 → main, push
- [ ] **H2** — SMEP on every core
- [ ] **HS** — SYSRET canonical-RIP guard
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
@@ -456,6 +467,33 @@ the wire formats will want them:*
node id and asserts refusal; kernel-side unit test for the harness sweep.
Suite 111.
*Landed. Four things the plan had not foreseen:*
- *One sweep idiom means one more kernel subscriber per provider, and the kernel's
published-exit table held **eight**. A normal boot now fields six (fat, input,
power, device-manager, display, and one per xHCI controller), so the table grew
to sixteen. It is not a table anyone notices until a service silently loses its
sweep, which is exactly the failure the old ceiling was two subscriptions away
from.*
- *The device manager hears each of its drivers die **twice** now — it is both the
supervisor its spawn named and, through the harness, a subscriber to published
exits — and the notify ring delivers the two badges separately. Untreated, one
death counted as two: the restart backoff doubled and the crash-loop cap fired
at half the deaths it names. `onDriverExit` therefore retires the dead process
id before it decides anything, and the second notification finds nothing to act
on. (The `driver-restart` and `pci-scan` drills are what would have caught it.)*
- *Refusal-equals-absence has a corollary for the verbs that **release**: FAT's
`close` used to answer 0 for an unknown node, so scoping it had to change that
too — a foreign node and a free one both answer `-ENOENT`, or the pair would
have been an oracle for which ids are live. The same applies to the harness's
`unsubscribe`.*
- *Ownership is per **task**, not per process, because the badge is: the kernel
stamps the sending thread's id, which is already the granularity of the exit
sweep that releases the state (a worker thread's death releases the handles that
worker opened). Nothing in the tree shares an id across its own threads today;
a per-process notion would need the kernel to stamp the leader, and belongs with
P5's spawner-wired namespaces if it is ever wanted.*
## H2 — SMEP
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to