diff --git a/src/kernel/tests.zig b/src/kernel/tests.zig index ebedca9..08fcf9e 100644 --- a/src/kernel/tests.zig +++ b/src/kernel/tests.zig @@ -50,6 +50,10 @@ fn result() void { pub fn run(case: []const u8, boot_info: *const BootInfo) void { if (eql(case, "smoke")) { smoke(boot_info); + } else if (eql(case, "discovery")) { + discoveryTest(); + } else if (eql(case, "wx")) { + wxTest(); } else if (eql(case, "timer")) { timer(); } else if (eql(case, "clock")) { @@ -171,6 +175,54 @@ fn timer() void { result(); } +/// Verify device discovery populated the platform facts the rest of the kernel +/// depends on — the results ACPI parsing stashed in globals at boot. These are +/// stable for the QEMU q35 + OVMF machine the harness runs, and span the tables: +/// MADT (LAPIC base, CPU count), FADT (PM/reset registers), and the AML parse +/// (the sleep type, plus the integrity check that every byte was consumed). +fn discoveryTest() void { + log("DANOS-TEST-BEGIN: discovery\n", .{}); + const pinfo = platform.platformInfo(); + const pw = platform.powerInfo(); + const am = platform.amlStats(); + + check("LAPIC base discovered (MADT)", pinfo.lapic_base == 0xFEE00000); + check("ACPI PM timer found (FADT)", pinfo.pm_timer.present()); + check("PM1a control register found (FADT)", pw.pm1a_cnt.present()); + check("reset register supported (FADT)", pw.reset_supported); + check("S5 sleep type found (AML)", pw.s5 != null); + check("AML parsed completely (consumed == total)", am.total > 0 and am.consumed == am.total); + check("at least one CPU enumerated (MADT)", platform.cpus().len >= 1); + + result(); +} + +/// Audit the W^X invariant across the memory classes: kernel code must be +/// executable, everything else must not be. `arch.pageExecutable` reads the leaf +/// page-table entry's NX bit, so this guards the permission overlay in paging.zig — +/// a broader check than `fault-nx`, which only exercises one data page. +fn wxTest() void { + log("DANOS-TEST-BEGIN: wx\n", .{}); + + check("kernel code is executable (R+X)", arch.pageExecutable(@intFromPtr(&wxTest))); + + const ro = "danos-wx-probe"; // string literal -> .rodata + check("rodata is non-executable (NX)", !arch.pageExecutable(@intFromPtr(ro.ptr))); + + check("kernel data is non-executable (NX)", !arch.pageExecutable(@intFromPtr(&passed))); + + if (heap.allocator().alloc(u8, 64) catch null) |h| { + check("heap is non-executable (NX)", !arch.pageExecutable(@intFromPtr(h.ptr))); + heap.allocator().free(h); + } + + var local: u64 = 0; + _ = &local; + check("stack is non-executable (NX)", !arch.pageExecutable(@intFromPtr(&local))); + + result(); +} + /// Verify the on-demand VMM: map a fresh frame at an unused virtual address, and /// check it's writable and reads back. fn vmm() void { diff --git a/test/qemu_test.py b/test/qemu_test.py index 7dda43e..938ef03 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -81,6 +81,12 @@ CASES = [ {"name": "smoke", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "discovery", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "wx", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, {"name": "timer", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, @@ -131,6 +137,14 @@ CASES = [ "expect": r"page fault \(vector 14\)", "fail": r"NX not enforced"}, {"name": "fault-null", "expect": r"page fault \(vector 14\)"}, + # The ACPI power path succeeds by QEMU *exiting* (S5 off / reset), so match the + # pre-transition marker; the FAIL line only appears if the transition didn't take. + {"name": "poweroff", + "expect": r"DANOS-POWER: attempting poweroff", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + {"name": "reboot", + "expect": r"DANOS-POWER: attempting reboot", + "fail": r"DANOS-TEST-RESULT: FAIL"}, ] TIMEOUT = 30 # seconds per case