# 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.
