kernel: user memory is reached only through a checked copy
A new user-memory module owns every kernel touch of a user buffer: copyFromUser, the new copyToUser, and the resolve behind both. The walk accumulates the U/S and writable bits down all four levels with the MMU's own AND rule — folding a 2 MiB leaf in before it resolves and refusing a 1 GiB leaf outright — so a copy honours what ring 3 itself would be allowed, closing the presence-only trust model the IPC layer carried since bring-up. It then confirms the frame is physmap-backed, because that is how the copy reaches it: an mmio_map'd BAR passes the permission walk and would otherwise fault ring 0 on an alias the physmap never mapped, on the IPC path as much as the new one. The nine stragglers that dereferenced user pointers raw now route through it, so a bad pointer returns -EFAULT where it used to fault the kernel. The write direction restructures its callees around kernel bounce buffers: scheduler and devices-broker enumerate from a slot cursor (a task exiting between chunks can neither duplicate nor lose an entry), klog_read drains the ring in chunks, and fs_node stages headers and names contiguously. fs_resolve copies out before installing the endpoint handle, so a faulting copy cannot strand a capability; its out-capacity bound no longer adds an unbounded ring-3 length to the base, which wrapped and trapped the kernel's own overflow check. debug_write reads the caller's message once. Suite 107/107 (new user-memory case: seven bad pointers refused, each paired with a sound call that must still succeed).
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
//! 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: <check> 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user