kernel: fix pci-scan triple fault and flaky restart-drill checks

pciScanTest put three [64]DeviceDescriptor arrays on the stack; at 328
bytes each that is a 66,096-byte frame, larger than the 64 KiB bootstrap
stack the boot context runs on, so the prologue overflowed the stack and
triple-faulted on entry before the test could print anything — the CPU
reset with no exception line. Reuse one descriptor buffer (frame ~21 KiB).

With the fault gone, the restart-drill checks proved flaky. They polled
process.write_buffer (which holds only the latest write-syscall message)
for the transient restarting/re-scan lines, which the concurrent
class-driver output overwrote before the starved low-priority poller could
observe them. The kernel broker count can't witness the restart either:
register is idempotent and the table has no unregister, so the count is
invariant across kill/prune/respawn.

Follow the sibling restart drills (usbReportTest/driverRestartTest): assert
the ordered drill in the harness regex over the full serial log (a
backreference requires the respawn to re-scan the same count), and keep only
race-free broker checks in the kernel — empty before the scan, populated
once it settles, never growing past N — sampled via scheduler.sleep at a
fixed cadence instead of a busy-poll.
This commit is contained in:
Daniel Samson 2026-07-14 13:48:33 +01:00
parent 0217662808
commit 24c49f56e1
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
2 changed files with 71 additions and 70 deletions

View File

