118 lines
6.2 KiB
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));
|
|
}
|