//! user-memory-test — QEMU fixture for the kernel's checked copy layer //! (system/kernel/user-memory.zig). //! //! Every check comes in a pair: the same system call is made once with a sound //! buffer and once with a pointer that is *inside* the user half but mapped in no //! process. The sound call must succeed and the unsound one must return a wrapped //! -errno — proving the refusal is attributable to the pointer and not to the //! call being impossible. Before H1 the unsound half of each pair dereferenced an //! unmapped page in ring 0, which halts the machine; the fixture reaching its //! final marker at all is the substance of the test. //! //! Prints `user-memory-test: ok` per pair and `user-memory-test: ok` at //! the end, which the harness asserts on (test/qemu_test.py). const abi = @import("abi"); const sc = @import("system-call"); const logging = @import("logging"); const device = @import("driver"); /// A user-half address that is mapped in no process: below the code image /// (0x0000_7000_0000_0000) and far from every arena the kernel hands out. It /// passes each system call's bounds check and then fails the page walk — the /// exact shape the checked copy exists for. const unmapped: usize = 0x0000_6000_0000_0000; /// The kernel returns failures as a small negative errno in the result register. fn failed(r: usize) bool { return r > ~@as(usize, 0) - 4095; } var failures: u32 = 0; fn check(comptime name: []const u8, ok: bool) void { if (ok) { _ = logging.write("user-memory-test: " ++ name ++ " ok\n"); } else { failures += 1; _ = logging.write("user-memory-test: FAIL " ++ name ++ "\n"); } } /// klog_read writes the ring's bytes out to the caller. Read from the ring's own /// tail so the offset is certainly valid and the copy is what decides the call. fn klogRead() void { var status: abi.KlogStatus = undefined; if (@as(isize, @bitCast(sc.systemCall1(.klog_status, @intFromPtr(&status)))) != 0) { check("klog_status (sound out buffer accepted)", false); return; } var buffer: [64]u8 = undefined; const good = sc.systemCall3(.klog_read, status.tail, @intFromPtr(&buffer), buffer.len); const bad = sc.systemCall3(.klog_read, status.tail, unmapped, buffer.len); check("klog_read (bad out buffer refused)", !failed(good) and failed(bad)); // The status struct itself is a write-direction copy of its own. const bad_status = sc.systemCall1(.klog_status, unmapped); check("klog_status (bad out buffer refused)", failed(bad_status)); } fn processEnumerate() void { var table: [4]abi.ProcessDescriptor = undefined; const good = sc.systemCall2(.process_enumerate, @intFromPtr(&table), table.len); const bad = sc.systemCall2(.process_enumerate, unmapped, table.len); check("process_enumerate (bad out buffer refused)", good != 0 and !failed(good) and failed(bad)); } /// Descriptors are hundreds of bytes each — keep them off the stack. Four spans /// more than one kernel chunk, so the chunked copy-out is genuinely exercised. var descriptors: [4]device.DeviceDescriptor = undefined; fn deviceEnumerate() void { const good = sc.systemCall2(.device_enumerate, @intFromPtr(&descriptors), descriptors.len); const bad = sc.systemCall2(.device_enumerate, unmapped, descriptors.len); check("device_enumerate (bad out buffer refused)", !failed(good) and failed(bad)); } fn fsResolve() void { var out: [256]u8 = undefined; const path = "/system/services/init"; const good = sc.systemCall5(.fs_resolve, @intFromPtr(path.ptr), path.len, 0, @intFromPtr(&out), out.len); const bad = sc.systemCall5(.fs_resolve, unmapped, path.len, 0, @intFromPtr(&out), out.len); check("fs_resolve (bad path buffer refused)", !failed(good) and failed(bad)); // The out-buffer capacity is unbounded ring-3 input: the range check must // not add it to the base, or the sum wraps the user-half bound and traps // the kernel's own overflow check. Surviving this call is the assertion. const wrapping = sc.systemCall5(.fs_resolve, @intFromPtr(path.ptr), path.len, 0, @intFromPtr(&out), ~@as(usize, 0)); check("fs_resolve (wrapping out capacity refused)", failed(wrapping)); } /// debug_write reads the caller's message; a bad pointer must not take the /// kernel down on the way to the log ring. fn debugWrite() void { const bad = sc.systemCall3(.debug_write, unmapped, 16, @intFromEnum(abi.KlogLevel.info)); check("debug_write (bad message buffer refused)", failed(bad)); } pub fn main() void { klogRead(); processEnumerate(); deviceEnumerate(); fsResolve(); debugWrite(); // Reaching here at all means seven bad user pointers failed their calls // instead of faulting ring 0. if (failures == 0) { _ = logging.write("user-memory-test: ok\n"); } else { _ = logging.write("user-memory-test: FAILED\n"); } }