# SMEP and SMAP — supervisor-mode hardening *Design, 2026-07-31. Not yet implemented. Companion to [protocol-namespace.md](protocol-namespace.md) on the security track — this is the hardware half; that is the namespace half.* Two CR4 bits that make the CPU refuse the two things a kernel should never do with user memory: - **SMEP** (Supervisor Mode Execution Prevention, CR4 bit 20): instruction fetch in ring 0 from a page whose U/S bit says *user* → #PF. Kills the classic ret2usr exploit shape — a kernel bug that redirects control flow can no longer land in attacker-prepared user code. - **SMAP** (Supervisor Mode Access Prevention, CR4 bit 21): data read/write in ring 0 to a user page → #PF, unless `EFLAGS.AC` is set. `stac`/`clac` open and close deliberate access windows; danos's design needs no windows at all (below). Detection is CPUID leaf 7, subleaf 0, EBX bit 7 (SMEP) and bit 20 (SMAP). Both bits are per-core state: the BSP and every AP must set them. ## Why, in danos terms Every syscall argument is an attacker-controlled integer, and several take pointers. A kernel bug that dereferences a crafted pointer reads, writes, or executes memory of the attacker's choosing — the exact bug class the isolation tracks exist to prevent. SMEP/SMAP turn that class from "silent compromise" into "immediate, attributable #PF with a kernel RIP in the log." The second benefit matters as much as the first: **SMAP is a permanent tripwire.** Once it is on, any *future* syscall that touches user memory directly — instead of going through the checked copy layer — faults the first time the QEMU suite runs it. The discipline stops depending on review. ## Where danos already stands The design is closer than it looks, because the IPC layer was built right: - **The copy layer is already SMAP-proof.** `copyAcross` and `copyFromUser` (`system/kernel/ipc-synchronous.zig:305,333`) never dereference a user virtual address: they walk the page tables and move bytes through the physmap — kernel mappings throughout. SMAP cannot object. - **Syscall entry already clears AC.** `SFMASK = 0x4_0700` clears IF, TF, DF, **AC** on every `syscall` (`system/kernel/architecture/x86_64/per-cpu.zig:76`). The syscall path is SMAP-clean from day one. - **The interrupt path is not.** Hardware does *not* clear AC on IDT delivery, and ring 3 can set AC with `popfq` — so a hostile process could take an interrupt with AC=1 and have the handler run with SMAP suspended. `isr_common` (`system/kernel/architecture/x86_64/isr.s:366`) needs a `clac` beside its `swapgs`. - **CR4 today:** the BSP inherits firmware CR4 (no kernel write anywhere); APs set PAE/OSFXSR/OSXMMEXCPT in `trampoline.s:62-68`. Neither path sets SMEP/SMAP yet, and both must. - **The stragglers.** Nine syscalls still dereference user pointers raw after a bounds check — every one is a SMAP #PF waiting to happen, and every one is *already* a latent kernel fault today (an unmapped-but-in- range user page oopses the kernel instead of failing the call). The verified sweep of `system/kernel/process.zig` (2026-07-31; a whole-kernel `@ptrFromInt` audit found no user-address dereference outside this file): | Syscall | Raw access | Direction | |---|---|---| | `system_spawn` | name + argument blob (`:972`, `:980`) | read | | `fs_resolve` | path in (`:1780`), result out (`:1797`) | read + write | | `fs_mount` | prefix + rewrite strings (`:1864`, `:1865`) | read | | `fs_unmount` | prefix string (`:1883`) | read | | `fs_node` | read buffer out (`:1820`) | write | | `debug_write` | message bytes (`:1700`; read twice — memcpy `:1710` and `log.append` `:1717`) | read | | `klog_read` | log bytes out (`:1741`) | write | | `klog_status` | status struct out (`:1758`) | write | | `process_enumerate` | descriptor array out (`:1132`) | write | | `device_enumerate` | descriptor array out (`:388`) | write | For the write-direction rows the `@ptrFromInt` is in process.zig but the stores happen in callees (`scheduler.enumerate` `system/kernel/scheduler.zig:1209`, `devices_broker.enumerate` `devices-broker.zig:136`, `log.readAt` `log.zig:209`, the vfs node calls `vfs.zig:257/269/289`) — converting them means bounce buffers plus `copyToUser` around those calls, not just editing the process.zig lines. (Some paths already do it right — the futex word and the device-register descriptor go through `copyFromUser` (`:1087`, `:924`). The write direction has no public helper yet, but the mechanism exists: `copyAcross` with a kernel source is exactly how IPC replies reach user buffers, so `copyToUser` is a mechanical mirror.) - **One known gap inside the copy layer itself:** the walk checks presence, not the leaf U/S and writable bits (`ipc-synchronous.zig:20-22` flags this). Today that is nearly moot — the user half contains only mappings the kernel itself created for that process — but it must close before shared or copy-on-write mappings exist, and closing it is part of making the copy layer the single trusted door. ## The plan **H1 — copy discipline (the real work).** A `user-memory` kernel module: `copyFromUser` / `copyToUser` (the missing write direction) via the physmap walk, with U/S and writable leaf checks closing the in-tree TODO. Convert the nine stragglers. This fixes the latent unmapped-page kernel fault on its own — it is worth doing even if SMEP/SMAP never shipped. QEMU suite green; no behavior change visible to correct programs. **H2 — SMEP.** A leaf-7 feature probe (the kernel has per-leaf `cpuid` helpers in `apic.zig` to generalize); set CR4.SMEP during per-CPU bring-up on BSP and APs — prefer the Zig-side per-CPU init over the trampoline assembly, so one code path covers every core and the trampoline stays minimal. Audit first that ring 0 never executes user-mapped pages: kernel text lives in the kernel half, `jump_to_user` is kernel code, and the AP trampoline page is kernel-mapped — expected clean, verify before flipping. **H3 — SMAP.** Add `clac` at `isr_common` entry. `clac` is #UD on CPUs without SMAP, so the instruction is a 3-byte NOP in the image, patched to `clac` at boot when CPUID advertises SMAP (one-time patch beats a conditional branch in the hottest path in the kernel). Then set CR4.SMAP in the same per-CPU init. From this point the whole QEMU suite doubles as the enforcement test: any missed raw dereference is a vector-14 with a kernel RIP and a user CR2 — loud and attributable. **H4 — keep it honest.** A line in the coding standards: kernel code touches user memory only through `user-memory`; there is no `stac` anywhere in the tree, and a PR that adds one is wrong by definition. SMAP enforces the rule mechanically at test time. Feature-gating follows the timekeeping rule (work on any VM, real Intel, real AMD): both bits are probed, absence is logged and tolerated — like the IOMMU's fail-open, the machine still boots, just unhardened. QEMU: TCG implements both; KVM inherits the host (Intel Ivy Bridge+ for SMEP, Broadwell+ for SMAP; AMD Zen+ for both). The test images should run with `-cpu max` so the suite always exercises the enabled paths. ## Adjacent, deliberately separate - **SYSRET canonical-RIP hardening** (`isr.s:192-194` documents it): a non-canonical return RIP makes `sysretq` #GP *in ring 0* on Intel. Same hardening bucket, independent fix (validate RCX before `sysretq`, fall back to `iretq`), should ride the same branch as H2/H3 but is not SMEP/SMAP. - **KPTI / Meltdown-class leaks are out of scope.** SMEP/SMAP police architectural accesses, not speculative ones. danos runs one kernel mapping in every address space and accepts that on affected hardware; revisit only if the threat model ever includes hostile native code on shared machines.