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,117 @@
|
||||
//! The single trusted door between ring 0 and a process's memory.
|
||||
//!
|
||||
//! Kernel code never dereferences a user virtual address. It walks that address
|
||||
//! space's page tables through the physmap — kernel mappings throughout — and
|
||||
//! moves the bytes there. Three properties fall out of that one decision:
|
||||
//!
|
||||
//! - **A bad pointer fails the system call.** danos has no fault-recovering
|
||||
//! copy-in, so a raw dereference of an unmapped-but-in-range user page would
|
||||
//! halt the machine. Here it is a `false` return and an `-EFAULT`.
|
||||
//! - **The copy is a single fetch.** A struct pulled in once cannot be changed
|
||||
//! underneath the checks that follow it — no TOCTOU against a hostile pointer.
|
||||
//! - **It is SMAP-proof by construction.** No ring-0 access to a user-mapped
|
||||
//! page ever happens, so the CR4.SMAP bit needs no `stac` window anywhere
|
||||
//! (docs/os-development/smep-smap.md). There is no `stac` in this tree, and a
|
||||
//! change that adds one is wrong by definition.
|
||||
//!
|
||||
//! The walk enforces the permissions ring 3 itself would face: a read needs the
|
||||
//! leaf user-accessible (U/S at every level), a write needs it writable too
|
||||
//! (R/W at every level). So a syscall argument cannot steer the kernel at a
|
||||
//! kernel-only mapping, nor make it write a process's own read-only text — the
|
||||
//! properties that matter once shared or copy-on-write mappings exist, and the
|
||||
//! reason the "presence only" caveat that used to sit at the top of
|
||||
//! ipc-synchronous.zig is gone.
|
||||
//!
|
||||
//! Scope: this module knows only about *user* address spaces. The kernel side of
|
||||
//! a copy (an IPC reply staged in kernel memory, a bounce buffer) is trusted and
|
||||
//! translated without permission checks — see `resolve`.
|
||||
|
||||
const std = @import("std");
|
||||
const boot_handoff = @import("boot-handoff");
|
||||
const abi = @import("abi");
|
||||
const architecture = @import("architecture");
|
||||
|
||||
const page_size = abi.page_size;
|
||||
|
||||
/// End of the user (low) canonical half. Every user buffer must lie below it, so
|
||||
/// kernel addresses and non-canonical values are refused by the range check
|
||||
/// alone, before any table is read.
|
||||
pub const user_half_end: u64 = 0x0000_8000_0000_0000;
|
||||
|
||||
/// Whether `[virtual, virtual + len)` lies wholly inside the user half. The
|
||||
/// length is compared against the remaining span rather than added to the base,
|
||||
/// so a huge `len` cannot wrap the check.
|
||||
pub fn userRangeOk(virtual: u64, len: usize) bool {
|
||||
if (virtual >= user_half_end) return false;
|
||||
return len <= user_half_end - virtual;
|
||||
}
|
||||
|
||||
/// Resolve one address for a copy. `address_space == 0` means the kernel's own
|
||||
/// tables — trusted, translated as-is. A real address space is a process's, and
|
||||
/// the walk demands what ring 3 would need: user-accessible, plus writable when
|
||||
/// this side of the copy is the destination.
|
||||
///
|
||||
/// A user frame must also be reachable *through the physmap*, because that is how
|
||||
/// the copy loops touch it. The physmap covers RAM only: `paging.init` skips every
|
||||
/// `.mmio` region, while `mmio_map` hands a driver its device's BAR as an ordinary
|
||||
/// user-accessible mapping. Such a page satisfies the permission walk and would
|
||||
/// then fault ring 0 on the physmap alias — the very #PF this layer exists to make
|
||||
/// impossible — so coverage is confirmed before the address is returned, and an
|
||||
/// uncovered frame is refused like any other bad buffer. (Confirming coverage,
|
||||
/// rather than testing the `device_grant` bit, is what keeps physmap-backed RAM
|
||||
/// that merely carries that bit — a scanout surface — usable as a buffer.)
|
||||
pub fn resolve(address_space: u64, virtual: u64, for_write: bool) ?u64 {
|
||||
if (address_space == 0) return architecture.translate(architecture.kernelPageTable(), virtual);
|
||||
const physical = architecture.translateUser(address_space, virtual, for_write) orelse return null;
|
||||
if (architecture.translate(architecture.kernelPageTable(), boot_handoff.physicalToVirtual(physical)) == null) return null;
|
||||
return physical;
|
||||
}
|
||||
|
||||
/// Copy `destination.len` bytes from `user_va` in address space `user_as` into the
|
||||
/// kernel buffer `destination`. False — never a #PF — if the range escapes the
|
||||
/// user half, or any source page is unmapped or not readable from ring 3.
|
||||
/// Handles page-straddling buffers.
|
||||
pub fn copyFromUser(user_as: u64, user_va: u64, destination: []u8) bool {
|
||||
if (user_as == 0) return false; // not a user address space
|
||||
if (!userRangeOk(user_va, destination.len)) return false;
|
||||
var off: usize = 0;
|
||||
while (off < destination.len) {
|
||||
const physical = resolve(user_as, user_va + off, false) orelse return false;
|
||||
const left = page_size - ((user_va + off) & (page_size - 1));
|
||||
const n = @min(left, destination.len - off);
|
||||
const source: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(physical));
|
||||
@memcpy(destination[off..][0..n], source[0..n]);
|
||||
off += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The write direction: copy the kernel buffer `source` out to `user_va` in
|
||||
/// address space `user_as`. False — never a #PF, never a partial promise — if the
|
||||
/// range escapes the user half, or any destination page is unmapped, kernel-only,
|
||||
/// or read-only for ring 3. (A refusal mid-way may already have written earlier
|
||||
/// pages; the caller fails the whole system call, so the buffer's contents are
|
||||
/// meaningless either way.)
|
||||
///
|
||||
/// The mirror of `copyFromUser`, and the only way kernel data reaches a user
|
||||
/// buffer outside the IPC path's `copyAcross`.
|
||||
pub fn copyToUser(user_as: u64, user_va: u64, source: []const u8) bool {
|
||||
if (user_as == 0) return false; // not a user address space
|
||||
if (!userRangeOk(user_va, source.len)) return false;
|
||||
var off: usize = 0;
|
||||
while (off < source.len) {
|
||||
const physical = resolve(user_as, user_va + off, true) orelse return false;
|
||||
const left = page_size - ((user_va + off) & (page_size - 1));
|
||||
const n = @min(left, source.len - off);
|
||||
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(physical));
|
||||
@memcpy(destination[0..n], source[off..][0..n]);
|
||||
off += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// `copyToUser` for any fixed-layout value — the shape most write-direction
|
||||
/// system calls want (a `KlogStatus`, a `FileAttributes`).
|
||||
pub fn copyValueToUser(user_as: u64, user_va: u64, value: anytype) bool {
|
||||
return copyToUser(user_as, user_va, std.mem.asBytes(value));
|
||||
}
|
||||
Reference in New Issue
Block a user