# DF-3978 — NVMM SVM: host IA32_SYSENTER_CS/ESP/EIP left guest-controlled after every vmrun — ring-0 RIP hijack via `sysenter` → **uid=0 (verified)**

**File**: `sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c` (+ `nvmm_x86_svmfunc.S`)
**Severity**: Critical — unprivileged local user → ring-0 code execution → uid=0
**Verdict**: **reproduced** — full chain (arm → MSR corruption confirmed by
in-kernel `rdmsr` → `sysenter` ring-0 pivot → `struct ucred` patch → **uid=0**,
root-owned artifact, guest survives) executed end-to-end on DragonFly
6.5-DEVELOPMENT running under QEMU 7.2 TCG with an APM-conformant SVM CPU
model. Fix validated on the same setup (`fix.diff`: corruption gone, pivot
gone). See "Emulator fidelity" for the exact scope of what is and is not
silicon.

## Root cause (source, path:line)

On AMD SVM the SYSENTER linkage MSRs are switched **only** by
VMLOAD/VMSAVE — they are not part of the VMRUN host-save area, so #VMEXIT
does not restore them:

* `sys/dev/virtual/nvmm/x86/nvmm_x86_svmfunc.S:180-182` — `svm_vmrun`'s asm
  prologue executes `vmload %rax; vmrun %rax; vmsave %rax`. The `vmload`
  loads the *guest VMCB's* `sysenter_cs/esp/eip` (plus STAR family,
  KERNELGSBASE, segments) into the live CPU. The asm's host save/restore
  covers only GPRs, TR, GSBASE and LDT.
* `sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:1646` calls
  `svm_vcpu_guest_misc_enter()` (1513-1521: saves **only** FSBASE and
  KERNELGSBASE) before the run loop, and `svm_vcpu_guest_misc_leave()`
  (1793 → 1524-1537) after it — which restores STAR/LSTAR/CSTAR/SFMASK/
  FSBASE/KERNELGSBASE, i.e. **6 of the 9** VMLOAD-managed MSRs.
  `IA32_SYSENTER_{CS,ESP,EIP}` are never restored anywhere.
* Arming path: `nvmm_x86_svm.c:2005-2010` — `svm_vcpu_setstate` copies
  `state->msrs[NVMM_X64_MSR_SYSENTER_{CS,ESP,EIP}]` **verbatim** into
  `vmcb->state.sysenter_*`. (The guest MSR bitmap even allows in-guest
  RDMSR/WRMSR of these MSRs: 2345-2347 — a guest kernel could arm them
  without SETSTATE.)
* Tree-wide grep: no other code in the DragonFly kernel ever writes
  `MSR_SYSENTER_{CS,ESP,EIP}` (only specialreg.h defines and the nvmm
  backends reference them), so the corruption persists indefinitely.
* `sysenter` is a 2-byte unprivileged instruction with **no enable bit**
  (unlike SYSCALL's EFER.SCE): any process executing it transitions to
  ring 0 with RIP=SYSENTER_EIP, RSP=SYSENTER_ESP, CS=SYSENTER_CS.
* The VMX backend is immune: `nvmm_x86_vmx.c:3021-3023` zeroes
  `VMCS_HOST_IA32_SYSENTER_*`, so every VMX exit restores a safe constant.

Hardware contract (VMLOAD loads SYSENTER MSRs; #VMEXIT does not restore
them): AMD APM vol.2 (VMRUN/VMSAVE/VMLOAD; the host state save area
covers RIP/RSP/RFLAGS/RAX, CR0/2/3/4, DR6/7, EFER, CS/SS, GDTR/IDTR only),
QEMU TCG's model, Linux KVM (old 2.6.32 `host_save_user_msrs[]` explicitly
rdmsr/wrmsr-s exactly these three MSRs around every entry; modern KVM
VMSAVEs/VMLOADs a per-CPU host VMCB), and Xen's `host_vmcb`.

## Verified chain (run.log — every step live on the guest)

1. **Baseline** (rig `kern.df3978_msrs`): `cs=0 esp=0 eip=0` — DragonFly
   never programs these MSRs; a compat-mode `sysenter` consumer child dies
   with SIGBUS (`status 138`), guest fine.
2. **Arm** — unprivileged `testnv` (uid 1002, group nvmm; `/dev/nvmm`
   root:nvmm 0640, opened O_RDONLY): MACHINE_CREATE → HVA/GPA_MAP →
   VCPU_CREATE → **VCPU_SETSTATE with msrs[SYSENTER_CS]=0x08
   (GSEL(GCODE_SEL=1,SEL_KPL)), ESP=0x50000800, EIP=0x40000000 (RWX
   user page)** → one VCPU_RUN (rc=0) → process exits.
3. **Corruption confirmed by the kernel itself**: rig rdmsr now reads
   `cs=0x8 esp=0x50000800 eip=0x40000000` — the attacker's values, on the
   CPU, *after the arming process is gone* (persistence property).
