From dba3939a0fd1dc0aaefff31ef971c6a266a18cca Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:45:02 +0100 Subject: [PATCH] add an SMP lock-stress test case Four pairs push 400k sequenced messages through small channels; checks FIFO order and cross-core execution. Verified to fail with the lock disabled. --- src/kernel/tests.zig | 88 ++++++++++++++++++++++++++++++++++++++++++++ test/qemu_test.py | 11 +++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/kernel/tests.zig b/src/kernel/tests.zig index 99229af..3a198ab 100644 --- a/src/kernel/tests.zig +++ b/src/kernel/tests.zig @@ -70,6 +70,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void { ipcTest(); } else if (eql(case, "smp")) { smpTest(); + } else if (eql(case, "smp-stress")) { + stressTest(); } else if (eql(case, "fault-ud")) { faultInvalidOpcode(); } else if (eql(case, "fault-pf")) { @@ -482,6 +484,92 @@ fn smpTest() void { result(); } +// --- SMP stress: hammer the big kernel lock across cores ------------------ + +const stress_pairs = 4; // producer/consumer pairs (8 tasks; fits the 16-task pool) +const stress_msgs = 100_000; // messages per pair +const stress_cap = 4; // small channel -> constant block/wake, more lock churn + +var stress_chan = [_]ipc.Channel(u64, stress_cap){.{}} ** stress_pairs; +var stress_recv = [_]u64{0} ** stress_pairs; // messages received per pair +var stress_order_ok = [_]bool{true} ** stress_pairs; // FIFO order held per pair +var stress_cores = [_]bool{false} ** 8; // cores that ran a consumer +var stress_prod_claim: usize = 0; +var stress_cons_claim: usize = 0; + +fn stressProducer() void { + // Claim a unique pair index (atomic: producers start on different cores). + const idx = @atomicRmw(usize, &stress_prod_claim, .Add, 1, .monotonic); + var v: u64 = 1; + while (v <= stress_msgs) : (v += 1) stress_chan[idx].send(v); + sched.exit(); +} + +fn stressConsumer() void { + const idx = @atomicRmw(usize, &stress_cons_claim, .Add, 1, .monotonic); + var expected: u64 = 1; + while (expected <= stress_msgs) : (expected += 1) { + const got = stress_chan[idx].recv(); + if (got != expected) stress_order_ok[idx] = false; // lost/reordered => lock broke + const c = sched.currentCpuIndex(); + if (c < stress_cores.len) stress_cores[c] = true; + stress_recv[idx] = expected; + } + sched.exit(); +} + +/// Stress the big kernel lock under sustained cross-core contention. Each pair drives +/// `stress_msgs` sequenced messages through a 4-slot channel — every send and recv +/// takes the lock, and the small buffer forces constant block/wake (so the scheduler +/// churns too). A single-producer/single-consumer channel must deliver in strict FIFO +/// order; if the lock let two cores into a critical section at once, the ring buffer +/// corrupts and the consumer sees a wrong or out-of-order value (or the run hangs / +/// faults). Passing means ~320k lock acquisitions across the cores stayed consistent. +fn stressTest() void { + log("DANOS-TEST-BEGIN: smp-stress\n", .{}); + stress_chan = [_]ipc.Channel(u64, stress_cap){.{}} ** stress_pairs; + stress_recv = [_]u64{0} ** stress_pairs; + stress_order_ok = [_]bool{true} ** stress_pairs; + stress_cores = [_]bool{false} ** 8; + stress_prod_claim = 0; + stress_cons_claim = 0; + + var i: usize = 0; + while (i < stress_pairs) : (i += 1) sched.spawn(stressConsumer, 4); + i = 0; + while (i < stress_pairs) : (i += 1) sched.spawn(stressProducer, 4); + + // Drop below the workers so they get the cores; wake periodically to check for + // completion. A broken lock instead hangs here (harness timeout) or faults. + sched.setPriority(1); + var spins: u64 = 0; + while (spins < 40_000_000_000) : (spins += 1) { + var done = true; + for (stress_recv) |n| { + if (n < stress_msgs) done = false; + } + if (done) break; + } + sched.setPriority(4); + + var total: u64 = 0; + for (stress_recv) |n| total += n; + var order_ok = true; + for (stress_order_ok) |ok| { + if (!ok) order_ok = false; + } + var cores: u32 = 0; + for (stress_cores) |s| { + if (s) cores += 1; + } + + log("DANOS-STRESS: {d}/{d} pairs complete on {d} cores\n", .{ total, @as(u64, stress_pairs) * stress_msgs, cores }); + check("every message delivered", total == @as(u64, stress_pairs) * stress_msgs); + check("strict FIFO order held (no lock corruption)", order_ok); + check("contention was genuinely cross-core", cores >= 2); + result(); +} + fn faultInvalidOpcode() void { log("DANOS-TEST-BEGIN: fault-ud\n", .{}); asm volatile ("ud2"); diff --git a/test/qemu_test.py b/test/qemu_test.py index 952043c..b19eaf6 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -113,6 +113,12 @@ CASES = [ "smp": 4, "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # Stress the big kernel lock across cores; heavier, so a longer timeout. + {"name": "smp-stress", + "smp": 4, + "timeout": 90, + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, {"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"}, {"name": "fault-pf", "expect": r"page fault \(vector 14\)"}, {"name": "fault-df", "expect": r"double fault \(vector 8\)"}, @@ -184,7 +190,8 @@ def run_case(arch, case): cmd += ["-smp", str(case["smp"])] qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: - deadline = time.monotonic() + TIMEOUT + timeout = case.get("timeout", TIMEOUT) + deadline = time.monotonic() + timeout while time.monotonic() < deadline: time.sleep(0.2) text = "" @@ -199,7 +206,7 @@ def run_case(arch, case): if expect.search(text): return True, "matched " + repr(case["expect"]) return False, "QEMU exited before matching (triple fault?)" - return False, f"timed out after {TIMEOUT}s without matching {case['expect']!r}" + return False, f"timed out after {timeout}s without matching {case['expect']!r}" finally: if qemu.poll() is None: qemu.terminate()