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:
Daniel Samson
2026-07-31 20:57:49 +01:00
parent c4f16a5448
commit 8d4a7cf240
16 changed files with 691 additions and 105 deletions
@@ -524,6 +524,58 @@ pub fn translateIn(pml4: u64, virtual: u64) ?u64 {
return (pte & address_mask) | (virtual & (page_size - 1));
}
/// `translateIn` with the ring-3 permission bits enforced: the walk accumulates
/// the protection flags of every level it descends through and refuses the
/// translation unless the *effective* permission allows the access ring 3 would
/// be allowed — U/S set at every level, and (for `for_write`) R/W set at every
/// level too. A bit cleared anywhere on the path denies, which is exactly how
/// the MMU combines them, so a checked kernel copy sees the same permissions the
/// process itself does.
///
/// This is the walk `system/kernel/user-memory.zig` copies through, and the
/// reason a kernel copy can never be steered at a kernel-only mapping or made to
/// write a read-only user page (a process's own text, say).
///
/// Huge pages: a 2 MiB PDE leaf resolves like `translateIn`, with its own U/S and
/// R/W folded into the accumulator first. A PDPTE with PS set (a 1 GiB leaf) is
/// refused rather than descended into — danos never builds one, and denying is
/// the safe direction for a permission-checked walk.
pub fn translateUserIn(pml4: u64, virtual: u64, for_write: bool) ?u64 {
// Start all-ones and AND in each level: a cleared bit at any level denies.
var effective: u64 = ~@as(u64, 0);
const pml4e = tableAt(pml4)[(virtual >> 39) & 0x1FF];
if (pml4e & present == 0) return null;
effective &= pml4e;
const pdpte = tableAt(pml4e & address_mask)[(virtual >> 30) & 0x1FF];
if (pdpte & present == 0) return null;
if (pdpte & page_size_bit != 0) return null; // 1 GiB leaf: never built here, refuse
effective &= pdpte;
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
if (pde & present == 0) return null;
effective &= pde;
if (pde & page_size_bit != 0) { // 2 MiB huge leaf: frame base is bits 51:21
if (!permits(effective, for_write)) return null;
return (pde & address_mask & ~@as(u64, huge_page_size - 1)) | (virtual & (huge_page_size - 1));
}
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
if (pte & present == 0) return null;
effective &= pte;
if (!permits(effective, for_write)) return null;
return (pte & address_mask) | (virtual & (page_size - 1));
}
/// Whether accumulated walk flags allow a ring-3 access: user-accessible always,
/// and writable when the access is a store.
fn permits(effective: u64, for_write: bool) bool {
if (effective & user == 0) return false;
if (for_write and effective & writable == 0) return false;
return true;
}
fn invalidate(virtual: u64) void {
// invlpg needs its operand via a register-indirect memory reference that Zig
// inline asm won't form directly, so stage the address in a register first.