4. **Consume** — a fresh forked process that **never opened /dev/nvmm**
   far-returns into a 32-bit compat code segment (GUCODE32_SEL → 0x1b) and
   executes `sysenter` (0F 34):
   `[C5] RETURNED FROM RING 0: cr0=0x80050033 marker=0x3978` — the CPL0
   payload read CR0 (impossible at CPL3; value matches QEMU's own dumps)
   and wrote the marker from ring 0.
5. **uid=0**: the payload (no SMEP/SMAP on this CPU) walked
   `p_ucred` (rig-resolved `struct proc *`, offsets
   offsetof-validated in-kernel) and zeroed cr_uid/cr_ruid/cr_svuid, then
   `iretq`-ed back to CPL3:
   `uid=0(root) gid=1002(testnv)` + `/tmp/df3978_pwned` owned by
   `root:wheel`, **guest healthy afterwards** (clean privesc, no crash).
6. Positive CPL0-entry traces from the debug iterations are preserved in
   `intlog-excerpt.txt` (`v=0e ... cpl=0 IP=0008:0000000040000004
   SP=0010:0000000050000800`); the successful final pivot is
   exception-free, so its proof is the CR0/marker/uid0 triple in run.log.

## Fix validation (fix_run.log)

`fix.diff` (this pack; authored pass-1, line-verified now): saves
SYSENTER_CS/ESP/EIP in `svm_vcpu_guest_misc_enter()` and restores them in
`svm_vcpu_guest_misc_leave()` alongside FSBASE/KERNELGSBASE. Applied
(`patch -p1`, 3 hunks: 583/1521/1540), module rebuilt, reloaded, machine
rebooted (clean MSRs), **identical campaign re-run**:

* after arm: `kern.df3978_msrs: cs=0 esp=0 eip=0` (was the armed values)
* consume child: `status 138` — the clean-host #GP (was: ring-0 return)
* no artifact, no uid change, guest healthy.

→ `fix_status = fixed` (baseline_reproduced=1, patched_reproduced=0).

## Emulator setup and fidelity (READ THIS)

* The audit host is Intel (no SVM). The stock harness guest
  (`-enable-kvm`, default `qemu64`) exposes neither SVM nor VMX:
  `kldload nvmm` fails `nvmm: cpu not supported` (MOD_LOAD error 45) —
  verified live. Pass-1's `-cpu host` guest had VMX only.
* Verification therefore used a **private QEMU 7.2.22 TCG instance**
  (`-accel tcg,thread=multi -cpu qemu64,+svm,+npt -m 2048 -smp 1`) on a
  **throwaway qcow2 overlay** of the harness disk (backing opened
  read-only; overlay discarded afterwards — harness disk untouched).
  TCG implements the SVM instruction set (VMRUN/VMLOAD/VMSAVE helpers)
  per the AMD APM: `helper_vmload` loads `sysenter_cs/esp/eip` into the
  live CPU state; the #VMEXIT host-restore list does not include them.
  In this model `vmload` and `vmrun` executed **for real inside the
  DragonFly guest kernel** — only the silicon is emulated, and every
  hardware-behavior assumption is separately backed by the APM text and
  four independent implementations (QEMU, old KVM, modern KVM, Xen).
* **Disclosed test-only driver relaxation** (`test_relax.diff`):
  QEMU 7.2 TCG does not expose CPUID.8000000A:EDX bit 3 (nrip-save), so
  the stock `svm_ident()` (nvmm_x86_svm.c:2612-2616) rejects the emulator
  CPU ("nvmm: SVM-NRIPS not supported") before the vulnerable path can
  run. The relaxation drops that init-time `return false` **only**. It
  was applied identically to the baseline (vulnerable) and the fixed
  module builds, so the A/B differential is unaffected; the vulnerable
  save/restore path under test is bit-identical to stock. NRIPS only
  affects exit-information quality, not MSR state handling.
* **64-bit vs compat `sysenter`**: QEMU's translator implements the Intel
  SDM rule — SYSENTER in 64-bit mode raises #UD (observed: SIGILL,
  phase A0). AMD silicon (where SVM hosts exist by definition) permits
  SYSENTER in long mode; the compat-mode (32-bit CS) form used by the
  PoC consumer is valid on both. This is a consumer-side emulator quirk,
  not a limitation of the corruption (step 3 is mode-independent).
* On real AMD-SVM hardware the same chain applies with kernel-text
  gadgets (SMEP may require gadget selection accordingly) — the MSR
  corruption and the CPL0 pivot are architectural.

## Reproduce

* build: `cc -O2 -o /tmp/exploit exploit.c -I/usr/src/sys` (guest)
* run:  `./run.sh` recipe — staged baseline/arm/consume as `testnv`,
  feeding the rig-resolved `struct proc *` via `/tmp/df3978.pproc`.
* expected (vulnerable): run.log — `[C5] ... *** RING 0 EXECUTED ***`,
  `*** UID 0 ***`, root-owned `/tmp/df3978_pwned`, guest alive.
* expected (fixed module): fix_run.log — MSRs stay clean, `status 138`.

## Artifacts

`exploit.c` (full chain), `df3978_rig.c` + `Makefile.rig` (kernel rig:
MSR dump + pfind resolver + offsets), `test_relax.diff` (disclosed
relaxation), `build.log`/`fix_build.log` (module builds),
`run.log`/`fix_run.log` (campaigns), `intlog-excerpt.txt` (CPL0 traces),
`env.txt`, `fix.diff`, `sysenter_hijack.c` (pass-1 trigger, superseded by
exploit.c).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
