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:
parent
0217662808
commit
24c49f56e1
|
|
@ -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(®istered), 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue