device-manager: hold the seeded hardware so none is left lying around

A device nobody holds can be claimed by anyone, so the manager now takes
every firmware-discovered device that carries mappable resources, whether or
not a driver wants it. The real gap was the HPET: an MMIO window, an IRQ, no
user-space driver, and there for the taking. Held by the manager it is
inert; unheld it was a way into physical memory.

Two deliberate exclusions. The loader's framebuffer, which the compositor
claims and which the manager must not take because it starts first. And
anything with no resources, which grants nothing worth holding.

Scope is the boot snapshot. A device reported later and matched to no driver
stays claimable — pci-cap-test and iommu-fault-test both reach an unmatched
NIC that way, so narrowing it is a separate change with those fixtures in
scope. Recorded in the plan rather than left implied.

The attacker fixture gains the assertion deferred since D2: after the system
settles, nothing with resources may be taken.

That assertion defeated itself twice before it worked, and both failures are
worth remembering. First it swept at 0.029 while the manager did not bind
its protocol until 0.047, so it reported a hole that closed a millisecond
later. The retry loop that "fixed" that was worse: the first pass TAKES the
device, so the second finds it unavailable because this process now holds
it, and concludes all is well — it passed with the manager's claiming
removed entirely. It now settles once and sweeps once, and fails when the
claiming is removed.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 22:42:42 +01:00
parent ba195fa0a2
commit df9c1ed827
5 changed files with 64 additions and 3 deletions
@@ -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 {