Files
danos/system/kernel/tests.zig
T
Daniel Samson a785efa4a3 Orderly shutdown: init's stop cascade into ring-3 S5 (M21.3)
The capstone. init becomes a real supervisor: it spawns its boot
services supervised against one endpoint that also carries its signals, a
re-arming heartbeat timer, and the power events it subscribes to. On the
power button (or a terminate signal — same path) it logs the shutdown,
runs the M17 stop sequence over its children in reverse spawn order
(vfs last), then asks the power service for S5.

The acpi service honors a shutdown request from a power subscriber — init
is the one subscriber, a soft gate that stands in for 'only the system
supervisor may power off' and, unlike a PID-1 check, survives the test
harness where the kernel's idle tasks take the early ids. The power
service is mechanism (write S5); deciding when to shut down and stopping
everything else first is init's policy — the microkernel split applied to
poweroff.

The orderly-shutdown scenario injects a real QMP power-button event and
watches the whole chain compose: button pressed -> init shutting down ->
entering S5 -> QEMU powers off. That single scenario proves the M17
lifecycle and the M21 event side compose into a clean shutdown. Suite
60/60.
2026-07-13 05:56:58 +01:00

2697 lines
119 KiB
Zig

//! In-kernel test cases, run at the end of bring-up when the kernel is built with
//! `-Dtest-case=<name>`. Each case writes structured markers to the serial port
//! that the QEMU harness (test/qemu_test.py) asserts on:
//!
//! [PASS]/[FAIL] <check> per assertion
//! DANOS-TEST-RESULT: PASS|FAIL overall, for non-faulting cases
//!
//! Faulting cases (fault-ud, fault-pf, fault-df) deliberately don't return a
//! result line — they trigger a CPU exception, and the harness asserts on the
//! exception report the handler prints (which also reaches serial).
const std = @import("std");
const boot_handoff = @import("boot-handoff");
const abi = @import("abi");
const device_abi = @import("device-abi");
const architecture = @import("architecture");
const devices_broker = @import("devices-broker.zig");
const platform = @import("platform");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const scheduler = @import("scheduler.zig");
const ipc = @import("ipc.zig");
const ipcsync = @import("ipc-synchronous.zig");
const irq = @import("irq.zig");
const sync = @import("sync.zig");
const process = @import("process.zig");
const initial_ramdisk = @import("initial-ramdisk");
/// Formatted write straight to serial, independent of the framebuffer console.
fn log(comptime fmt: []const u8, args: anytype) void {
var buffer: [128]u8 = undefined;
architecture.serialWrite(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}
var passed: u32 = 0;
var failed: u32 = 0;
fn check(name: []const u8, ok: bool) void {
if (ok) {
passed += 1;
log("[PASS] {s}\n", .{name});
} else {
failed += 1;
log("[FAIL] {s}\n", .{name});
}
}
/// Emit the overall result line the harness matches, then the done sentinel.
fn result() void {
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
if (failed == 0) "PASS" else "FAIL",
passed,
failed,
});
log("DANOS-TEST-DONE\n", .{});
}
pub fn run(case: []const u8, boot_information: *const BootInformation) void {
if (eql(case, "smoke")) {
smoke(boot_information);
} else if (eql(case, "discovery")) {
discoveryTest();
} else if (eql(case, "wx")) {
wxTest();
} else if (eql(case, "timer")) {
timer();
} else if (eql(case, "clock")) {
clock();
} else if (eql(case, "vmm")) {
vmm();
} else if (eql(case, "heap")) {
heapTest();
} else if (eql(case, "sched")) {
schedulerTest();
} else if (eql(case, "priority")) {
priorityTest();
} else if (eql(case, "sleep")) {
sleepTest();
} else if (eql(case, "event")) {
eventTest();
} else if (eql(case, "ipc")) {
ipcTest();
} else if (eql(case, "ipc-call")) {
ipcCallTest();
} else if (eql(case, "ipc-cap")) {
capabilityTest();
} else if (eql(case, "dma")) {
dmaTest();
} else if (eql(case, "msi")) {
msiTest();
} else if (eql(case, "iommu")) {
iommuTest();
} else if (eql(case, "ioport")) {
ioPortTest();
} else if (eql(case, "clock")) {
clockTest();
} else if (eql(case, "smp")) {
smpTest();
} else if (eql(case, "affinity")) {
affinityTest();
} else if (eql(case, "smp-stress")) {
stressTest();
} else if (eql(case, "smp-retry")) {
smpRetryTest();
} else if (eql(case, "fault-ud")) {
faultInvalidOpcode();
} else if (eql(case, "fault-pf")) {
faultPageFault();
} else if (eql(case, "fault-df")) {
faultDoubleFault();
} else if (eql(case, "fault-ap-df")) {
faultApTest();
} else if (eql(case, "fault-nx")) {
faultNoExecute();
} else if (eql(case, "fault-null")) {
faultNull();
} else if (eql(case, "usermem")) {
userMemTest();
} else if (eql(case, "user-pf")) {
userPfTest();
} else if (eql(case, "fault-recovery")) {
faultRecoveryTest(boot_information);
} else if (eql(case, "args")) {
argsTest(boot_information);
} else if (eql(case, "init")) {
initTest(boot_information);
} else if (eql(case, "process")) {
processTest(boot_information);
} else if (eql(case, "process-list")) {
processListTest(boot_information);
} else if (eql(case, "process-kill")) {
processKillTest(boot_information);
} else if (eql(case, "supervision")) {
supervisionTest(boot_information);
} else if (eql(case, "claim-release")) {
claimReleaseTest(boot_information);
} else if (eql(case, "vfs-client-death")) {
vfsClientDeathTest(boot_information);
} else if (eql(case, "signals")) {
signalsTest(boot_information);
} else if (eql(case, "driver-restart")) {
driverRestartTest(boot_information);
} else if (eql(case, "usb-report")) {
usbReportTest(boot_information);
} else if (eql(case, "device-list")) {
deviceListTest(boot_information);
} else if (eql(case, "pci-scan")) {
pciScanTest(boot_information);
} else if (eql(case, "acpi-parse")) {
acpiParseTest(boot_information);
} else if (eql(case, "acpi-report")) {
acpiReportTest(boot_information);
} else if (eql(case, "acpi-ps2")) {
acpiReportTest(boot_information); // same spawn; the harness regex differs
} else if (eql(case, "power-button")) {
acpiReportTest(boot_information); // boot the manager (spawns the acpi service); harness injects the button
} else if (eql(case, "orderly-shutdown")) {
orderlyShutdownTest(boot_information);
} else if (eql(case, "initial-ramdisk")) {
initialRamdiskTest(boot_information);
} else if (eql(case, "vfs")) {
vfsTest(boot_information);
} else if (eql(case, "input")) {
inputTest(boot_information);
} else if (eql(case, "hpet")) {
hpetTest(boot_information);
} else if (eql(case, "iopass")) {
ioPassTest();
} else if (eql(case, "irqfree")) {
irqFreeTest();
} else if (eql(case, "bus")) {
busTest(boot_information);
} else if (eql(case, "device-manager")) {
deviceManagerTest(boot_information);
} else if (eql(case, "poweroff")) {
powerTest(.off);
} else if (eql(case, "reboot")) {
powerTest(.reboot);
} else {
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case});
}
}
fn platformHal() platform.Hal {
return .{
.mapMmio = architecture.mapMmio,
.pioRead = architecture.pioRead,
.pioWrite = architecture.pioWrite,
};
}
/// Drive an ACPI power transition. On success the machine powers off or resets,
/// so QEMU exits — the harness observes the process exit. If control returns, the
/// transition failed and we emit a FAIL result.
fn powerTest(comptime action: enum { off, reboot }) void {
const name = if (action == .off) "poweroff" else "reboot";
log("DANOS-TEST-BEGIN: {s}\n", .{name});
const hal = platformHal();
log("DANOS-POWER: attempting {s}\n", .{name});
switch (action) {
.off => platform.shutdown(hal),
.reboot => platform.reboot(hal),
}
check("power transition took effect", false);
result();
}
const BootInformation = boot_handoff.BootInformation;
fn eql(a: []const u8, b: []const u8) bool {
return std.mem.eql(u8, a, b);
}
/// Non-destructive checks of the memory map and frame allocator.
fn smoke(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: smoke\n", .{});
// The memory map has some usable RAM.
const mm = boot_information.memory_map;
const regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(mm.regions)))[0..mm.len];
var usable: u64 = 0;
for (regions) |r| {
if (r.kind == .usable) usable += r.pages;
}
check("memory map reports usable RAM", usable > 0);
// The frame allocator hands out distinct, page-aligned frames.
const a = pmm.alloc();
const b = pmm.alloc();
check("alloc returns a frame", a != null);
check("alloc returns distinct frames", a != null and b != null and a.? != b.?);
check("frames are page-aligned", (a orelse 1) % abi.page_size == 0);
// Freeing restores the count.
const before = pmm.stats().free_frames;
if (a) |p| pmm.free(p);
if (b) |p| pmm.free(p);
check("free returns frames to the pool", pmm.stats().free_frames == before + 2);
// Paging is active on our own tables (the root is non-zero and page-aligned).
const root = architecture.activePageTable();
check("paging active (page-table root set)", root != 0 and root % abi.page_size == 0);
result();
}
/// Verify device interrupts fire and return: the timer tick counter must advance
/// on its own. Interrupts are already enabled by kmain before tests run.
fn timer() void {
log("DANOS-TEST-BEGIN: timer\n", .{});
const start = architecture.ticks();
// Busy-wait for the counter to advance. architecture.ticks() is a volatile load, so
// the compiler re-reads it each iteration and sees the interrupt's update.
// The cap is only a safety net; the harness timeout is the real backstop.
var spins: u64 = 0;
while (architecture.ticks() == start and spins < 5_000_000_000) spins +%= 1;
check("timer interrupts advance the tick count", architecture.ticks() > start);
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.platformInformation();
const pw = platform.powerInformation();
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);
// M15: every PCI function now carries its own 4 KiB ECAM configuration space as
// resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc).
// M19.3: the kernel seeds only the bridge; functions arrive by the ring-3
// scan (proven equivalent in pci-scan before the walk retired).
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
var bridges: u32 = 0;
var bridge_shape_ok = false;
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_host_bridge)) continue;
bridges += 1;
var has_bus_range = false;
var has_io = false;
var memory_windows: u32 = 0;
for (d.resources[0..@intCast(d.resource_count)]) |resource| {
if (resource.kind == @intFromEnum(device_abi.ResourceKind.bus_range)) has_bus_range = true;
if (resource.kind == @intFromEnum(device_abi.ResourceKind.io_port)) has_io = true;
if (resource.kind == @intFromEnum(device_abi.ResourceKind.memory)) memory_windows += 1;
}
// ECAM plus at least one MMIO aperture, the bus range, the I/O window.
if (has_bus_range and has_io and memory_windows >= 2) bridge_shape_ok = true;
}
check("a PCI host bridge was seeded (MCFG)", bridges >= 1);
check("the bridge carries ECAM, apertures, bus range, and the I/O window", bridge_shape_ok);
// M19.0: every PCI memory resource (config slice and BARs alike) must be
// contained in one of its parent bridge's windows — the aperture derivation
// from the memory map is what makes a future user-space device_register of
// these functions pass containment. This is the assert that catches a
// too-coarse hole computation before M19.2 would.
var bars_contained = true;
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
if (d.parent >= n) {
bars_contained = false;
continue;
}
const bridge = buffer[@intCast(d.parent)];
for (d.resources[0..@intCast(d.resource_count)]) |r| {
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
var inside = false;
for (bridge.resources[0..@intCast(bridge.resource_count)]) |w| {
if (w.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
if (r.start >= w.start and r.start + r.len <= w.start + w.len) inside = true;
}
if (!inside) {
bars_contained = false;
log(" escaping BAR: 0x{x}+0x{x} on device {d}\n", .{ r.start, r.len, d.id });
}
}
}
check("every PCI BAR lies inside a bridge aperture (M19.0)", bars_contained);
result();
}
/// Audit the W^X invariant across the memory classes: kernel code must be
/// executable, everything else must not be. `architecture.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)", architecture.pageExecutable(@intFromPtr(&wxTest)));
const ro = "danos-wx-probe"; // string literal -> .rodata
check("rodata is non-executable (NX)", !architecture.pageExecutable(@intFromPtr(ro.ptr)));
check("kernel data is non-executable (NX)", !architecture.pageExecutable(@intFromPtr(&passed)));
if (heap.allocator().alloc(u8, 64) catch null) |h| {
check("heap is non-executable (NX)", !architecture.pageExecutable(@intFromPtr(h.ptr)));
heap.allocator().free(h);
}
var local: u64 = 0;
_ = &local;
check("stack is non-executable (NX)", !architecture.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 {
log("DANOS-TEST-BEGIN: vmm\n", .{});
const frame = pmm.alloc();
check("frame available to map", frame != null);
if (frame) |physical| {
var virtual: u64 = 0x0000_4000_0000_0000; // canonical, well clear of everything mapped
architecture.mapPage(virtual, physical, true);
const p: *volatile u64 = @ptrFromInt(virtual);
p.* = 0xdead_c0de_cafe_babe;
check("mapped page is writable and reads back", p.* == 0xdead_c0de_cafe_babe);
architecture.unmapPage(virtual);
pmm.free(physical);
virtual += 0;
}
result();
}
/// Exercise the kernel heap: basic alloc/write/free, reuse, growth beyond the
/// initial region, and a std container backed by it.
fn heapTest() void {
log("DANOS-TEST-BEGIN: heap\n", .{});
const a = heap.allocator();
// Allocate, write a pattern, read it back, free.
const buffer = a.alloc(u8, 4096) catch null;
check("alloc 4096 bytes", buffer != null);
if (buffer) |b| {
@memset(b, 0xAB);
check("heap memory is writable and reads back", b[0] == 0xAB and b[4095] == 0xAB);
a.free(b);
}
// Freeing then re-allocating the same size should reuse the block.
const p1 = a.alloc(u64, 8) catch null;
const address1 = if (p1) |p| @intFromPtr(p.ptr) else 0;
if (p1) |p| a.free(p);
const p2 = a.alloc(u64, 8) catch null;
const address2 = if (p2) |p| @intFromPtr(p.ptr) else 0;
check("freed block is reused", address1 != 0 and address1 == address2);
if (p2) |p| a.free(p);
// Force growth past the initial page and check every block is usable.
var blocks: [64]?[]u8 = .{null} ** 64;
var ok = true;
for (&blocks, 0..) |*slot, i| {
const b = a.alloc(u8, 4096) catch null;
slot.* = b;
if (b) |bb| @memset(bb, @intCast(i & 0xff)) else {
ok = false;
}
}
for (blocks, 0..) |slot, i| {
if (slot) |bb| {
if (bb[0] != @as(u8, @intCast(i & 0xff)) or bb[4095] != @as(u8, @intCast(i & 0xff))) ok = false;
}
}
check("many allocations (heap growth) stay valid", ok);
for (blocks) |slot| {
if (slot) |bb| a.free(bb);
}
// A std container backed by the kernel heap.
var list: std.ArrayList(u32) = .empty;
var sum: u64 = 0;
var expected: u64 = 0;
var i: u32 = 0;
var list_ok = true;
while (i < 1000) : (i += 1) {
list.append(a, i) catch {
list_ok = false;
};
expected += i;
}
for (list.items) |v| sum += v;
list.deinit(a);
check("std.ArrayList on the kernel heap", list_ok and sum == expected);
result();
}
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
/// advances with real ticks, and — the point of the TSC clock — nanosecond
/// resolution far finer than the 1 ms tick, with the unit functions consistent.
fn clock() void {
log("DANOS-TEST-BEGIN: clock\n", .{});
const timer_clock = architecture.timerClockHz();
check("timer clock frequency measured", timer_clock > 1_000_000 and timer_clock < 100_000_000_000);
const clock_hz = architecture.clockHz();
check("monotonic clock frequency measured", clock_hz > 100_000_000 and clock_hz < 100_000_000_000);
// Uptime advances over ~5 real ticks (1000 Hz => 1 tick == 1 ms).
const start_ticks = architecture.ticks();
const start_ms = architecture.millis();
var spins: u64 = 0;
while (architecture.ticks() < start_ticks + 5 and spins < 5_000_000_000) spins +%= 1;
const elapsed_ms = architecture.millis() - start_ms;
check("uptime advances with ticks", elapsed_ms >= 5 and elapsed_ms < 100);
// Sub-millisecond resolution: spin until nanos() first advances, then confirm
// that first step happened within a millisecond — so nanos() resolves finer
// than the 1 ms tick (a tick clock's smallest step *is* 1 ms). Spinning to the
// first change is robust to QEMU's coarse TSC update granularity.
const n1 = architecture.nanos();
var s2: u64 = 0;
while (architecture.nanos() == n1 and s2 < 10_000_000) s2 +%= 1;
const n2 = architecture.nanos();
check("nanos() has sub-millisecond resolution", n2 > n1 and (n2 - n1) < 1_000_000);
// The unit functions agree (within rounding).
const ns = architecture.nanos();
check("nanos/micros/millis are consistent", diffWithin(architecture.micros(), ns / 1000, 1000) and diffWithin(architecture.millis(), ns / 1_000_000, 2));
result();
}
fn diffWithin(a: u64, b: u64, tol: u64) bool {
return if (a > b) a - b <= tol else b - a <= tol;
}
// --- scheduler tests ------------------------------------------------------
var counters = [_]u64{0} ** 3;
fn spin0() void {
const p: *volatile u64 = &counters[0];
while (true) p.* = p.* +% 1;
}
fn spin1() void {
const p: *volatile u64 = &counters[1];
while (true) p.* = p.* +% 1;
}
fn spin2() void {
const p: *volatile u64 = &counters[2];
while (true) p.* = p.* +% 1;
}
/// Preemption: spawn three tasks that busy-loop *without* yielding. If they all
/// make progress, the timer must be preempting between them (and the context
/// switch works) — because nothing yields voluntarily.
fn schedulerTest() void {
log("DANOS-TEST-BEGIN: sched\n", .{});
counters = .{ 0, 0, 0 };
scheduler.spawn(spin0, 4);
scheduler.spawn(spin1, 4);
scheduler.spawn(spin2, 4);
const c0: *volatile u64 = &counters[0];
const c1: *volatile u64 = &counters[1];
const c2: *volatile u64 = &counters[2];
var spins: u64 = 0;
while ((c0.* == 0 or c1.* == 0 or c2.* == 0) and spins < 5_000_000_000) spins +%= 1;
check("all three non-yielding tasks made progress (preemption)", c0.* > 0 and c1.* > 0 and c2.* > 0);
result();
}
var run_order = [_]u8{0} ** 4;
var run_n: usize = 0;
fn recordExit(priority: u8) void {
run_order[run_n] = priority;
run_n += 1;
scheduler.exit();
}
fn taskHigh() void {
recordExit(6);
}
fn taskMid() void {
recordExit(4);
}
fn taskLow() void {
recordExit(2);
}
/// Fixed priority: with preemption off (deterministic), spawn tasks at three
/// priorities and let them run cooperatively. They must run highest-first.
fn priorityTest() void {
log("DANOS-TEST-BEGIN: priority\n", .{});
scheduler.setPreemption(false);
scheduler.setPriority(1); // above the idle task (0), below the workers — runs last
run_n = 0;
scheduler.spawn(taskLow, 2);
scheduler.spawn(taskMid, 4);
scheduler.spawn(taskHigh, 6);
while (run_n < 3) scheduler.yield(); // regain control only once the workers are done
check("tasks ran highest-priority first", run_order[0] == 6 and run_order[1] == 4 and run_order[2] == 2);
scheduler.setPriority(4);
scheduler.setPreemption(true);
result();
}
var event_wait_queue: scheduler.WaitQueue = .{};
var event_stage: u32 = 0;
fn eventWaiter() void {
event_stage = 1; // reached the wait
scheduler.wait(&event_wait_queue); // block until woken
event_stage = 3; // woken and resumed
scheduler.exit();
}
/// Event-based blocking: a task blocks on a wait queue and is woken. The waiter is
/// higher priority, so waking it preempts us and it runs to completion at once.
fn eventTest() void {
log("DANOS-TEST-BEGIN: event\n", .{});
event_stage = 0;
scheduler.spawn(eventWaiter, 6); // higher priority than this task (4)
var spins: u64 = 0;
while (event_stage != 1 and spins < 1_000_000_000) : (spins += 1) scheduler.yield();
check("waiter reached the wait and blocked", event_stage == 1);
scheduler.wake(&event_wait_queue);
check("wake resumed the blocked waiter (preempting)", event_stage == 3);
result();
}
var channel: ipc.Channel(u64, 4) = .{};
var receive_sum: u64 = 0;
var receive_count: u64 = 0;
fn producer() void {
var i: u64 = 1;
while (i <= 100) : (i += 1) channel.send(i);
scheduler.exit();
}
fn consumer() void {
var n: u64 = 0;
while (n < 100) : (n += 1) {
receive_sum += channel.receive();
receive_count += 1;
}
scheduler.exit();
}
/// IPC: a producer and consumer pass 100 messages through a 4-slot channel. The
/// small buffer forces the channel full and empty repeatedly, exercising both the
/// blocking-send and blocking-receive paths. The messages must arrive intact.
fn ipcTest() void {
log("DANOS-TEST-BEGIN: ipc\n", .{});
channel = .{};
receive_sum = 0;
receive_count = 0;
scheduler.spawn(consumer, 5); // above this task (4) so they run and we observe after
scheduler.spawn(producer, 5);
var spins: u64 = 0;
while (receive_count < 100 and spins < 2_000_000_000) : (spins += 1) scheduler.yield();
check("all 100 messages received", receive_count == 100);
check("messages arrived intact (sum 1..100 == 5050)", receive_sum == 5050);
result();
}
/// Blocking: sleep(50) should block this task for about 50 ms (measured on the
/// calibrated clock) — not busy-wait — while the idle task runs.
fn sleepTest() void {
log("DANOS-TEST-BEGIN: sleep\n", .{});
const t0 = architecture.millis();
scheduler.sleep(50);
const elapsed = architecture.millis() - t0;
check("sleep(50) blocked for ~50 ms", elapsed >= 50 and elapsed <= 70);
result();
}
// --- SMP parallelism ------------------------------------------------------
var seen_core = [_]bool{false} ** 8;
var smp_running: bool = true;
/// A worker that, while running, records which core it's executing on. Spread across
/// spawned workers and idle APs, these should land on more than one core.
fn smpWorker() void {
const p: *volatile bool = &smp_running;
while (p.*) {
const c = scheduler.currentCpuIndex();
if (c < seen_core.len) seen_core[c] = true;
}
scheduler.exit();
}
/// Prove tasks run **in parallel** on multiple cores (not just interleaved on one).
/// Spawn several CPU-bound workers; each stamps the core it runs on into `seen_core`.
/// With the application processors online, more than one core should show up — which
/// can only happen if work is genuinely running at the same time on different cores.
/// (Run with QEMU `-smp N`; on a single core this would see just one and fail.)
fn smpTest() void {
log("DANOS-TEST-BEGIN: smp\n", .{});
seen_core = .{false} ** 8;
smp_running = true;
var i: usize = 0;
while (i < 4) : (i += 1) scheduler.spawn(smpWorker, 4);
// Let the workers run across cores for a stretch of real time.
var spins: u64 = 0;
while (spins < 2_000_000_000) spins +%= 1;
smp_running = false;
var cores_seen: u32 = 0;
for (seen_core) |s| {
if (s) cores_seen += 1;
}
log("DANOS-SMP: workers ran on {d} distinct core(s)\n", .{cores_seen});
check("tasks ran on multiple cores in parallel", cores_seen >= 2);
// Bring-up is done, so the trampoline frame must be inert: zeroed (no stale code)
// and non-executable (W^X restored). It's armed only while a core is climbing.
const tramp = architecture.trampolinePage();
check("trampoline frame reserved", tramp != 0);
if (tramp != 0) {
const bytes: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(tramp));
var zeroed = true;
for (0..4096) |b| {
if (bytes[b] != 0) zeroed = false;
}
check("trampoline page zeroed when dormant", zeroed);
check("trampoline page non-executable when dormant", !architecture.pageExecutable(tramp));
}
result();
}
// --- affinity: a pinned task never migrates -------------------------------
var affinity_cores = [_]bool{false} ** 8;
var affinity_running: bool = true;
fn affinityWorker() void {
const p: *volatile bool = &affinity_running;
while (p.*) {
const c = scheduler.currentCpuIndex();
if (c < affinity_cores.len) affinity_cores[c] = true;
}
scheduler.exit();
}
/// A task pinned to a core must run **only** on that core. Pin a busy worker to
/// core 1 and let it run through many preemptions; it must have stamped core 1 and no
/// other. An *unpinned* task scatters across cores (that's what the smp test shows),
/// so a broken pin fails this deterministically — over this many time slices a
/// free-floating task will land on some other core.
fn affinityTest() void {
log("DANOS-TEST-BEGIN: affinity\n", .{});
affinity_cores = .{false} ** 8;
affinity_running = true;
if (!scheduler.spawnOn(affinityWorker, 4, 1)) {
check("worker pinned to core 1 (run with -smp)", false);
result();
return;
}
var spins: u64 = 0;
while (spins < 3_000_000_000) spins +%= 1; // many time slices across the cores
affinity_running = false;
var settle: u64 = 0;
while (settle < 200_000_000) settle +%= 1; // let the worker see the flag and exit
var others: u32 = 0;
for (affinity_cores, 0..) |seen, c| {
if (seen and c != 1) others += 1;
}
log("DANOS-AFFINITY: pinned worker touched core 1={}, other cores={d}\n", .{ affinity_cores[1], others });
check("pinned task ran on its core (1)", affinity_cores[1]);
check("pinned task never migrated to another core", others == 0);
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_receive = [_]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 index = @atomicRmw(usize, &stress_prod_claim, .Add, 1, .monotonic);
var v: u64 = 1;
while (v <= stress_msgs) : (v += 1) stress_chan[index].send(v);
scheduler.exit();
}
fn stressConsumer() void {
const index = @atomicRmw(usize, &stress_cons_claim, .Add, 1, .monotonic);
var expected: u64 = 1;
while (expected <= stress_msgs) : (expected += 1) {
const got = stress_chan[index].receive();
if (got != expected) stress_order_ok[index] = false; // lost/reordered => lock broke
const c = scheduler.currentCpuIndex();
if (c < stress_cores.len) stress_cores[c] = true;
stress_receive[index] = expected;
}
scheduler.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 receive
/// 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_receive = [_]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) scheduler.spawn(stressConsumer, 4);
i = 0;
while (i < stress_pairs) : (i += 1) scheduler.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.
scheduler.setPriority(1);
var spins: u64 = 0;
while (spins < 40_000_000_000) : (spins += 1) {
var done = true;
for (stress_receive) |n| {
if (n < stress_msgs) done = false;
}
if (done) break;
}
scheduler.setPriority(4);
var total: u64 = 0;
for (stress_receive) |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();
}
/// Retry: `main` forced the first AP wake attempt to fail (architecture.testFailNextWakes),
/// so a core missed its first INIT-SIPI-SIPI. The boot retry must have brought it back
/// anyway — every enumerated core should be online. If retry were broken, that core
/// would be parked and the count would fall short.
fn smpRetryTest() void {
log("DANOS-TEST-BEGIN: smp-retry\n", .{});
const total = platform.cpus().len;
const online = scheduler.onlineCount();
log("DANOS-RETRY: {d}/{d} cores online after a forced first-wake failure\n", .{ online, total });
check("multiple cores enumerated (run with -smp)", total >= 2);
check("retry brought every core online despite a failed first wake", online == total);
result();
}
// --- ring 3 (user mode) -----------------------------------------------------
/// The mmap/munmap grant path: create a fresh address space, hand out pages into
/// its mmap arena the way the `mmap` system_call does, prove they are real (write and
/// read them back through the physmap), then release them via the `munmap` path
/// (translate -> unmap -> free) and tear the address space down. The frame count
/// must return exactly to where it started — a leak or a double-free would show
/// as drift. Exercises the new `translate`/`unmapUserPageInto` primitives that
/// back munmap, without needing a user binary that calls the syscalls.
fn userMemTest() void {
log("DANOS-TEST-BEGIN: usermem\n", .{});
const base_free = pmm.stats().free_frames;
const aspace = architecture.createAddressSpace() orelse {
check("created a fresh address space", false);
result();
return;
};
check("created a fresh address space", aspace != 0);
// Grant three pages into the arena, mapped RW + NX (the mmap contract).
const npages = 3;
const arena = process.heap_arena_base;
var frames: [npages]u64 = undefined;
var mapped: usize = 0;
while (mapped < npages) : (mapped += 1) {
frames[mapped] = pmm.alloc() orelse break;
architecture.mapUserPageInto(aspace, arena + mapped * abi.page_size, frames[mapped], true, false);
}
check("granted three user pages", mapped == npages);
// Each page resolves back to the frame we mapped, and is writable RAM.
var translate_ok = true;
var rw_ok = true;
for (0..npages) |i| {
const va = arena + i * abi.page_size;
const physical = architecture.translate(aspace, va) orelse {
translate_ok = false;
continue;
};
if (physical != frames[i]) translate_ok = false;
const p: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(physical));
p[0] = 0xA5;
if (p[0] != 0xA5) rw_ok = false;
}
check("translate resolves each grant to its frame", translate_ok);
check("granted pages are writable RAM", rw_ok);
// Release them the way munmap does, then tear down the address space.
for (0..npages) |i| {
const va = arena + i * abi.page_size;
if (architecture.translate(aspace, va)) |physical| {
architecture.unmapUserPageInto(aspace, va);
pmm.free(physical);
}
}
check("munmap unmapped every grant", architecture.translate(aspace, arena) == null);
architecture.destroyAddressSpace(aspace);
check("no frames leaked (free count restored)", pmm.stats().free_frames == base_free);
result();
}
// --- synchronous IPC --------------------------------------------------------
var ipc_endpoint: *ipcsync.Endpoint = undefined;
var ipc_replies_ok: bool = false;
var ipc_done: bool = false;
/// Echo-increment server: reply to each request with request+1, forever.
fn ipcServer() void {
var reply_buffer: [8]u8 = undefined;
var reply_len: u64 = 0;
var badge: u64 = 0;
var received_cap: u64 = abi.no_cap;
while (true) {
var receive: [8]u8 = undefined;
const n = ipcsync.replyWait(ipc_endpoint, @intFromPtr(&reply_buffer), reply_len, @intFromPtr(&receive), receive.len, abi.no_cap, &badge, &received_cap);
if (n < 0) scheduler.exit();
const v = std.mem.readInt(u64, receive[0..8], .little);
std.mem.writeInt(u64, reply_buffer[0..8], v + 1, .little);
reply_len = 8;
}
}
/// Client: make 100 synchronous calls, checking every reply is request+1.
fn ipcClient() void {
var ok = true;
var i: u64 = 0;
while (i < 100) : (i += 1) {
var message: [8]u8 = undefined;
std.mem.writeInt(u64, message[0..8], i, .little);
var reply: [8]u8 = undefined;
var received_cap: u64 = abi.no_cap;
const n = ipcsync.call(ipc_endpoint, @intFromPtr(&message), 8, @intFromPtr(&reply), reply.len, abi.no_cap, &received_cap);
if (n != 8 or std.mem.readInt(u64, reply[0..8], .little) != i + 1) ok = false;
}
ipc_replies_ok = ok;
ipc_done = true;
scheduler.exit();
}
/// Synchronous IPC: a client and a server (two kernel tasks) ping-pong 100 calls
/// through one Endpoint. Each round exercises the full rendezvous — the client
/// blocks in `call`, the server blocks in `replyWait`, the reply is routed back to
/// the exact caller, and `copyAcross` moves the bytes — so 100 correct replies
/// prove the block/wake and reply-routing paths. (Kernel tasks, so no user ELF.)
fn ipcCallTest() void {
log("DANOS-TEST-BEGIN: ipc-call\n", .{});
ipc_endpoint = ipcsync.createIpcEndpoint().?;
ipc_replies_ok = false;
ipc_done = false;
scheduler.spawn(ipcServer, 5); // above this task, so the workers run
scheduler.spawn(ipcClient, 5);
const done: *volatile bool = &ipc_done;
var spins: u64 = 0;
while (!done.* and spins < 100_000_000) : (spins += 1) scheduler.yield();
check("client completed 100 synchronous calls", ipc_done);
check("every reply was request+1 (rendezvous + reply routing intact)", ipc_replies_ok);
result();
}
// --- IPC capability passing (M13) -------------------------------------------
var cap_endpoint: *ipcsync.Endpoint = undefined;
var cap_ep_x: *ipcsync.Endpoint = undefined; // client mints, sends to the server
var cap_ep_y: *ipcsync.Endpoint = undefined; // server mints, sends back to the client
var cap_server_got_x: bool = false;
var cap_client_got_y: bool = false;
var cap_done: bool = false;
/// Server half of the "open" pattern: receive one request carrying a capability,
/// verify it, then reply handing back a capability of its own.
fn capServer() void {
const me = scheduler.current();
cap_ep_y = ipcsync.createIpcEndpoint().?;
const h_y = ipcsync.installHandle(me, cap_ep_y); // the handle to send back in the reply
var reply_buffer: [8]u8 = .{0} ** 8;
var receive: [8]u8 = undefined;
var badge: u64 = 0;
var received: u64 = abi.no_cap;
// Phase 1: no reply owed yet — receive the client's request, which carries ep_x.
_ = ipcsync.replyWait(cap_endpoint, @intFromPtr(&reply_buffer), 0, @intFromPtr(&receive), receive.len, abi.no_cap, &badge, &received);
cap_server_got_x = received != abi.no_cap and
ipcsync.resolveHandle(me, received) == cap_ep_x and
cap_ep_x.refcount == 2; // shared (client's handle + this one), not moved
// Phase 2: reply to the held client, handing it ep_y; then block for a next
// request that never comes (so this replyWait does not return).
_ = ipcsync.replyWait(cap_endpoint, @intFromPtr(&reply_buffer), 8, @intFromPtr(&receive), receive.len, @intCast(h_y), &badge, &received);
scheduler.exit();
}
/// Client half of "open": mint a capability, send it in a call, receive one back.
fn capClient() void {
const me = scheduler.current();
cap_ep_x = ipcsync.createIpcEndpoint().?;
const h_x = ipcsync.installHandle(me, cap_ep_x);
var message: [8]u8 = .{0} ** 8;
var reply: [8]u8 = undefined;
var received: u64 = abi.no_cap;
_ = ipcsync.call(cap_endpoint, @intFromPtr(&message), 8, @intFromPtr(&reply), reply.len, @intCast(h_x), &received);
cap_client_got_y = received != abi.no_cap and
ipcsync.resolveHandle(me, received) == cap_ep_y and
cap_ep_y.refcount == 2;
cap_done = true;
scheduler.exit();
}
/// IPC capability passing: a client hands the server an endpoint in a `call`, and the
/// server hands one back in its reply — the primitive that lets a bus driver give a
/// class driver a private channel to one device (M13). Two kernel tasks (no user ELF);
/// each verifies the endpoint it received is the *same* object the peer sent (resolves
/// equal) and was *shared*, not moved (refcount bumped to 2).
fn capabilityTest() void {
log("DANOS-TEST-BEGIN: ipc-cap\n", .{});
cap_endpoint = ipcsync.createIpcEndpoint().?;
cap_server_got_x = false;
cap_client_got_y = false;
cap_done = false;
scheduler.spawn(capServer, 5);
scheduler.spawn(capClient, 5);
const done: *volatile bool = &cap_done;
var spins: u64 = 0;
while (!done.* and spins < 100_000_000) : (spins += 1) scheduler.yield();
check("capability test completed", cap_done);
check("server received the client's endpoint (same object, shared not moved)", cap_server_got_x);
check("client received the server's endpoint back (same object, shared not moved)", cap_client_got_y);
result();
}
/// DMA memory (M14): the properties a bus-mastering driver needs — physically
/// contiguous, a known physical address, correct cacheability, pinned, and reclaimed
/// on teardown. Exercises the kernel mechanism directly (`pmm.allocContiguous` +
/// `mapUserDmaInto`); the `dma_alloc`/`dma_free` syscalls are thin wrappers over it,
/// following the tested `mmap`/`mmio_map` shape, and land their first real use with the
/// first DMA driver.
fn dmaTest() void {
log("DANOS-TEST-BEGIN: dma\n", .{});
const base_free = pmm.stats().free_frames;
// A contiguous run: aligned, and it consumed exactly that many frames.
const frames = 4;
const phys = pmm.allocContiguous(frames, ~@as(u64, 0)) orelse {
check("allocContiguous(4) succeeded", false);
result();
return;
};
check("contiguous run is page-aligned", phys % abi.page_size == 0);
check("contiguous run consumed 4 frames", pmm.stats().free_frames == base_free - frames);
// The below-4G cap is honoured (legacy 32-bit DMA engines).
const low = pmm.allocContiguous(2, @as(u64, 4) << 30) orelse 0;
check("below-4G run stays under 4 GiB", low != 0 and low + 2 * abi.page_size <= (@as(u64, 4) << 30));
// Map the run into a fresh address space as coherent DMA and translate each page
// back: the same physical run, in order — proving contiguity and the mapping.
const aspace = architecture.createAddressSpace().?;
architecture.mapUserDmaInto(aspace, process.dma_arena_base, phys, frames * abi.page_size);
var mapped_ok = true;
for (0..frames) |i| {
const va = process.dma_arena_base + i * abi.page_size;
const got = architecture.translate(aspace, va) orelse {
mapped_ok = false;
break;
};
if (got != phys + i * abi.page_size) mapped_ok = false;
}
check("DMA pages translate to the contiguous physical run", mapped_ok);
// Teardown must reclaim the DMA RAM (the leaves carry no device_grant, so
// freeSubtree frees them as ordinary frames) — a driver that just dies leaks none.
architecture.destroyAddressSpace(aspace);
for (0..2) |i| pmm.free(low + i * abi.page_size);
check("no frames leaked after DMA teardown", pmm.stats().free_frames == base_free);
result();
}
/// MSI (M15): a per-device, edge-triggered interrupt vector delivered as an IPC
/// notification. QEMU's HPET has no MSI, so this exercises the vector-routing path with
/// a self-IPI standing in for the device's MSI memory write — proving the kernel
/// allocates a vector, `dispatch` recognises it as MSI (EOI + notify, no mask cycle),
/// and the bound endpoint is notified. The `msi_bind` syscall wraps `irq.msiBind` with
/// the device-claim check and lands its first real use with the first PCI driver.
fn msiTest() void {
log("DANOS-TEST-BEGIN: msi\n", .{});
const endpoint = ipcsync.createIpcEndpoint().?;
const flags = sync.enter();
const vector = irq.msiBind(endpoint, 0) catch {
sync.leave(flags);
check("msiBind allocated a vector", false);
result();
return;
};
sync.leave(flags);
check("msiBind allocated a vector in the device window", vector >= architecture.irq_vector_base and
vector < architecture.irq_vector_base + architecture.irq_vector_count);
// Fire the vector — the stand-in for the device writing its MSI (address, data).
const tail: *volatile u8 = &endpoint.notify_tail;
const before = tail.*;
architecture.selfIpi(vector);
var spins: u64 = 0;
while (tail.* == before and spins < 100_000_000) : (spins += 1) scheduler.yield();
check("self-IPI at the MSI vector notified the bound endpoint", tail.* != before);
result();
}
/// IOMMU (M16): with an emulated VT-d unit present (the harness boots this case with
/// `-device intel-iommu`), danos must find it in the ACPI DMAR table, map its register
/// block, and read back a real version. This is *detection*, the honest first step —
/// no translation domains are programmed yet, so DMA is still unprotected; enforcement
/// lands with the first DMA driver (docs/driver-model.md M16).
fn iommuTest() void {
log("DANOS-TEST-BEGIN: iommu\n", .{});
const pinfo = platform.platformInformation();
check("IOMMU found in the DMAR table", pinfo.iommu_present);
check("VT-d unit has a register base", pinfo.iommu_base != 0);
check("VT-d version register reads back nonzero (real, mappable unit)", pinfo.iommu_version != 0);
log("DANOS-IOMMU: base=0x{x} version=0x{x} capabilities=0x{x}\n", .{ pinfo.iommu_base, pinfo.iommu_version, pinfo.iommu_capabilities });
result();
}
/// Port I/O grants: ring 3 has no `in`/`out`, so a legacy driver reaches its ports
/// through `io_read`/`io_write`, gated by `device_claim` and the device's `io_port`
/// resource exactly like `mmio_map` gates memory. Target the PS/2 controller's status
/// port (0x64) — discovered on every PC and side-effect-free to read. Proves the
/// capability gate (`resolveIoPort` admits an in-range access, refuses out-of-range,
/// over-wide, and unclaimed) and that the kernel actually performs the `in`. The
/// `io_read`/`io_write` syscalls wrap this with the same ring-3 dispatch every device
/// driver already uses.
fn ioPortTest() void {
log("DANOS-TEST-BEGIN: ioport\n", .{});
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
// Post-M20.3 the PS/2 node is registered at runtime by the ring-3 acpi
// service, so it is absent from this boot snapshot. Exercise the same
// io_port claim/resolve mechanism against the acpi-tables node's broad I/O
// grant — the window that now carries port authority (the service uses it
// for exactly this). The PS/2 status port 0x64 is offset 0x64 within it.
var found_id: ?u64 = null;
var found_res: u64 = 0;
outer: for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.acpi_tables)) continue;
for (0..d.resource_count) |ri| {
const r = d.resources[ri];
if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0 and r.len > 0x64) {
found_id = d.id;
found_res = ri;
break :outer;
}
}
}
const id = found_id orelse {
check("discovered the acpi-tables I/O window", false);
result();
return;
};
check("discovered the acpi-tables I/O window", true);
const me = scheduler.current();
check("claimed the io_port device", devices_broker.claim(id, me.id));
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64);
check("a 4-byte access at the last port is refused", process.resolveIoPort(me, id, found_res, 0xFFFF, 4) == null);
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 0x10000, 1) == null);
check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0x64, 1) == null);
// The kernel actually issues the `in`. Reaching this line at all proves it didn't
// fault; a width-1 read must return a single byte.
const status = architecture.pioRead(1, 0x64);
check("reading the PS/2 status port returned a byte", status <= 0xFF);
log("DANOS-IOPORT: PS/2 status = 0x{x}\n", .{status});
result();
}
/// The monotonic clock (the source `clock()` surfaces to user space). It must be
/// calibrated, move forward over a spin, and never run backwards — the guarantees a
/// driver's deadline timeout depends on. The syscall is a thin wrapper over this same
/// `architecture.nanos()`.
fn clockTest() void {
log("DANOS-TEST-BEGIN: clock\n", .{});
const t0 = architecture.nanos();
check("monotonic clock is calibrated (nonzero)", t0 != 0);
var last = t0;
var monotonic = true;
var advanced = false;
var i: u32 = 0;
while (i < 1_000_000) : (i += 1) {
const t = architecture.nanos();
if (t < last) monotonic = false;
if (t > t0) advanced = true;
last = t;
}
check("clock advanced over the spin", advanced);
check("clock never ran backwards (monotonic)", monotonic);
result();
}
var proc_worker_run: bool = true;
var proc_worker_ran: bool = false;
/// A kernel task that spins while a process runs, to prove the two coexist under
/// preemption (a process on its own CR3 does not stall kernel work).
fn procWorker() void {
const running: *volatile bool = &proc_worker_run;
const ran: *volatile bool = &proc_worker_ran;
while (running.*) ran.* = true;
scheduler.exit();
}
/// Real processes: load /system/services/init as TWO scheduled ring-3 processes, each with
/// its own address space at the same virtual addresses, running concurrently
/// with a kernel task. Both must make heartbeat syscalls from CPL 3 — which can
/// only happen if each runs on its own page tables (CR3 switched correctly per
/// process) and preemption interleaves them with the kernel worker. This is the
/// strongest cheap proof of address-space isolation.
fn processTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: process\n", .{});
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
process.write_count = 0;
process.write_from_user = false;
proc_worker_run = true;
proc_worker_ran = false;
scheduler.spawn(procWorker, 4); // kernel task at the processes' priority
var spawned: u32 = 0;
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
// Wait (real time) for several heartbeats across the two processes. Each
// process sleeps ~1 s between beats, so a few seconds yields several.
scheduler.setPriority(1); // drop below the workers so they get the cores
const deadline = architecture.millis() + 8000;
while (process.write_count < 4 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
proc_worker_run = false;
log("DANOS-PROC: spawned {d} processes, {d} heartbeats\n", .{ spawned, process.write_count });
check("both init processes spawned on their own address spaces", spawned == 2);
check("processes made repeated heartbeat syscalls (>=4)", process.write_count >= 4);
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
check("a kernel task coexisted with the processes (preemption)", proc_worker_ran);
result();
}
/// Isolation: a ring-3 read of a kernel-only page (the LAPIC page — present,
/// supervisor) must page-fault with error code 0x5 (present | user) at the user
/// RIP. The fault report is the pass signal (matched by the harness); if the
/// read is somehow allowed the blob spins and the harness times out.
fn userPfTest() void {
log("DANOS-TEST-BEGIN: user-pf\n", .{});
scheduler.setPreemption(false);
_ = process.run(process.pfBlob()) catch {};
log("DANOS-TEST-RESULT: FAIL (user read of kernel memory did not fault)\n", .{});
}
/// Spawn a real scheduled ring-3 process whose body is the user-pf blob (a read of
/// a kernel-only page, then a spin), so its first instruction raises #PF. Built by
/// hand — address space, code page RO+X, stack page RW+NX — because the blob is a
/// raw code fragment, not an ELF `spawnProcess` could load. Returns false if any
/// allocation fails.
fn spawnFaultingProcess() ?u32 {
const blob = process.pfBlob();
const flags = sync.enter();
defer sync.leave(flags);
const aspace = architecture.createAddressSpace() orelse return null;
const code_frame = pmm.alloc() orelse {
architecture.destroyAddressSpace(aspace);
return null;
};
// Fill through the physmap (the user mapping is read-only); pad with int3 so a
// stray jump traps instead of sliding.
const code: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(code_frame));
@memset(code[0..abi.page_size], 0xCC);
@memcpy(code[0..blob.len], blob);
architecture.mapUserPageInto(aspace, process.code_virtual, code_frame, false, true); // RO + X
const stack_frame = pmm.alloc() orelse {
architecture.destroyAddressSpace(aspace); // frees code_frame too — it's mapped
return null;
};
architecture.mapUserPageInto(aspace, process.stack_base_virtual, stack_frame, true, false); // RW + NX
// Supervised by the calling test task, so exitReasonOf can read the verdict.
const id = scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_base_virtual + abi.page_size, 4, "fault-probe", scheduler.currentId(), null) orelse {
architecture.destroyAddressSpace(aspace);
return null;
};
return id;
}
/// Fault recovery (docs/resilience.md step 2): a scheduled ring-3 process that
/// faults must be killed — counted, resources reclaimed — while the rest of the
/// system keeps running. init heartbeats before and after the kill are the proof
/// the OS survived; the old behaviour (halt the core) would freeze the beat and
/// time the harness out.
fn faultRecoveryTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: fault-recovery\n", .{});
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
process.write_count = 0;
process.fault_kill_count = 0;
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
check("init spawned as the surviving process", spawned);
// A first heartbeat proves init runs before the fault.
scheduler.setPriority(1); // drop below the processes so they get the core
var deadline = architecture.millis() + 8000;
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
check("init heartbeat before the fault", process.write_count >= 1);
const probe = spawnFaultingProcess() orelse 0;
check("faulting process spawned", probe != 0);
// The kill: the faulting process #PFs on its first instruction and the kernel
// reaps it instead of halting.
scheduler.setPriority(1);
deadline = architecture.millis() + 5000;
while (process.fault_kill_count < 1 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
check("faulting process was killed (not the machine)", process.fault_kill_count == 1);
check("the probe's reason reads segmentation_fault", process.exitReasonOf(scheduler.currentId(), probe) == @intFromEnum(abi.ExitReason.segmentation_fault));
// Life after the kill: init must keep beating on the same core.
const beats_at_kill = process.write_count;
scheduler.setPriority(1);
deadline = architecture.millis() + 8000;
while (process.write_count < beats_at_kill + 2 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
check("init kept heartbeating after the kill", process.write_count >= beats_at_kill + 2);
result();
}
/// The full PID-1 path: the bootloader read /system/services/init off the boot volume and
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
/// — the same call the normal boot path makes — then confirm it beats. init
/// heartbeats forever, so this proves it reaches ring 3, makes repeated syscalls
/// (write + sleep), and stays alive rather than exiting.
fn initTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: init\n", .{});
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
process.write_count = 0;
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |err| blk: {
log("DANOS-INIT-ERR: {s}\n", .{@errorName(err)});
break :blk false;
};
check("init loaded and spawned as a process", spawned);
// Wait (real time) for at least two heartbeats — proving it runs, writes,
// and sleeps repeatedly (init sleeps ~1 s between beats).
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 2 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
const prefix = "init: heartbeat";
const beat_ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("init produced repeated heartbeats (>=2)", process.write_count >= 2);
check("heartbeat text arrived intact", beat_ok);
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
check("init is still alive (did not exit)", process.exit_code == 0);
result();
}
/// process_enumerate's kernel half: spawn two init processes next to the kernel
/// tasks and snapshot the table. The snapshot must list both by name with distinct,
/// kernel-supervised ids, include the kernel tasks (id 0, empty name), and report
/// the same total through a too-small buffer (the truncation contract: the caller
/// learns how big a buffer to bring).
fn processListTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: process-list\n", .{});
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
var spawned: u32 = 0;
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
check("two init processes spawned", spawned == 2);
var table: [32]abi.ProcessDescriptor = undefined;
const total = scheduler.enumerate(&table);
check("enumerate counts the boot task and both processes (>=3)", total >= 3);
var inits: u32 = 0;
var init_ids: [2]u32 = .{ 0, 0 };
var kernel_task_seen = false;
var states_sane = true;
for (table[0..@min(total, table.len)]) |descriptor| {
if (descriptor.state > @intFromEnum(abi.ProcessState.blocked)) states_sane = false;
if (descriptor.name_length == 0) kernel_task_seen = true;
if (eql(descriptor.name[0..descriptor.name_length], "/system/services/init")) {
if (inits < 2) init_ids[inits] = descriptor.id;
inits += 1;
check("init entry is kernel-supervised (supervisor 0)", descriptor.supervisor == 0);
}
}
check("both init processes listed by name", inits == 2);
check("listed processes carry distinct ids", init_ids[0] != init_ids[1]);
check("kernel tasks are listed too (empty name)", kernel_task_seen);
check("every state is a ProcessState value", states_sane);
var one: [1]abi.ProcessDescriptor = undefined;
check("a too-small buffer still learns the true total", scheduler.enumerate(&one) == total);
result();
}
/// process_kill + the exit notification, kernel half. Two victims, two paths:
/// - init, which heartbeats and sleeps: caught blocked, reaped on the killer's
/// own call — and its heartbeat must stop.
/// - process-test's spinner role (from the initial ramdisk), which loops in user
/// mode making no system calls: with more cores it is caught running, taking
/// the deferred path (kill_pending, finished by the victim core's next tick).
/// Each death must post one exit notification badge (exit bit + the child's id)
/// on the endpoint given at spawn; wrong-supervisor and unknown-id kills must be
/// refused. The waits block in replyWait, so a lost notification times the
/// harness out rather than passing vacuously.
fn processKillTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: process-kill\n", .{});
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
const me = scheduler.currentId();
const endpoint = ipcsync.createIpcEndpoint() orelse {
check("exit endpoint allocated", false);
result();
return;
};
process.write_count = 0;
const sleeper = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
check("init spawned as the supervised sleeper victim", sleeper != 0);
// Let it reach its heartbeat loop (write, then a 1 s sleep) so the kill most
// likely catches it blocked.
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
check("victim heartbeat before the kill", process.write_count >= 1);
// Kills that must be refused, before the one that must not be.
check("a non-supervisor may not kill (-EPERM)", process.killProcess(me + 12345, sleeper) == -ipcsync.EPERM);
check("an unknown id misses (-ESRCH)", process.killProcess(me, 0xFFFF_FF00) == -ipcsync.ESRCH);
check("a kernel task is not a killable process (-ESRCH)", process.killProcess(me, 0) == -ipcsync.ESRCH);
check("the supervisor's kill is accepted", process.killProcess(me, sleeper) == 0);
var badge: u64 = 0;
var received_cap: u64 = 0;
var r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the sleeper's exit notification arrived (length 0)", r == 0);
check("its badge carries the exit bit and the child id", badge == abi.notify_badge_bit | abi.notify_exit_bit | sleeper);
// M17.2: the recorded reason — the notification is the fence, so it is
// already readable, and gated by the same supervisor check as the kill.
check("the sleeper's reason reads killed", process.exitReasonOf(me, sleeper) == @intFromEnum(abi.ExitReason.killed));
check("a non-supervisor may not read the reason (-EPERM)", process.exitReasonOf(me + 12345, sleeper) == -ipcsync.EPERM);
check("an unknown id has no reason (-ESRCH)", process.exitReasonOf(me, 0xFFFF_FF00) == -ipcsync.ESRCH);
const beats_at_kill = process.write_count;
scheduler.sleep(1500); // more than one heartbeat period
check("the heartbeat stopped with the kill", process.write_count == beats_at_kill);
// The spinner: no system calls, so only the tick can deliver a deferred kill.
var spinner: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "process-test")) continue;
spinner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "spinner" }, me, endpoint) catch 0;
break;
}
check("process-test spawned as the supervised spinner victim", spinner != 0);
scheduler.sleep(100); // give another core a chance to be running it
check("the spinner's kill is accepted", process.killProcess(me, spinner) == 0);
r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the spinner's exit notification arrived (length 0)", r == 0);
check("its badge carries the exit bit and the child id", badge == abi.notify_badge_bit | abi.notify_exit_bit | spinner);
// M17.2: a child that ends on its own must read exited, not killed —
// args-echo with arguments echoes once and returns from main.
var clean: u32 = 0;
i = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "args-echo")) continue;
clean = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "clean-exit" }, me, endpoint) catch 0;
break;
}
check("args-echo spawned as the clean-exit child", clean != 0);
r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the clean child's exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | clean);
check("the clean child's reason reads exited", process.exitReasonOf(me, clean) == @intFromEnum(abi.ExitReason.exited));
var table: [32]abi.ProcessDescriptor = undefined;
const total = scheduler.enumerate(&table);
var still_listed = false;
for (table[0..@min(total, table.len)]) |descriptor| {
if (descriptor.id == sleeper or descriptor.id == spinner) still_listed = true;
}
check("neither victim is listed after its kill", !still_listed);
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
result();
}
/// M17.1: a dead process's device claims are released by the reap, so a restarted
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
/// First the broker release in isolation — two owners, one released, the other's
/// claim must survive. Then the death-path wiring with a real child: the claim is
/// made on the child's behalf (the broker is kernel-callable), the child is
/// killed, and once the exit notification arrives — posted last, after release —
/// the device must be unclaimed and claimable again.
fn claimReleaseTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: claim-release\n", .{});
var buffer: [2]device_abi.DeviceDescriptor = undefined;
const total = devices_broker.enumerate(&buffer);
check("the device tree is seeded (>= 2 devices)", total >= 2);
if (total < 2) {
result();
return;
}
// The broker release in isolation.
check("device 0 claimed by owner 111", devices_broker.claim(0, 111));
check("device 1 claimed by owner 222", devices_broker.claim(1, 222));
devices_broker.releaseAllOwnedBy(111);
check("owner 111's claim is released", devices_broker.ownerOf(0) == null);
check("owner 222's claim survives", (devices_broker.ownerOf(1) orelse 0) == 222);
devices_broker.releaseAllOwnedBy(222);
check("cleanup released owner 222", devices_broker.ownerOf(1) == null);
// The death-path wiring: a real process dies holding a claim.
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const me = scheduler.currentId();
const endpoint = ipcsync.createIpcEndpoint() orelse {
check("exit endpoint allocated", false);
result();
return;
};
const child = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
check("supervised child spawned", child != 0);
check("device 0 claimed on the child's behalf", devices_broker.claim(0, child));
check("the kill is accepted", process.killProcess(me, child) == 0);
var badge: u64 = 0;
var received_cap: u64 = 0;
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
check("death released the child's claim", devices_broker.ownerOf(0) == null);
check("the device is claimable again", devices_broker.claim(0, me));
devices_broker.releaseAllOwnedBy(me);
result();
}
/// M17.3: the published exit events, proven by their first subscriber. The VFS
/// subscribes at startup; a client opens a file and parks holding the handle;
/// the kill posts the exit event to the VFS's endpoint; the VFS releases the
/// dead client's handle and says so — the service-side mirror of iron rule 1
/// (a service must never depend on clients cleaning up after themselves).
fn vfsClientDeathTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: vfs-client-death\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.write_count = 0;
check("vfs spawned", spawnNamed(rd, "vfs"));
const me = scheduler.currentId();
const endpoint = ipcsync.createIpcEndpoint() orelse {
check("exit endpoint allocated", false);
result();
return;
};
var client: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "vfs-test")) continue;
client = process.spawnProcessSupervised(item.blob, 4, &.{ "vfs-test", "park" }, me, endpoint) catch 0;
break;
}
check("parked client spawned (supervised)", client != 0);
// Its heartbeat is the fence: once it beats, the handle is open.
const parked = "vfstest: parked";
scheduler.setPriority(1);
var deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= parked.len and eql(process.write_buffer[0..parked.len], parked)) break;
scheduler.yield();
}
scheduler.setPriority(4);
check("client parked holding an open handle", process.write_len >= parked.len and eql(process.write_buffer[0..parked.len], parked));
check("the kill is accepted", process.killProcess(me, client) == 0);
var badge: u64 = 0;
var received_cap: u64 = 0;
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | client);
// The VFS heard the same published event; its release line is the proof.
const released = "vfs: released 1 handle(s) for dead client";
scheduler.setPriority(1);
deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= released.len and eql(process.write_buffer[0..released.len], released)) break;
scheduler.yield();
}
scheduler.setPriority(4);
check("the VFS released the dead client's handle", process.write_len >= released.len and eql(process.write_buffer[0..released.len], released));
result();
}
/// M17.4 from ring 3: process-test's signal-run role drives the whole lifecycle
/// surface — the zero-length ping (answered by the harness), signals as
/// statements (reload logged, terminate = clean exit), the one-shot timer, and
/// both endings of the stop sequence (polite -> exited, deaf -> killed at the
/// deadline). Its "process-test: signals ok" is the pass marker.
fn signalsTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: signals\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image); // the parent system_spawns its children by name
process.write_count = 0;
var runner: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "process-test")) continue;
runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0;
break;
}
check("signal-run parent spawned", runner != 0);
const pass_marker = "process-test: signals ok";
const fail_marker = "process-test: FAIL";
scheduler.setPriority(1);
const deadline = architecture.millis() + 15000;
var saw_pass = false;
var saw_fail = false;
while (architecture.millis() < deadline and !saw_pass and !saw_fail) {
if (process.write_len >= pass_marker.len and eql(process.write_buffer[0..pass_marker.len], pass_marker)) saw_pass = true;
if (process.write_len >= fail_marker.len and eql(process.write_buffer[0..fail_marker.len], fail_marker)) saw_fail = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("the signal-run parent reported ok", saw_pass and !saw_fail);
result();
}
/// M18.1: the device manager's restart machinery, end to end. In test-restart
/// mode the manager also supervises crash-test: a fixture that claims device 0,
/// hellos, and faults. The scenario asserts three markers in order — the real
/// xHCI driver hellos clean and stays; crash-test is restarted with backoff
/// (each respawn re-claiming the device the dead instance held, M17.1 through
/// the manager's path); the crash loop caps and the manager gives up.
fn driverRestartTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: driver-restart\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image); // the manager system_spawns drivers by name
process.write_count = 0;
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-restart" }, scheduler.currentId(), null) catch 0;
break;
}
check("device-manager spawned in test-restart mode", manager != 0);
// The assertions live in the harness: its expect regex requires, in order,
// the xHCI hello ack, a crash-test restart, and the crash-loop cap — read
// from the whole serial capture, immune to the transient-line races a
// write_buffer poll would have here (many processes log concurrently).
result();
}
/// M18.2: bus tree reports, end to end. The manager (test-usb-restart mode)
/// spawns the xHCI driver; the driver maps its BAR, scans the root-hub ports,
/// and reports the two QEMU devices; the manager mirrors them, kills the
/// reporter (the test trigger), prunes both children, restarts the driver with
/// backoff, and the respawned instance re-claims, re-scans, and re-reports.
/// The harness's ordered expect regex is the assertion; this test only
/// orchestrates the spawn.
fn usbReportTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: usb-report\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image);
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
break;
}
check("device-manager spawned in test-usb-restart mode", manager != 0);
result();
}
/// M18.3: the application surface. device-list enumerates the manager's tree
/// over IPC, subscribes with its endpoint as a capability, and prints every
/// published event; the manager's delayed test-kill of the reporter produces a
/// removed/added storm the subscriber must observe. The harness's ordered
/// expect regex is the assertion.
fn deviceListTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: device-list\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image);
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
break;
}
check("device-manager spawned in test-usb-restart mode", manager != 0);
check("device-list spawned", spawnNamed(rd, "device-list"));
result();
}
/// M19.1: the ring-3 PCI scan agrees with the kernel's. The manager spawns
/// pci-bus for the host bridge; the driver walks the same ECAM window through
/// its mmio_map grant and must find exactly the functions the kernel's own
/// enumeration recorded — the equivalence that licenses retiring the kernel
/// walk in M19.3.
fn pciScanTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: pci-scan\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
// 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.
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);
process.setInitialRamdisk(image);
process.write_count = 0;
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0;
break;
}
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);
var deadline = architecture.millis() + 15000;
while (architecture.millis() < deadline and reported == 0) {
if (process.write_len > count_prefix.len + count_suffix.len and eql(process.write_buffer[0..count_prefix.len], count_prefix)) {
const line = process.write_buffer[0..process.write_len];
const digits_end = std.mem.indexOf(u8, line, count_suffix) orelse {
scheduler.yield();
continue;
};
reported = std.fmt.parseInt(u32, line[count_prefix.len..digits_end], 10) catch 0;
}
scheduler.yield();
}
scheduler.setPriority(4);
check("the ring-3 scan reported a function count", reported >= 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;
}
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 (process.write_len >= restart_marker.len and eql(process.write_buffer[0..restart_marker.len], 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 (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], 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);
result();
}
/// M21.3 capstone: orderly shutdown. Boot init with the initial-ramdisk
/// published, so init spawns the full service tree (vfs, input, device-manager
/// -> discovery/acpi); the harness injects a real power-button event via QMP;
/// the acpi service publishes it; init runs the stop sequence over its children
/// and asks the power service for S5; the machine powers off (QEMU exits). The
/// kernel test only spawns init — the ordered chain is the harness assertion.
fn orderlyShutdownTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{});
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over init and the initial_ramdisk", false);
result();
return;
}
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
process.setInitialRamdisk(ramdisk);
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
check("init spawned as PID root of user space", spawned);
result();
}
/// M20.2: the acpi service registers + reports its _HID devices. Boot normally
/// (the manager spawns discovery); the harness's expect regex requires the two
/// PS/2 nodes among the service's report lines, each with its _CRS resources —
/// the ring-3 _CRS/_STA evaluation working end to end. The kernel test only
/// starts the manager.
fn acpiReportTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: acpi-report\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image);
var spawned = false;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
_ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
spawned = true;
break;
}
check("device-manager spawned", spawned);
result();
}
/// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns
/// the discovery service (the acpi build variant); it claims the acpi-tables
/// node, maps the blobs, parses them, and logs its Device count — which must
/// equal what the kernel's own parse produced (the equivalence that licenses
/// retiring the kernel's device build in M20.3).
fn acpiParseTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: acpi-parse\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
// The kernel's own count, from the namespace it already built for \_S5.
const kernel_devices = platform.amlDeviceCount();
check("the kernel namespace has devices to compare against", kernel_devices >= 1);
// Spawn the discovery service directly with that count as argv: it parses
// the same blobs in ring 3 and self-verifies, printing "acpi-parse: ok" iff
// the counts match. The harness's expect regex is that marker — deterministic,
// no racing the shared serial buffer.
process.setInitialRamdisk(image);
var count_text: [16]u8 = undefined;
const count_arg = std.fmt.bufPrint(&count_text, "{d}", .{kernel_devices}) catch "0";
var spawned = false;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "discovery")) continue;
_ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", count_arg }, scheduler.currentId(), null) catch 0;
spawned = true;
break;
}
check("discovery service spawned", spawned);
result();
}
/// The whole user-side surface at once: spawn process-test's supervisor role,
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
/// children supervised, sees them in process_enumerate, kills them (one blocked,
/// one spinning), collects both exit notifications, and confirms they are gone.
/// Its "process-test: ok" is the pass marker; any FAIL line is specific.
fn supervisionTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: supervision\n", .{});
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
if (boot_information.initial_ramdisk_len == 0) {
result();
return;
}
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(ramdisk); // the supervisor system_spawns its children by name
process.write_count = 0;
process.write_from_user = false;
var started = false;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "process-test")) continue;
started = if (process.spawnProcess(item.blob, 4, &.{ "process-test", "run" })) true else |_| false;
break;
}
check("process-test spawned as the user-space supervisor", started);
const marker = "process-test: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker)) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker);
if (!ok and process.write_len > 0) log("DANOS-SUPERVISION: got \"{s}\"\n", .{process.write_buffer[0..process.write_len]});
check("the supervisor completed every step (spawn/list/kill/notify)", ok);
check("it ran in user mode (CPL 3)", process.write_from_user);
result();
}
/// The initial_ramdisk path: the bootloader handed over an image bundling extra user
/// binaries; parse it, spawn every program, and confirm one (the vfs stub)
/// reaches ring 3 and heartbeats — proving the whole ferry-parse-spawn pipeline.
fn initialRamdiskTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: initial_ramdisk\n", .{});
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
if (boot_information.initial_ramdisk_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
check("initial_ramdisk image is valid", true);
check("initial_ramdisk contains at least one binary", rd.count >= 1);
process.write_count = 0;
process.write_from_user = false;
var spawned: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (process.spawnProcess(item.blob, 4, &.{item.name})) spawned += 1 else |err| {
log("DANOS-INITRD-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
}
}
check("every initial_ramdisk binary spawned", spawned == rd.count);
// Wait for the spawned programs to run and make syscalls (they write + sleep).
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 2 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
check("initial_ramdisk processes ran and made syscalls (>=2)", process.write_count >= 2);
check("syscalls came from user mode (CPL 3)", process.write_from_user);
result();
}
/// The full VFS path: spawn the user-space VFS server and a client from the
/// initial_ramdisk. The client opens a file through the runtime file API, writes, seeks, reads
/// it back, and — only if the round trip matched — heartbeats "vfstest: ok". So
/// seeing that marker proves client open/write/read reached the server over IPC
/// and came back correct. (The client retries until the server registers.)
fn vfsTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: vfs\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.write_count = 0;
process.write_from_user = false;
// Spawn just the server and its client (other initial_ramdisk binaries would write to
// the shared evidence buffer and confuse the marker check).
_ = spawnNamed(rd, "vfs");
_ = spawnNamed(rd, "vfs-test");
// Wait for the client's success heartbeat (it round-trips, then beats ~1/s).
const prefix = "vfstest: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix) and process.write_count >= 2) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("client completed the VFS round trip (open/write/read matched)", ok);
check("the round trip ran repeatedly (server stays up)", process.write_count >= 2);
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
result();
}
/// The full input path: spawn the input service, a synthetic source, and a subscriber from
/// the initial_ramdisk. The source publishes keyboard, mouse, and joystick events in turn;
/// the service routes them (with the asynchronous ipc_send) to the subscriber, which took
/// all three classes and — only once it has received one — heartbeats "input-test: ok".
/// Seeing that marker proves an event travelled source -> service -> subscriber over IPC,
/// exercising the async buffered-send primitive, capability-passing subscription, and
/// per-device routing. The source and service stay silent after startup so the subscriber's
/// line is the one left in the shared evidence buffer.
fn inputTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: input\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.write_count = 0;
process.write_from_user = false;
_ = spawnNamed(rd, "input"); // the fan-out service
_ = spawnNamed(rd, "input-source"); // a synthetic keyboard publishing events
_ = spawnNamed(rd, "input-test"); // the subscriber whose "ok" line is the marker
// Wait for the subscriber's success heartbeat (it beats once per received event).
const prefix = "input-test: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 12000;
while (architecture.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix) and process.write_count >= 2) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("a subscriber received a broadcast key event over IPC (source -> service -> subscriber)", ok);
check("events kept flowing (service + async send stay up)", process.write_count >= 2);
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
result();
}
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
/// blob. Instance 2 parses the kernel-built System V entry stack via the runtime
/// and echoes its whole argv in one write, which must arrive exactly as sent.
fn argsTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: args\n", .{});
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
if (boot_information.initial_ramdisk_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image); // args-echo respawns itself through system_spawn
process.write_count = 0;
process.write_from_user = false;
check("args-echo spawned from the initial_ramdisk", spawnNamed(rd, "args-echo"));
// Wait for the *second* instance's echo (the first writes nothing).
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
const expected = "args: args-echo alpha beta-42\n";
const echoed = process.write_len == expected.len and eql(process.write_buffer[0..process.write_len], expected);
if (!echoed and process.write_len > 0) log("DANOS-ARGS: got \"{s}\"\n", .{process.write_buffer[0..process.write_len]});
check("argv arrived intact (argv[0] = name, argv[1..] = spawn arguments)", echoed);
check("echo came from user mode (CPL 3)", process.write_from_user);
result();
}
/// Spawn the initial_ramdisk binary named `name` as a ring-3 process. Returns false if it
/// isn't in the image or fails to load.
fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (eql(item.name, name)) {
return if (process.spawnProcess(item.blob, 4, &.{item.name})) true else |_| false;
}
}
return false;
}
/// IO passthrough + IRQ-as-IPC: a user-space driver drives real hardware and is
/// *woken by it*. Spawn hpet, which claims the HPET, maps its registers into its
/// own ring-3 address space, arms a level-triggered comparator, binds the interrupt
/// to an IPC endpoint, and then blocks. It prints "hpet: ok" only after being woken
/// `target_ticks` times — it cannot reach that line by polling, because the loop's
/// only exit is through `replyWait` returning a notification badge.
///
/// The interesting assertion is the last one, which doesn't trust hpet at all: it
/// reads the I/O APIC's redirection entry back and checks the kernel really routed
/// the line (our vector, level-triggered) and really left it unmasked after the
/// driver's final `irq_ack`. hpet disables its comparator on the last interrupt, so
/// that state is quiescent and not a race.
fn hpetTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: hpet\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.write_count = 0;
process.write_from_user = false;
check("hpet spawned from the initial_ramdisk", spawnNamed(rd, "hpet"));
const prefix = "hpet: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix) and process.write_count >= 2) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("user driver mapped HPET MMIO and was woken by its interrupt", ok);
check("driver syscalls came from user mode (CPL 3)", process.write_from_user);
check("kernel routed and re-armed the HPET's line at the I/O APIC", hpetRouteOk());
result();
}
/// Read back the I/O APIC redirection entry for the HPET's GSI and confirm the
/// kernel programmed it: a vector in the device window, level-triggered, unmasked.
/// Independent of anything the driver reported about itself.
fn hpetRouteOk() bool {
const gsi = hpetGsi() orelse return false;
if (gsi >= architecture.irqRouteCount()) return false;
const low = architecture.irqRouteRaw(gsi); // entry index == GSI (this I/O APIC's gsi_base is 0)
const vector: u8 = @truncate(low & 0xFF);
const masked = low & (1 << 16) != 0;
const level = low & (1 << 15) != 0;
return vector >= architecture.irq_vector_base and
vector < architecture.irq_vector_base + architecture.irq_vector_count and
level and !masked;
}
/// The GSI discovery recorded for the HPET, from the same device table the driver saw.
fn hpetGsi() ?u32 {
var buffer: [16]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.timer)) continue;
if (d.parent != device_abi.no_parent) continue; // the block, not a comparator child
for (0..d.resource_count) |j| {
const r = d.resources[j];
if (r.kind == @intFromEnum(device_abi.ResourceKind.irq)) return @intCast(r.start);
}
}
return null;
}
/// Bus driver: a user process claims a device that contains other devices, enumerates
/// them from the hardware, and publishes each as a child via `device_register` — the
/// primitive a PCI bridge or USB hub driver is built from.
///
/// `bus` treats the HPET's register block as a bus and its comparators as children,
/// giving each a 0x20 sub-window. It checks its own work (children come back from the
/// table with the right parent and a strictly narrower window) and, importantly, that
/// the kernel **refuses** a child whose window escapes the parent's — without that,
/// `device_register` would be a system_call for mapping arbitrary physical memory. It prints
/// "bus: ok" only if all of that holds.
///
/// The kernel-side check here is the one bus can't make: that the children really did
/// land in the device table with the containment invariant intact.
fn busTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: bus\n", .{});
// M19.0: device_register is idempotent on exact match — a restarted
// registering bus must not duplicate its children. Driven directly against
// the broker: claim an unclaimed node, register the same (class, hid,
// resourceless) child twice, expect one id and one table entry.
{
const me = scheduler.currentId();
var probe: [1]device_abi.DeviceDescriptor = undefined;
const total = devices_broker.enumerate(&probe);
check("device tree is seeded for the idempotence check", total >= 1);
if (devices_broker.ownerOf(0) == null) {
check("claimed device 0 for the idempotence check", devices_broker.claim(0, me));
var child = std.mem.zeroes(device_abi.DeviceDescriptor);
child.class = @intFromEnum(device_abi.DeviceClass.unknown);
child.pci_class = device_abi.no_pci_class;
child.hid_len = 4;
child.hid[0..4].* = "idem".*;
const first = devices_broker.register(0, me, &child) catch 0;
check("first register succeeded", first != 0);
const before = devices_broker.enumerate(&probe);
const second = devices_broker.register(0, me, &child) catch 0;
check("re-register returned the same id", second == first);
check("re-register grew nothing", devices_broker.enumerate(&probe) == before);
devices_broker.releaseAllOwnedBy(me);
} else {
check("device 0 unexpectedly claimed before the idempotence check", false);
}
}
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.write_count = 0;
process.write_from_user = false;
check("bus spawned from the initial_ramdisk", spawnNamed(rd, "bus"));
const prefix = "bus: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix)) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("bus driver published children and the kernel refused an out-of-window one", ok);
check("driver syscalls came from user mode (CPL 3)", process.write_from_user);
check("every registered child is contained in its parent", childrenContained());
result();
}
/// The device manager (a ring-3 service) enumerates /system/devices, matches each
/// device to a driver, and — eventually — spawns it. This increment only checks the
/// discovery+matching half: it must find the HPET (a timer) and decide `hpet` serves
/// it, printing "device-manager: ok". It uses no special privilege — the same
/// `device_enumerate` any process could call. (Spawning is the next increment.)
fn deviceManagerTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: device-manager\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
// Let `system_spawn` find bundled binaries by name (the normal boot path does
// this too). Only the device-manager is spawned here — so if `hpet` runs at all,
// it's because the manager discovered the timer, matched, and spawned it.
process.setInitialRamdisk(image);
process.write_count = 0;
process.write_from_user = false;
check("device-manager spawned from the initial_ramdisk", spawnNamed(rd, "device-manager"));
// End-to-end proof: the driver the manager spawned reaches its own live marker.
// `hpet: ok` is hpet's final, stable message (it claims the timer, maps its MMIO,
// binds its IRQ, services one, then sleeps) — nothing overwrites the buffer after,
// so it's race-free to poll for. Its arrival means the whole
// discover -> match -> system_spawn -> driver-up chain worked.
const prefix = "hpet: ok";
scheduler.setPriority(1);
const deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix)) break;
scheduler.yield();
}
scheduler.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
check("device manager matched the timer and system_spawn'd hpet, which came up", ok);
check("its syscalls came from user mode (CPL 3)", process.write_from_user);
result();
}
/// Every child `bus` registered must have each of its resources inside a parent
/// resource of the same kind — the invariant `device_register` exists to maintain,
/// checked from the kernel's own table rather than the driver's word for it.
///
/// Only *registered* children are checked, not the whole tree. Firmware topology is
/// trusted and doesn't obey containment: a PCI function's BAR is not inside its host
/// bridge's `bus_range`, because a bus-number range isn't an address window.
fn childrenContained() bool {
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
const bus_id = hpetDeviceId() orelse return false;
const p = buffer[@intCast(bus_id)];
var children: usize = 0;
for (buffer[0..n]) |d| {
if (d.parent != bus_id) continue;
children += 1;
for (0..d.resource_count) |i| {
const r = d.resources[i];
var ok = false;
for (0..p.resource_count) |j| {
const pr = p.resources[j];
if (pr.kind != r.kind) continue;
if (r.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
if (pr.start == r.start) ok = true;
} else if (r.len != 0 and r.start >= pr.start and
r.start + r.len <= pr.start + pr.len) ok = true;
}
if (!ok) return false;
}
}
return children > 0; // bus must have published at least one
}
/// Device id of the HPET (the bus bus claims), from the same table drivers see.
fn hpetDeviceId() ?u64 {
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.timer)) continue;
if (d.parent != device_abi.no_parent) continue; // a comparator child, not the block
for (0..d.resource_count) |j| {
if (d.resources[j].kind == @intFromEnum(device_abi.ResourceKind.memory)) return d.id;
}
}
return null;
}
/// IRQ teardown. When a driver exits, its bindings must be released: the line masked
/// (so a dead driver's device goes quiet instead of storming) and the slot cleared
/// (so an ISR never posts a notification into the endpoint that is about to be freed).
///
/// This is the path `hpet` never takes — it runs forever — so it gets its own test.
/// Two properties, both read back from the hardware rather than from our own state:
///
/// 1. A bound GSI is routed and unmasked.
/// 2. After `releaseOwner` for the binding's owner, that same entry is masked again.
///
/// And one property that can only be checked from kernel state: a *different* owner's
/// binding on the same endpoint survives. Endpoints are shared (ipc_register hands out
/// references), so teardown keyed on the endpoint pointer rather than the owning task
/// would mask a live sibling driver's device line.
fn irqFreeTest() void {
log("DANOS-TEST-BEGIN: irqfree\n", .{});
const gsi = hpetGsi() orelse {
check("discovery recorded an IRQ resource for the HPET", false);
result();
return;
};
const endpoint = ipcsync.createIpcEndpoint() orelse {
check("allocated an endpoint", false);
result();
return;
};
// Two owners, one shared endpoint. `other` binds a second line if the I/O APIC has
// one spare; if not, the sharing half of the test is skipped rather than faked.
const owner: u32 = 4242;
const other: u32 = 4343;
const spare: ?u32 = if (gsi + 1 < irq.maximum_gsi and architecture.irqOwnsGsi(gsi + 1)) gsi + 1 else null;
{
const flags = sync.enter();
defer sync.leave(flags);
irq.bind(gsi, endpoint, owner) catch {};
if (spare) |s| irq.bind(s, endpoint, other) catch {};
}
check("bound GSI is routed and unmasked", !entryMasked(gsi));
if (spare) |s| check("second owner's GSI is routed and unmasked", !entryMasked(s));
{
const flags = sync.enter();
defer sync.leave(flags);
irq.releaseOwner(owner);
}
check("exiting owner's line is masked again", entryMasked(gsi));
if (spare) |s| {
check("a sibling owner's binding on the same endpoint survives", !entryMasked(s));
const flags = sync.enter();
defer sync.leave(flags);
irq.releaseOwner(other);
}
result();
}
/// Is redirection entry `gsi` masked? (bit 16 of the low dword; entry index == GSI
/// because this I/O APIC's gsi_base is 0.)
fn entryMasked(gsi: u32) bool {
return architecture.irqRouteRaw(gsi) & (1 << 16) != 0;
}
/// The MMIO-grant teardown fix: a device-granted leaf must NOT be returned to the
/// RAM allocator when its address space is destroyed. Map a real RAM frame as a
/// device grant, tear the address space down, and confirm the frame is still held
/// (only the page tables came back) — then free it explicitly. A regression guard
/// for the freeSubtree device_grant skip that keeps IO passthrough from corrupting
/// the frame pool.
fn ioPassTest() void {
log("DANOS-TEST-BEGIN: iopass\n", .{});
const base_free = pmm.stats().free_frames;
const aspace = architecture.createAddressSpace() orelse {
check("created a fresh address space", false);
result();
return;
};
const frame = pmm.alloc() orelse {
architecture.destroyAddressSpace(aspace);
check("allocated a frame to grant", false);
result();
return;
};
// Map it the way mmio_map does (device grant), then tear the space down.
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size);
architecture.destroyAddressSpace(aspace);
// The page tables were reclaimed; the device-granted frame must not have been.
check("device-granted frame survived teardown (not reclaimed as RAM)", pmm.stats().free_frames == base_free - 1);
pmm.free(frame);
check("no leak once the frame is explicitly freed", pmm.stats().free_frames == base_free);
result();
}
fn faultInvalidOpcode() void {
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
asm volatile ("ud2");
}
/// Verify NX: fetching an instruction from a data page (mapped no-execute) faults.
fn faultNoExecute() void {
log("DANOS-TEST-BEGIN: fault-nx\n", .{});
var scratch: u64 = 0xC3; // a lone `ret` — harmless if NX somehow let it run
const f: *const fn () void = @ptrFromInt(@intFromPtr(&scratch));
f(); // instruction fetch from an NX page -> #PF before it executes
log("DANOS-TEST-RESULT: FAIL (NX not enforced)\n", .{});
}
/// Verify the null guard: dereferencing address 0 (page 0 left unmapped) faults.
fn faultNull() void {
log("DANOS-TEST-BEGIN: fault-null\n", .{});
// Launder the address through empty asm so the compiler no longer knows it's
// 0 (otherwise it folds a null-pointer safety panic instead of doing the real
// access). `allowzero` skips the same null check on the cast. The write then
// hits the unmapped page 0 and takes a real hardware #PF.
var address: u64 = 0;
address = asm (""
: [ret] "=r" (-> u64),
: [in] "0" (address),
);
const p: *allowzero volatile u64 = @ptrFromInt(address);
p.* = 1;
}
fn faultPageFault() void {
log("DANOS-TEST-BEGIN: fault-pf\n", .{});
// Runtime address so the backend emits a register store (not a `mov moffs`,
// which the self-hosted x86_64 backend can't encode).
var address: u64 = 0xdeadbeef000; // well above all mapped RAM
const p: *volatile u64 = @ptrFromInt(address);
p.* = 1;
address += 0;
}
fn faultDoubleFault() void {
log("DANOS-TEST-BEGIN: fault-df\n", .{});
architecture.disableInterrupts(); // so only the ud2 delivery (not a timer tick) triggers the #DF
// Point RSP at unmapped memory, then fault: the CPU can't push the fault
// frame, which escalates to #DF — survivable only because #DF runs on IST1.
var bad_sp: u64 = 0x5000000000;
asm volatile (
\\mov %[sp], %%rsp
\\ud2
:
: [sp] "r" (bad_sp),
: .{ .memory = true });
bad_sp += 0;
}
var ap_reached_fault: bool = false;
/// A task that faults with a #DF *on whatever core it's pinned to*. Announces the
/// core, then triggers the same double fault as `faultDoubleFault` — which is only
/// survivable on IST1, so it exercises that core's own TSS.
fn apDoubleFaultTask() void {
log("DANOS-AP: task running on core {d}, triggering #DF\n", .{scheduler.currentCpuIndex()});
@atomicStore(bool, &ap_reached_fault, true, .release);
architecture.disableInterrupts();
var bad_sp: u64 = 0x5000000000;
asm volatile (
\\mov %[sp], %%rsp
\\ud2
:
: [sp] "r" (bad_sp),
: .{ .memory = true });
bad_sp += 0;
}
/// Fault on an application processor. Pins a double-faulting task to core 1, so the
/// fault is taken and handled by *that core's own* IDT and TSS/IST — not the BSP's.
/// The harness matches "core N: double fault (vector 8)" with N ≥ 1, which can only
/// appear if the AP caught the #DF on its IST1 (a broken per-core TSS would
/// triple-fault and reset instead). We then show the BSP still runs afterwards, so
/// the fault was *contained* to the AP, not fatal to the system.
fn faultApTest() void {
log("DANOS-TEST-BEGIN: fault-ap-df\n", .{});
if (!scheduler.spawnOn(apDoubleFaultTask, 6, 1)) {
log("DANOS-AP: could not pin to core 1 (run with -smp) - FAIL\n", .{});
architecture.halt();
}
// Wait until the AP is about to fault, then keep running to prove containment.
var spins: u64 = 0;
while (!@atomicLoad(bool, &ap_reached_fault, .acquire) and spins < 5_000_000_000) spins +%= 1;
var settle: u64 = 0;
while (settle < 500_000_000) settle +%= 1; // let the AP take + report the fault
log("DANOS-BSP: core {d} still running after the AP fault (contained)\n", .{scheduler.currentCpuIndex()});
architecture.halt();
}