@ -238,6 +238,19 @@ fn bufferHas(needle: []const u8) bool {
return std.mem.indexOf(u8, process.write_buffer[0..process.write_len], needle) != null;
}
/// How many `pci_device` functions the devices broker currently holds. A durable
/// snapshot, unlike a `bufferHas` poll of the single-latest write_buffer line, so a
/// test can wait on it without racing transient log output. `scratch` is
/// caller-owned to keep the (large) descriptor array off this helper's own frame.
fn brokerPciCount(scratch: []device_abi.DeviceDescriptor) u32 {
const k = @min(devices_broker.enumerate(scratch), scratch.len);
var count: u32 = 0;
for (scratch[0..k]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) count += 1;
}
return count;
}
/// Non-destructive checks of the memory map and frame allocator.
fn smoke(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: smoke\n", .{});
@ -1903,18 +1916,14 @@ fn pciScanTest(boot_information: *const BootInformation) void {
};
// Post-flip (M19.3) ground truth: the kernel no longer enumerates PCI
// functions, so equivalence inverts the broker's function count after
// the scan must equal what the driver itself reported finding.
// functions, so equivalence inverts every PCI function the broker holds was
// put there by the ring-3 driver, so before the driver runs the broker holds
// none. One reusable descriptor buffer (each snapshot is ~20 KiB; three live
// at once would overflow the 64 KiB bootstrap stack this test runs on).
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
var boot_pci: u32 = 0;
for (buffer[0..n]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) boot_pci += 1;
}
check("the kernel seeded no PCI functions (the walk retired)", boot_pci == 0);
check("the kernel seeded no PCI functions (the walk retired)", brokerPciCount(&buffer) == 0);
process.setInitialRamdisk(image);
process.write_count = 0;
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
@ -1925,68 +1934,49 @@ fn pciScanTest(boot_information: *const BootInformation) void {
}
check("device-manager spawned (test-pci-restart mode)", manager != 0);
// First scan: wait for the driver's count line and parse the number.
const count_prefix = "pci-bus: ";
const count_suffix = " functions found";
var reported: u32 = 0;
scheduler.setPriority(1);
// The manager spawns pci-bus, which scans the ECAM window and registers every
// function it finds, so the broker's PCI count climbs from zero and plateaus.
// Wait for it to *settle*: latch N only once the count has held steady for a
// stretch, so a mid-scan sample can't latch a low N that the rest of the same
// scan then appears to exceed. The count is monotonic (registrations only add;
// the table has no unregister) so the plateau is permanent stability is
// reached the moment the scan finishes and holds indefinitely. Sleep between
// samples rather than busy-yield: the boot context outranks the drivers, and a
// busy spin would starve the very processes it waits on; a sleeping task is
// woken by the timer, so the drivers run in between.
const poll_ms = 5;
var registered: u32 = 0;
var steady: u32 = 0;
var deadline = architecture.millis() + 15000;
while (architecture.millis() < deadline and reported == 0) {
const line = process.write_buffer[0..process.write_len];
if (std.mem.indexOf(u8, line, count_prefix)) |start| {
if (std.mem.indexOf(u8, line, count_suffix)) |digits_end| {
reported = std.fmt.parseInt(u32, line[start + count_prefix.len .. digits_end], 10) catch 0;
}
}
scheduler.yield();
while (architecture.millis() < deadline and steady < 60) { // 60 * 5ms = 300ms steady
const now = brokerPciCount(&buffer);
if (now != 0 and now == registered) steady += 1 else steady = 0;
registered = now;
scheduler.sleep(poll_ms);
}
scheduler.setPriority(4);
check("the ring-3 scan reported a function count", reported >= 1);
check("the ring-3 scan registered its PCI functions in the broker", registered >= 1);
// Every reported function was registered: the broker holds exactly them.
var registered: [64]device_abi.DeviceDescriptor = undefined;
const r = @min(devices_broker.enumerate(&registered), registered.len);
var registered_pci: u32 = 0;
for (registered[0..r]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) registered_pci += 1;
// The restart drill the manager kills pci-bus ~1 s after its scan, prunes its
// own child tree, and respawns it to re-claim, re-scan, and re-register is
// asserted by the harness's ordered regex over the whole serial log, the way
// every restart drill is (see usbReportTest / driverRestartTest): the manager's
// kill/prune/respawn lines are transient and would race a write_buffer poll, and
// the *broker* count can't witness the restart at all the table has no
// unregister and register is idempotent (devices-broker.zig), so the kill leaves
// the nodes in place and the respawn's re-registration dedupes against them.
//
// That idempotence is exactly this test's kernel-side claim: watch, across the
// whole drill, that the count never grows past N. A broken dedup would append
// the re-scanned functions as duplicates (N -> 2N), and with no unregister that
// overshoot would persist so a single late sample would catch it; the loop is
// belt-and-braces over the ~2 s the kill + backoff + respawn takes.
var duplicated = false;
deadline = architecture.millis() + 5000;
while (architecture.millis() < deadline and !duplicated) {
if (brokerPciCount(&buffer) > registered) duplicated = true;
scheduler.sleep(20);
}
check("the broker holds exactly the reported functions", registered_pci == reported);
const kernel_count = reported; // the no-duplicate check below reuses it
// The restart drill: the manager kills pci-bus after its reports; the
// respawn re-claims, re-scans, and re-registers.
const restart_marker = "device-manager: restarting pci-bus";
scheduler.setPriority(1);
deadline = architecture.millis() + 15000;
var restarted = false;
while (architecture.millis() < deadline and !restarted) {
if (bufferHas(restart_marker)) restarted = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("the manager restarted pci-bus", restarted);
var marker_buffer: [48]u8 = undefined;
const marker = std.fmt.bufPrint(&marker_buffer, "pci-bus: {d} functions found", .{reported}) catch "";
scheduler.setPriority(1);
deadline = architecture.millis() + 15000;
var seen = false;
while (architecture.millis() < deadline and !seen) {
if (bufferHas(marker)) seen = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("the respawned scan reported the same count", seen);
// No duplicates: the registrations deduped against the kernel's own nodes
// on the first pass, and against themselves on the second.
var after: [64]device_abi.DeviceDescriptor = undefined;
const m = @min(devices_broker.enumerate(&after), after.len);
var after_count: u32 = 0;
for (after[0..m]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) after_count += 1;
}
check("no duplicate PCI nodes after register + restart + re-register", after_count == kernel_count);
check("no duplicate PCI nodes after the restart drill", !duplicated);
result();
}

View File

@ -486,12 +486,23 @@ CASES = [
"expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*"
r"acpi: reported PNP0F13 \(device \d+, 1 resources\)",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map
# grant) finds exactly the functions the kernel's own walk recorded.
# M19.1/M19.3: the ring-3 PCI scan. pci-bus walks the ECAM through its mmio_map
# grant and registers every function it finds; the kernel's own walk retired, so
# the broker starts empty and the driver populates it. The manager then runs the
# restart drill: ~1 s after the scan it kills pci-bus, prunes its child tree, and
# respawns it to re-claim, re-scan, and re-register the same functions. The kernel
# test asserts the broker equivalence (empty before, populated after, no
# duplicates); this ordered regex asserts the drill itself over the whole serial
# log — the backreference requires the respawn to re-scan the same count, and the
# full-capture match is immune to the transient-line races an in-kernel poll hits.
{"name": "pci-scan",
"smp": 4,
"timeout": 60,
"expect": r"DANOS-TEST-RESULT: PASS",
"expect": r"pci-bus: (\d+) functions found[\s\S]*"
r"device-manager: test mode: killing the reporter[\s\S]*"
r"device-manager: restarting pci-bus[\s\S]*"
r"pci-bus: \1 functions found[\s\S]*"
r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# M18.3: the application surface — device-list enumerates the tree over IPC,
# subscribes (endpoint as capability), and observes the removed/added events