Hardened paging into a real VMM

This commit is contained in:
2026-07-03 13:29:34 +01:00
parent d910e6261a
commit 20b4661ff3
10 changed files with 328 additions and 120 deletions
+60 -13
View File
@@ -33,17 +33,33 @@ fn check(name: []const u8, ok: bool) void {
}
}
/// 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_info: *const BootInfo) void {
if (eql(case, "smoke")) {
smoke(boot_info);
} else if (eql(case, "timer")) {
timer();
} else if (eql(case, "vmm")) {
vmm();
} 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-nx")) {
faultNoExecute();
} else if (eql(case, "fault-null")) {
faultNull();
} else {
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case});
}
@@ -85,12 +101,7 @@ fn smoke(boot_info: *const BootInfo) void {
const cr3 = arch.readCr3();
check("paging active (CR3 set)", cr3 != 0 and cr3 % danos.page_size == 0);
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
if (failed == 0) "PASS" else "FAIL",
passed,
failed,
});
log("DANOS-TEST-DONE\n", .{});
result();
}
/// Verify device interrupts fire and return: the timer tick counter must advance
@@ -105,12 +116,26 @@ fn timer() void {
while (arch.ticks() == start and spins < 5_000_000_000) spins +%= 1;
check("timer interrupts advance the tick count", arch.ticks() > start);
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
if (failed == 0) "PASS" else "FAIL",
passed,
failed,
});
log("DANOS-TEST-DONE\n", .{});
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) |phys| {
var virt: u64 = 0x0000_4000_0000_0000; // canonical, well clear of everything mapped
arch.mapPage(virt, phys, true);
const p: *volatile u64 = @ptrFromInt(virt);
p.* = 0xdead_c0de_cafe_babe;
check("mapped page is writable and reads back", p.* == 0xdead_c0de_cafe_babe);
arch.unmapPage(virt);
pmm.free(phys);
virt += 0;
}
result();
}
fn faultInvalidOpcode() void {
@@ -118,11 +143,33 @@ fn faultInvalidOpcode() void {
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 addr: u64 = 0;
addr = asm ("" : [ret] "=r" (-> u64) : [in] "0" (addr));
const p: *allowzero volatile u64 = @ptrFromInt(addr);
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 addr: u64 = 0xdeadbeef000; // above our identity-mapped 4 GiB
var addr: u64 = 0xdeadbeef000; // well above all mapped RAM
const p: *volatile u64 = @ptrFromInt(addr);
p.* = 1;
addr += 0;