The security-track design set. communication.md is the model: four layers (namespace / protocol / channel / transport), packets and signals, parties addressed by the channel and objects by target, transports as replaceable buffer+doorbell mechanisms. protocol-namespace.md is L3+L2: /protocol names contracts, resolution establishes a channel, init is the registrar, restriction is per-process namespace delegation (with the microphone-prompt worked example), the envelope is the universal packet header, migration P1-P5 retires ServiceId. file-system-hierarchy.md replaces the unix-FHS spec with the danos-native tree (/applications, /protocol, /system, /volumes) and its migration table. ipc.md is re-cut as the kernel-ipc transport document. smep-smap.md designs the kernel hardening: copy discipline for the eight raw user-pointer syscalls, then SMEP, then SMAP as a permanent tripwire.
6.8 KiB
SMEP and SMAP — supervisor-mode hardening
Design, 2026-07-31. Not yet implemented. Companion to 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.ACis set.stac/clacopen 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.
copyAcrossandcopyFromUser(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_0700clears IF, TF, DF, AC on everysyscall(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 aclacbeside itsswapgs. -
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. A handful of 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 audit list, from the 2026-07-31 survey of
system/kernel/process.zig:Syscall Raw access system_spawnname + argument blob ( :972,:980)fs_resolvepath in, result out ( :1780,:1797)fs_mount/fs_unmountprefix + rewrite strings ( :1864)fs_noderead buffer out ( :1820)debug_writemessage bytes ( :1700)klog_readlog bytes out ( :1740)process_enumeratedescriptor array out ( :1132)device_enumeratedescriptor array out ( :388)(Some paths already do it right — the futex word and the device-register descriptor go through
copyFromUser(:1087,:924). The write direction has no helper yet.) -
One known gap inside the copy layer itself: the walk checks presence, not the leaf U/S and writable bits (
ipc-synchronous.zig:20-22flags 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 eight 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-194documents it): a non-canonical return RIP makessysretq#GP in ring 0 on Intel. Same hardening bucket, independent fix (validate RCX beforesysretq, fall back toiretq), 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.