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