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:
@@ -29,6 +29,7 @@ const process = @import("process.zig");
|
||||
const initial_ramdisk = @import("initial-ramdisk");
|
||||
const kernel_log = @import("log.zig");
|
||||
const kernel_vfs = @import("vfs.zig");
|
||||
const user_memory = @import("user-memory.zig");
|
||||
|
||||
/// Formatted test-marker write. Goes through the kernel log (not straight to
|
||||
/// serial): the log lock is what keeps marker lines from interleaving with
|
||||
@@ -141,6 +142,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
faultNull();
|
||||
} else if (eql(case, "usermem")) {
|
||||
userMemTest();
|
||||
} else if (eql(case, "user-memory")) {
|
||||
userMemoryTest(boot_information);
|
||||
} else if (eql(case, "user-pf")) {
|
||||
userPfTest();
|
||||
} else if (eql(case, "fault-recovery")) {
|
||||
@@ -1023,6 +1026,103 @@ fn userMemTest() void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// The checked copy layer (system/kernel/user-memory.zig), against a scratch
|
||||
/// address space built here rather than a live process — so the refusals can be
|
||||
/// provoked exactly: a kernel-half address, an unmapped user page, and a user
|
||||
/// page mapped read-only. Then the fixture proves the same refusals reach ring 3
|
||||
/// as -errno instead of a kernel fault.
|
||||
fn userMemoryTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: user-memory\n", .{});
|
||||
const base_free = pmm.stats().free_frames;
|
||||
|
||||
const address_space = architecture.createAddressSpace() orelse {
|
||||
check("created a scratch address space", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
check("created a scratch address space", address_space != 0);
|
||||
|
||||
// Three consecutive pages: two writable, the third read-only — so a copy that
|
||||
// straddles into the third proves the write check applies per page, not just
|
||||
// to the first one the walk touches.
|
||||
const writable_pages = 2;
|
||||
const total_pages = 3;
|
||||
const arena = process.heap_arena_base;
|
||||
var frames: [total_pages]u64 = undefined;
|
||||
var mapped: usize = 0;
|
||||
while (mapped < total_pages) : (mapped += 1) {
|
||||
frames[mapped] = pmm.alloc() orelse break;
|
||||
architecture.mapUserPageInto(address_space, arena + mapped * abi.page_size, frames[mapped], mapped < writable_pages, false);
|
||||
}
|
||||
check("mapped two writable and one read-only user page", mapped == total_pages);
|
||||
if (mapped == total_pages) {
|
||||
const read_only = arena + writable_pages * abi.page_size;
|
||||
const unmapped = arena + total_pages * abi.page_size;
|
||||
const kernel_half: u64 = 0xFFFF_8000_0000_0000;
|
||||
|
||||
var out: [16]u8 = undefined;
|
||||
const pattern = [_]u8{ 0xC0, 0xDE, 0xF0, 0x0D, 0xBA, 0xAD, 0xF0, 0x0D };
|
||||
|
||||
// A round trip through the writable page: what copyToUser placed is what
|
||||
// copyFromUser brings back, and the frame really holds it.
|
||||
const wrote = user_memory.copyToUser(address_space, arena + 32, &pattern);
|
||||
const read_back = user_memory.copyFromUser(address_space, arena + 32, out[0..pattern.len]);
|
||||
const frame_view: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frames[0] + 32));
|
||||
check("copyToUser/copyFromUser round trip", wrote and read_back and
|
||||
eql(out[0..pattern.len], &pattern) and eql(frame_view[0..pattern.len], &pattern));
|
||||
|
||||
// Straddling the 4 KiB boundary between the two writable pages.
|
||||
const straddle = arena + abi.page_size - 4;
|
||||
check("a page-straddling round trip", user_memory.copyToUser(address_space, straddle, &pattern) and
|
||||
user_memory.copyFromUser(address_space, straddle, out[0..pattern.len]) and
|
||||
eql(out[0..pattern.len], &pattern));
|
||||
|
||||
// Kernel-half addresses are refused by the range check, before any walk.
|
||||
check("copyToUser refuses a kernel-half address", !user_memory.copyToUser(address_space, kernel_half, &pattern));
|
||||
check("copyFromUser refuses a kernel-half address", !user_memory.copyFromUser(address_space, kernel_half, out[0..pattern.len]));
|
||||
check("a range running off the end of the user half is refused", !user_memory.copyToUser(address_space, user_memory.user_half_end - 4, &pattern));
|
||||
|
||||
// An unmapped-but-in-range page: the latent kernel fault H1 exists to kill.
|
||||
check("copyToUser refuses an unmapped user page", !user_memory.copyToUser(address_space, unmapped, &pattern));
|
||||
check("copyFromUser refuses an unmapped user page", !user_memory.copyFromUser(address_space, unmapped, out[0..pattern.len]));
|
||||
|
||||
// The leaf permission bits: a read-only user page may be read, never written.
|
||||
check("copyToUser refuses a read-only user mapping", !user_memory.copyToUser(address_space, read_only, &pattern));
|
||||
check("copyFromUser accepts a read-only user mapping", user_memory.copyFromUser(address_space, read_only, out[0..pattern.len]));
|
||||
check("a write straddling into a read-only page is refused", !user_memory.copyToUser(address_space, read_only - 4, &pattern));
|
||||
|
||||
// The kernel's own address space is not a user address space.
|
||||
check("copyToUser refuses address space 0", !user_memory.copyToUser(0, arena, &pattern));
|
||||
check("copyFromUser refuses address space 0", !user_memory.copyFromUser(0, arena, out[0..pattern.len]));
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < mapped) : (i += 1) {
|
||||
const va = arena + i * abi.page_size;
|
||||
architecture.unmapUserPageInto(address_space, va);
|
||||
pmm.free(frames[i]);
|
||||
}
|
||||
architecture.destroyAddressSpace(address_space);
|
||||
check("no frames leaked (free count restored)", pmm.stats().free_frames == base_free);
|
||||
|
||||
// Now the ring-3 half: the fixture aims bad pointers at the converted system
|
||||
// calls and must get failures back with the machine still running.
|
||||
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);
|
||||
check("user-memory-test spawned", spawnNamed(rd, "user-memory-test"));
|
||||
result();
|
||||
}
|
||||
|
||||
// --- synchronous IPC --------------------------------------------------------
|
||||
|
||||
var ipc_endpoint: *ipcsync.Endpoint = undefined;
|
||||
|
||||
Reference in New Issue
Block a user