acpi: discovery is handed its node like every other driver

The last claimant. The kernel seeds the acpi-tables node, so it sits in the
same boot snapshot the manager already scans to find the PCI host bridge —
there was never a bootstrap problem, only a lookup nobody had written. The
manager claims it and names it in the spawn; the service stops claiming.

Every driver in the system now receives its hardware rather than taking it.

Two failures on the way, both mine. addDriver puts the device id in argv[1],
and the acpi service read argv[1] as a self-verify device-count floor — so
handed device 7 it decided it was in test mode, printed "acpi-parse: ok",
and never reported a device. The test argument is now floor:N, which a bare
id cannot be mistaken for.

And acpi-parse spawns the service directly rather than through the manager,
so nothing handed it the node. That test now claims and transfers it exactly
as the manager does, which is the right shape: the test plays the manager's
role instead of the service reaching for hardware.

device_claim now has two callers left: the manager, which is the acquirer
and should have it, and the display service's GOP path. That is recorded as
question 10 — the framebuffer is not a device, so the answer is likely that
it leaves the device table rather than being exempted from its rules.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 21:54:45 +01:00
parent 6b3a381626
commit b7d97ebb5d
4 changed files with 67 additions and 39 deletions
+25 -28
View File
@@ -43,10 +43,10 @@ that cannot safely run in user space.**
| D2 | Adversarial case: a process handed nothing is refused, on a held device and a free one | **done** — `device-authority-test`; the claim half joins it at D6 | | D2 | Adversarial case: a process handed nothing is refused, on a held device and a free one | **done** — `device-authority-test`; the claim half joins it at D6 |
| D3 | The manager claims the seeded devices at boot, before any driver is spawned | **merged into D4** — see below | | D3 | The manager claims the seeded devices at boot, before any driver is spawned | **merged into D4** — see below |
| D4 | The manager claims + delegates on `hello`; `usb-xhci-bus` is the first driver converted | **done** — caught an IOMMU regression I introduced; see below | | D4 | The manager claims + delegates on `hello`; `usb-xhci-bus` is the first driver converted | **done** — caught an IOMMU regression I introduced; see below |
| D5 | The other four claimants converted: `pci-bus`, `ps2-bus`, `virtio-gpu`, `acpi` | **partial** — `pci-bus` + `virtio-gpu` done; `ps2-bus` and discovery need question 9 | | D5 | The other four claimants converted: `pci-bus`, `ps2-bus`, `virtio-gpu`, `acpi` | **done** — every driver now receives its hardware; question 9 answered |
| D0 | The grant rides `system_spawn` — atomic, so no driver need change to receive one | **done** — and caught a test-marker bug that made the D2 fixture unfailable | | D0 | The grant rides `system_spawn` — atomic, so no driver need change to receive one | **done** — and caught a test-marker bug that made the D2 fixture unfailable |
| D10 | Every driver hellos, on its own merits (liveness, one class of driver) | not started — optional, independent | | D10 | Every driver hellos, on its own merits (liveness, one class of driver) | not started — optional, independent |
| D6 | `device_claim` refuses a device the caller was not handed; the hole is closed | not started | | D6 | `device_claim` refuses a device the caller was not handed; the hole is closed | **blocked on question 10** — only the display service's GOP path still claims |
| D7 | Zero-resource devices stop being kernel objects — inventory moves to the manager | **blocked** — nothing else mints their ids; see question 8 | | D7 | Zero-resource devices stop being kernel objects — inventory moves to the manager | **blocked** — nothing else mints their ids; see question 8 |
| D8 | **`maximum_children_per_parent` deleted** | **done** — it was unblocked from the moment D9 landed; I kept reading my own stale label | | D8 | **`maximum_children_per_parent` deleted** | **done** — it was unblocked from the moment D9 landed; I kept reading my own stale label |
| D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | **done** — one of the two invented numbers is gone | | D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | **done** — one of the two invented numbers is gone |
@@ -91,38 +91,35 @@ They land together, with the manager claiming only for drivers in an explicit
anything in D4 — recorded rather than dismissed, because D4 moved the `hello` earlier anything in D4 — recorded rather than dismissed, because D4 moved the `hello` earlier
and so did shift boot timing. Watch it across the remaining steps. and so did shift boot timing. Watch it across the remaining steps.
### Open question 9 — danos has two device-acquisition patterns; D6 fits only one ### Question 9 — answered: a singleton gets every device that matched it
Earlier versions of this question listed symptoms — "`ps2-bus` ignores its `argv[1]`", The 8042 is one controller described by two ACPI nodes, so it cannot be split across
"discovery has no assignment" — which hid that they are the same fact. processes. The manager now hands the single `ps2-bus` instance **every** matching node:
the first rides the spawn, the rest are transferred to the running instance. Late
arrival is safe because the ordering is natural rather than lucky — the controller node
carries the ports and is needed at once, the mouse node not until after identify
(measured: handed over at 0.336, first touched at 0.456). The count is whatever matched,
so a machine with no PS/2 ports or one port needs no special case.
| Pattern | Who | How it gets its device | Discovery turned out not to be special either. The kernel seeds the `acpi-tables` node,
|---|---|---| so it is in the same boot snapshot the manager already scans for the PCI host bridge —
| Per-device driver | `pci-bus`, `usb-xhci-bus`, `virtio-gpu`, `usb-hid`, `usb-storage` | assigned one id, one instance per device — **all converted** | it is handed over at spawn like everything else.
| Singleton that finds its own | `ps2-bus`, discovery | spawned once with `no_device`, walks the table itself |
The second is deliberate, not an oversight. `onChildAdded` says so: ### Open question 10 — the framebuffer is the last thing anyone claims
> An hid-matched driver (ps2-bus) is a singleton that finds its own devices once `device_claim` now has exactly two callers: the device manager, which is the acquirer
> spawned — spawn it once, no device assignment. and should have it, and `system/services/display/backend.zig`, whose GOP path claims the
kernel-seeded display node.
`device_claim` is the mechanism that makes that pattern work. **D6 removes it and puts Closing `device_claim` breaks the compositor's boot floor. Exempting it puts a hole in
nothing in its place**, which is the whole of the blockage. the middle of the authority model, in the one place an exemption is most expensive.
The manager is not ignorant: it matched *both* `PNP0303` and `PNP0F13` to `ps2-bus` and The likely answer is neither. **The framebuffer is not a device** — it is where pixels
chose not to assign either, so it already knows which devices a singleton wants. An go, handed over by the loader, and the kernel wraps it in a `DeviceClass.display`
answer is therefore in reach — hand a singleton each matching device as it matches. The descriptor only so `mmio_map` can hand it over write-combining. If that is right, it
cost: only the first can ride the spawn, so later ones arrive while the driver is should leave the device table rather than be exempted from its rules, and the compositor
running, and `ps2-bus` enumerates once at startup and would have to tolerate that. should receive the pixels some other way. That is a change to the display path, which is
out of scope for this run.
**The decision: do singletons stop being singletons — one instance per device, like
everything else — or does the system keep a second acquisition path for them?** The
first is uniform and costs a rewrite of `ps2-bus`'s startup. The second keeps a
mechanism whose only remaining users are two drivers, and every exemption in an
authority model is somewhere the model does not hold.
Nothing else blocks D6. Both invented ceilings are already gone, so this is about
closing the claiming hole, not about a number.
### Settled 2026-08-08: the grant rides `system_spawn` (D0) ### Settled 2026-08-08: the grant rides `system_spawn` (D0)
+11 -1
View File
@@ -3098,7 +3098,17 @@ fn acpiParseTest(boot_information: *const BootInformation) void {
while (i < rd.count) : (i += 1) { while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue; const item = rd.entry(i) orelse continue;
if (!eql(initial_ramdisk.basename(item.name), "discovery")) continue; if (!eql(initial_ramdisk.basename(item.name), "discovery")) continue;
_ = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "1" }, scheduler.currentId(), null) catch 0; // Hand it the acpi-tables node the way the manager would: claim it here, then
// move it to the child. Discovery no longer claims for itself.
var scratch: [64]device_abi.DeviceDescriptor = undefined;
const seen_devices = devices_broker.enumerate(&scratch);
const tables: ?u64 = for (scratch[0..@min(seen_devices, scratch.len)]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.acpi_tables)) break d.id;
} else null;
const me_parse = scheduler.currentId();
if (tables) |node| _ = claimOk(node, me_parse);
const child = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "floor:1" }, me_parse, null) catch 0;
if (tables) |node| devices_broker.transfer(node, me_parse, child) catch {};
spawned = true; spawned = true;
break; break;
} }
+17 -9
View File
@@ -104,11 +104,19 @@ fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
} }
pub fn main(init: process.Init) void { pub fn main(init: process.Init) void {
// When the acpi-parse scenario spawns this directly, argv[1] is a device-count // When the acpi-parse scenario spawns this directly, it passes `floor:N` — a
// *floor* to self-verify against. The kernel no longer parses AML, so there is // device-count floor to self-verify against. The kernel no longer parses AML, so
// no exact count to match — proving the ring-3 parse found at least a floor of // there is no exact count to match; proving the ring-3 parse found at least N
// devices is the check. Deterministic, no log-scraping. // Device objects is the check. Deterministic, no log-scraping.
const floor: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null; //
// The `floor:` prefix matters. argv[1] is the assigned device id for every driver
// the manager spawns, so a bare number here would be read as a floor — which is
// exactly what happened when discovery started being given its node: it saw
// argv[1] = "7", decided it was in self-verify mode, and never reported a device.
const floor: ?usize = if (init.arguments.get(1)) |a| blk: {
if (!std.mem.startsWith(u8, a, "floor:")) break :blk null;
break :blk std.fmt.parseInt(usize, a["floor:".len..], 10) catch null;
} else null;
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch { const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
_ = logging.write("/system/services/acpi: out of memory\n"); _ = logging.write("/system/services/acpi: out of memory\n");
@@ -118,11 +126,11 @@ pub fn main(init: process.Init) void {
_ = logging.write("/system/services/acpi: no acpi-tables node to claim\n"); _ = logging.write("/system/services/acpi: no acpi-tables node to claim\n");
return; return;
}; };
// The node arrived with the spawn: the manager holds it and names it in the call
// that creates this process. Discovery was the last thing in the system that
// acquired hardware by naming it rather than being given it
// (docs/os-development/device-authority.md).
node_id = node.id; node_id = node.id;
device.claim(node_id) catch |e| {
std.log.warn("unable to claim acpi-tables: {s}", .{@errorName(e)});
return;
};
// Map the node's resources: the AML blobs (bytecode), the FADT (intact // Map the node's resources: the AML blobs (bytecode), the FADT (intact
// "FACP" header — decision 3), the io_port grant, and the SCI irq. // "FACP" header — decision 3), the io_port grant, and the SCI irq.
@@ -257,6 +257,7 @@ const delegated_drivers = [_][]const u8{
"pci-bus", "pci-bus",
"virtio-gpu", "virtio-gpu",
"ps2-bus", "ps2-bus",
"discovery",
}; };
/// Matched on the **last path component**, because a driver reaches this table under /// Matched on the **last path component**, because a driver reaches this table under
@@ -420,6 +421,18 @@ fn initialise(endpoint: ipc.Handle) bool {
const total = device.enumerate(buffer); const total = device.enumerate(buffer);
const count = @min(total, buffer.len); const count = @min(total, buffer.len);
// The node discovery needs: the kernel seeds it, so it is in this same snapshot
// and can be handed over like any other assignment. Discovery used to find and
// claim it itself — the last driver that acquired hardware by naming it rather
// than being given it (docs/os-development/device-authority.md).
var tables_node: u64 = device_manager_protocol.no_device;
for (buffer[0..count]) |descriptor| {
if (descriptor.class == @intFromEnum(device.DeviceClass.acpi_tables)) {
tables_node = descriptor.id;
break;
}
}
var matched: usize = 0; var matched: usize = 0;
for (buffer[0..count]) |descriptor| { for (buffer[0..count]) |descriptor| {
if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) { if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) {
@@ -441,7 +454,7 @@ fn initialise(endpoint: ipc.Handle) bool {
// under the neutral name "discovery", spawned once at startup. It finds and // under the neutral name "discovery", spawned once at startup. It finds and
// claims the acpi-tables (or devicetree-blob) node itself. Not a per-device // claims the acpi-tables (or devicetree-blob) node itself. Not a per-device
// match — it is the discoverer, not a driver bound to one device. // match — it is the discoverer, not a driver bound to one device.
addDriver("discovery", device_manager_protocol.no_device, false); addDriver("discovery", tables_node, false);
if (test_restart_mode) { if (test_restart_mode) {
// The driver-restart scenario's fixture: claims device 0 (the tree // The driver-restart scenario's fixture: claims device 0 (the tree