diff --git a/docs/bounds-track-plan.md b/docs/bounds-track-plan.md index c57192c..2e37820 100644 --- a/docs/bounds-track-plan.md +++ b/docs/bounds-track-plan.md @@ -135,14 +135,25 @@ giver, and its comment says so rather than implying a protection it is not provi | E1 | Record a giver per device; `device_transfer` and the spawn grant set it — **done** | | E2 | On task death a device reverts to its giver if alive, else its claim clears — **done** | | E3 | `device_claim` refuses a device that has a giver — **done**, but unreachable: E2 already closed the window | -| E4 | The manager claims every resource-bearing device at boot, so nothing is left takeable | -| E5 | The attacker fixture gains the claim half it has been waiting for since D2 | +| E4 | The manager claims every resource-bearing device at boot, so nothing is left takeable — **done** (boot snapshot only; see below) | +| E5 | The attacker fixture gains the claim half it has been waiting for since D2 — **done with E4** | | E6 | Delete the delegated-set scaffolding — every driver is delegated now | Ordering: E1 alone changes no behaviour. E2 must precede E3, or a restart cannot re-acquire. E4 must precede E5, or the attacker will find takeable devices and the assertion will be wrong about why. E6 is cleanup. +### E4's scope, stated + +It covers the **boot snapshot**. A device *reported* later and matched to no driver +stays claimable — `pci-cap-test` and `iommu-fault-test` both rely on that to reach an +unmatched NIC. Narrowing it further is a separate change with those fixtures in scope. + +The real gap it closed was the **HPET**: an MMIO window, an IRQ, no user-space driver, +and claimable by anyone. Excluded on purpose: the loader's framebuffer (the manager +starts before display, so taking it would break the boot screen) and anything with no +resources, which grants nothing. + ### Not in this run, and not blocking it **Zero-resource devices stay in the kernel.** Moving them out needs an answer to who diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 7681ce4..7d03c72 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -4334,6 +4334,11 @@ fn deviceAuthorityTest(boot_information: *const BootInformation) void { }; process.setInitialRamdisk(image); + // The manager must be up: it is what holds the seeded hardware, and without it + // every device would be lying around unheld and the last assertion would have + // nothing to observe — a test that cannot fail. + check("registry (init) spawned", spawnRegistry(rd)); + check("device-manager spawned", spawnNamed(rd, "device-manager")); check("device-authority-test spawned", spawnNamedWithArg(rd, "device-authority-test", "run")); // The VERDICT prefix matters: the fixture prints one "device-authority: ok " diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index a374cff..daf0c19 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -433,6 +433,28 @@ fn initialise(endpoint: ipc.Handle) bool { } } + // **Hold the firmware-discovered hardware, so none of it is left lying around.** + // A device nobody holds can be claimed by anyone, so every seeded device that + // carries mappable resources is taken here whether or not a driver wants it — the + // HPET most of all, which has an MMIO window and an IRQ and no user-space driver. + // Held by the manager it is inert; unheld it was there for the taking. + // + // Two deliberate exclusions: + // - the loader's framebuffer, which the compositor claims and which is not + // hardware anyone is delegated (the manager starts before display, so taking + // it here would break the boot screen); + // - anything with no resources, which grants nothing and so is not worth holding. + // + // This covers the boot snapshot only. A device *reported* later and matched to no + // driver stays claimable — the pci-cap and iommu-fault fixtures rely on exactly + // that to reach an unmatched NIC. Narrowing it further is a separate change with + // those fixtures in scope. + for (buffer[0..count]) |descriptor| { + if (descriptor.resource_count == 0) continue; + if (descriptor.class == @intFromEnum(device.DeviceClass.display)) continue; + device.claim(descriptor.id) catch continue; // already held, or not ours to take + } + var matched: usize = 0; for (buffer[0..count]) |descriptor| { if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) { diff --git a/test/system/services/device-authority-test/build.zig b/test/system/services/device-authority-test/build.zig index 1b19cfd..b5dabc0 100644 --- a/test/system/services/device-authority-test/build.zig +++ b/test/system/services/device-authority-test/build.zig @@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void { const exe = build_support.userBinary(b, .{ .name = "device-authority-test", .root_source_file = b.path("device-authority-test.zig"), - .imports = &.{ "driver", "logging", "process" }, + .imports = &.{ "driver", "logging", "process", "time" }, }); b.installArtifact(exe); } diff --git a/test/system/services/device-authority-test/device-authority-test.zig b/test/system/services/device-authority-test/device-authority-test.zig index 8dc10f5..01fd317 100644 --- a/test/system/services/device-authority-test/device-authority-test.zig +++ b/test/system/services/device-authority-test/device-authority-test.zig @@ -37,6 +37,7 @@ const std = @import("std"); const device = @import("driver"); const logging = @import("logging"); const process = @import("process"); +const time = @import("time"); fn line(comptime format: []const u8, arguments: anytype) void { var buffer: [160]u8 = undefined; @@ -88,6 +89,28 @@ fn run() void { check("and no child was left behind by the refusal", process.processes(&process_table) == before); } + // 5. **Nothing with mappable resources is left lying around.** A device nobody + // holds can be claimed by anyone, so the manager takes every seeded device that + // carries resources — the HPET above all, which has an MMIO window and an IRQ + // and no user-space driver. The one exception is the loader's framebuffer, + // which the compositor claims. So from here, a resource-bearing device should + // refuse to be taken, and the reason should be that someone already has it. + // Settle first, then sweep **once**. The manager is still starting when this + // fixture is spawned, so an immediate sweep finds hardware unheld and reports a + // hole that closes a millisecond later. Retrying until the sweep comes back + // empty is worse than useless: the first pass *takes* the device, so the second + // finds it unavailable — because this process now holds it — and concludes all + // is well. One sweep, after a wait long enough for the manager to have claimed. + time.sleepMillis(1500); + var takeable: usize = 0; + for (table[0..seen]) |descriptor| { + if (descriptor.resource_count == 0) continue; + if (descriptor.class == @intFromEnum(device.DeviceClass.display)) continue; + device.claim(descriptor.id) catch continue; // refused, as it should be + takeable += 1; + } + check("no resource-bearing device is left for the taking", takeable == 0); + if (failures == 0) { line("device-authority: VERDICT ok ({d} devices, none of them mine)\n", .{seen}); } else {