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:
@@ -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** |
|
| 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** |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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
|
re-acquire. E4 must precede E5, or the attacker will find takeable devices and the
|
||||||
assertion will be wrong about why. E6 is cleanup.
|
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
|
### Not in this run, and not blocking it
|
||||||
|
|
||||||
**Zero-resource devices stay in the kernel.** Moving them out needs an answer to who
|
**Zero-resource devices stay in the kernel.** Moving them out needs an answer to who
|
||||||
|
|||||||
@@ -4334,6 +4334,11 @@ fn deviceAuthorityTest(boot_information: *const BootInformation) void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
process.setInitialRamdisk(image);
|
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"));
|
check("device-authority-test spawned", spawnNamedWithArg(rd, "device-authority-test", "run"));
|
||||||
|
|
||||||
// The VERDICT prefix matters: the fixture prints one "device-authority: ok <name>"
|
// The VERDICT prefix matters: the fixture prints one "device-authority: ok <name>"
|
||||||
|
|||||||
@@ -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;
|
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)) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
const exe = build_support.userBinary(b, .{
|
const exe = build_support.userBinary(b, .{
|
||||||
.name = "device-authority-test",
|
.name = "device-authority-test",
|
||||||
.root_source_file = b.path("device-authority-test.zig"),
|
.root_source_file = b.path("device-authority-test.zig"),
|
||||||
.imports = &.{ "driver", "logging", "process" },
|
.imports = &.{ "driver", "logging", "process", "time" },
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const std = @import("std");
|
|||||||
const device = @import("driver");
|
const device = @import("driver");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const process = @import("process");
|
const process = @import("process");
|
||||||
|
const time = @import("time");
|
||||||
|
|
||||||
fn line(comptime format: []const u8, arguments: anytype) void {
|
fn line(comptime format: []const u8, arguments: anytype) void {
|
||||||
var buffer: [160]u8 = undefined;
|
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);
|
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) {
|
if (failures == 0) {
|
||||||
line("device-authority: VERDICT ok ({d} devices, none of them mine)\n", .{seen});
|
line("device-authority: VERDICT ok ({d} devices, none of them mine)\n", .{seen});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user