danos/system/kernel/user-memory.zig

118 lines
6.2 KiB
Zig

//! 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));
}