From b7d97ebb5d330ecc1df0b943f915789293e018f9 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:54:45 +0100 Subject: [PATCH] acpi: discovery is handed its node like every other driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/bounds-track-plan.md | 53 +++++++++---------- system/kernel/tests.zig | 12 ++++- system/services/acpi/acpi.zig | 26 +++++---- .../device-manager/device-manager.zig | 15 +++++- 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/docs/bounds-track-plan.md b/docs/bounds-track-plan.md index 0af5806..dd3f653 100644 --- a/docs/bounds-track-plan.md +++ b/docs/bounds-track-plan.md @@ -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 | | 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 | -| 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 | | 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 | | 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 | @@ -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 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]`", -"discovery has no assignment" — which hid that they are the same fact. +The 8042 is one controller described by two ACPI nodes, so it cannot be split across +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 | -|---|---|---| -| Per-device driver | `pci-bus`, `usb-xhci-bus`, `virtio-gpu`, `usb-hid`, `usb-storage` | assigned one id, one instance per device — **all converted** | -| Singleton that finds its own | `ps2-bus`, discovery | spawned once with `no_device`, walks the table itself | +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 — +it is handed over at spawn like everything else. -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 -> spawned — spawn it once, no device assignment. +`device_claim` now has exactly two callers: the device manager, which is the acquirer +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 -nothing in its place**, which is the whole of the blockage. +Closing `device_claim` breaks the compositor's boot floor. Exempting it puts a hole in +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 -chose not to assign either, so it already knows which devices a singleton wants. An -answer is therefore in reach — hand a singleton each matching device as it matches. The -cost: only the first can ride the spawn, so later ones arrive while the driver is -running, and `ps2-bus` enumerates once at startup and would have to tolerate that. - -**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. +The likely answer is neither. **The framebuffer is not a device** — it is where pixels +go, handed over by the loader, and the kernel wraps it in a `DeviceClass.display` +descriptor only so `mmio_map` can hand it over write-combining. If that is right, it +should leave the device table rather than be exempted from its rules, and the compositor +should receive the pixels some other way. That is a change to the display path, which is +out of scope for this run. ### Settled 2026-08-08: the grant rides `system_spawn` (D0) diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 9f4ee11..e8ccf80 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -3098,7 +3098,17 @@ fn acpiParseTest(boot_information: *const BootInformation) void { while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse 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; break; } diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 9c0545b..de5d01f 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -104,11 +104,19 @@ fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { } pub fn main(init: process.Init) void { - // When the acpi-parse scenario spawns this directly, argv[1] is a device-count - // *floor* to self-verify against. The kernel no longer parses AML, so there is - // no exact count to match — proving the ring-3 parse found at least a floor of - // devices 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; + // When the acpi-parse scenario spawns this directly, it passes `floor:N` — a + // device-count floor to self-verify against. The kernel no longer parses AML, so + // there is no exact count to match; proving the ring-3 parse found at least N + // Device objects is the check. Deterministic, no log-scraping. + // + // 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 { _ = 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"); 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; - 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 // "FACP" header — decision 3), the io_port grant, and the SCI irq. diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 0692992..a374cff 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -257,6 +257,7 @@ const delegated_drivers = [_][]const u8{ "pci-bus", "virtio-gpu", "ps2-bus", + "discovery", }; /// 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 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; for (buffer[0..count]) |descriptor| { 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 // 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. - addDriver("discovery", device_manager_protocol.no_device, false); + addDriver("discovery", tables_node, false); if (test_restart_mode) { // The driver-restart scenario's fixture: claims device 0 (the tree