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:
@@ -257,6 +257,16 @@ pub fn translate(root: u64, virtual: u64) ?u64 {
|
||||
return paging.translateIn(root, virtual);
|
||||
}
|
||||
|
||||
/// `translate` for an address the kernel is about to touch *on a process's
|
||||
/// behalf*: the walk additionally demands the permission ring 3 would need — the
|
||||
/// leaf user-accessible (U/S set at every level), and writable (R/W at every
|
||||
/// level) when `for_write`. Null means "the process itself could not do this",
|
||||
/// which the checked copy layer (system/kernel/user-memory.zig) turns into
|
||||
/// -EFAULT instead of a kernel dereference.
|
||||
pub fn translateUser(root: u64, virtual: u64, for_write: bool) ?u64 {
|
||||
return paging.translateUserIn(root, virtual, for_write);
|
||||
}
|
||||
|
||||
/// Map a page accessible from ring 3 (U/S bit at every level). The caller keeps
|
||||
/// W^X: code read-only + executable, data writable + no-execute.
|
||||
pub fn mapUserPage(virtual: u64, physical: u64, writable: bool, executable: bool) void {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user