NVMM SVM: host IA32_SYSENTER_CS/ESP/EIP MSRs left guest-controlled after every vmrun β any process pivots to ring 0 via 2-byte `sysenter`
Summary
svm_vmrun's asm prologue executes vmload (svmfunc.S:180), which loads the guest VMCB's SYSENTER_CS/ESP/EIP into live hardware. #VMEXIT does not restore them (absent from the host save area), and svm_vcpu_guest_misc_leave restores only 6 of the 9 VMLOAD-managed MSRs β SYSENTER_* never restored anywhere in DragonFly. Attacker arms values via NVMM_IOC_VCPU_SETSTATE (writes them verbatim into the VMCB) and one VCPU_RUN installs them; corruption persists indefinitely; ANY process executing sysenter transitions to ring 0 at attacker-chosen RIP/ESP. Hardware contract proven from four independent implementations (QEMU TCG APM model, Linux 2.6.32 KVM host_save_user_msrs, modern KVM host-VMCB, Xen host_vmcb). VMX backend immune (VMCS_HOST_IA32_SYSENTER_*=0). NetBSD upstream nvmm has the identical omission. Live-verified on guest as unprivileged testnv (full ioctl chain, baseline/post-run sysenter differential β VMX immunity confirmed); AMD VMLOAD step hardware-gated (no SVM in guest).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3978 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 8.7 KB | β raw | |
| exploit.c | β | 16.1 KB | view raw | |
| df3978_rig.c | β | 3.0 KB | view raw | |
| Makefile.rig | β | 58 B | β download | |
| test_relax.diff | β | 896 B | view raw | |
| build.sh | β | 177 B | view raw | |
| run.sh | β | 296 B | view raw | |
| build.log | β | 12.1 KB | view raw | |
| fix_build.log | β | 1013 B | view raw | |
| run.log | β | 4.0 KB | view raw | |
| fix_run.log | β | 2.5 KB | view raw | |
| intlog-excerpt.txt | β | 3.1 KB | view raw | |
| env.txt | β | 2.1 KB | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| sysenter_hijack.c | β | 7.3 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 9.7 KB | view raw |
DF-3978 β NVMM SVM host SYSENTER MSR hijack β uid=0 REPRODUCED
DragonFly nvmm SVM backend leaves the host IA32_SYSENTER_{CS,ESP,EIP}
MSRs guest-controlled after every VCPU_RUN (vmload in
nvmm_x86_svmfunc.S:180 installs them; nothing ever restores them β
svm_vcpu_guest_misc_leave covers 6 of 9 VMLOAD-managed MSRs). Any
process's 2-byte sysenter then enters ring 0 at attacker-chosen
RIP/RSP/CS. Critical; CWE-284; also present in NetBSD upstream nvmm.
Verdict: reproduced (uid0_privesc), fix validated. Full chain run as
unprivileged testnv on DragonFly 6.5 under a private QEMU 7.2 TCG
SVM instance (-cpu qemu64,+svm,+npt): MSR corruption confirmed by
in-kernel rdmsr (persists after the armer exits), compat-mode sysenter
pivot to a CPL0 payload in a user page (no SMEP/SMAP), struct ucred
patch, iretq back, uid=0 + root-owned artifact, guest survives.
With fix.diff (save/restore the three MSRs in misc_enter/leave) the
identical campaign leaves the MSRs clean and the consumer faults with the
clean-host #GP.
Reproduce
Prereqs (detailed in VERDICT.md "Emulator setup"):
- private TCG instance (the Intel audit host cannot expose SVM):
qemu-system-x86_64 -accel tcg,thread=multi -cpu qemu64,+svm,+npt \ -m 2048 -smp 1 -drive file=<overlay of harness disk>,if=virtio \ -netdev user,id=n0,hostfwd=tcp:127.0.0.1:5522-:22 ... - guest: apply
test_relax.diffto/usr/src(TCG lacks the NRIPS CPUID bit; the stock ident gate refuses the emulator CPU β init-time gate only, disclosed, applied to baseline AND fixed builds), build+loadnvmm.ko(build.log), build+load the rig (df3978_rig.c), createtestnvin groupnvmm(gid 90). cc -O2 -o /tmp/exploit exploit.c -I/usr/src/sys
Staged campaign (run.sh):
sysctl kern.df3978_msrs # cs=0 esp=0 eip=0 (clean) su -m testnv -c '/bin/sh -c "/tmp/exploit 1 10 40 a0 a4 baseline"' sysctl kern.df3978_msrs # still clean; child SIGBUS 138 su -m testnv -c '/bin/sh -c "/tmp/exploit 1 10 40 a0 a4 arm"' sysctl kern.df3978_msrs # VULNERABLE: cs=0x8 esp=0x50000800 eip=0x40000000 # consume (deferred rig resolve): su -m testnv -c '/bin/sh -c "nohup /tmp/exploit 0 10 40 a0 a4 consume > /tmp/c.log 2>&1 &"' # -> sysctl kern.df3978_pid=<waiting pid>; echo <procptr> > /tmp/df3978.pproc
Success criterion (see run.log): [C5] RETURNED FROM RING 0:
cr0=0x80050033 marker=0x3978, then *** UID 0 ***, id β
uid=0(root), /tmp/df3978_pwned owned by root:wheel, guest alive.
Fixed module (apply fix.diff, rebuild, reload, reboot): same
campaign β MSRs stay cs=0 esp=0 eip=0, child status 138, no artifact
(fix_run.log).
Caveats (honest scope)
Executed on QEMU TCG's APM-conformant SVM model, not AMD silicon (the
audit host is Intel; the stock KVM/qemu64 harness guest cannot even load
nvmm β nvmm: cpu not supported). The DragonFly kernel code under test
ran for real; every hardware-behavior claim is independently backed by
the AMD APM and QEMU/old-KVM/modern-KVM/Xen implementations. QEMU
implements the Intel-SDM #UD rule for 64-bit-mode sysenter (AMD
silicon permits it in long mode), so the PoC consumer pivots from a
32-bit compat CS β valid on both vendors; the MSR corruption itself is
mode-independent.
Files
VERDICT.md (full narrative + line refs) Β· exploit.c (full chain) Β·
df3978_rig.c/Makefile.rig (kernel rig: rdmsr dump, pfind resolver,
offsets) Β· test_relax.diff (disclosed test-only ident-gate relaxation) Β·
build.sh/run.sh Β· build.log/fix_build.log Β· run.log/
fix_run.log Β· intlog-excerpt.txt (CPL0 traces) Β· env.txt Β·
fix.diff Β· manifest.json Β· verdict.json Β· sysenter_hijack.c
(pass-1 trigger, superseded).
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 executesvmload %rax; vmrun %rax; vmsave %rax. Thevmloadloads the guest VMCB'ssysenter_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:1646callssvm_vcpu_guest_misc_enter()(1513-1521: saves only FSBASE and KERNELGSBASE) before the run loop, andsvm_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_setstatecopiesstate->msrs[NVMM_X64_MSR_SYSENTER_{CS,ESP,EIP}]verbatim intovmcb->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. sysenteris 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-3023zeroesVMCS_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)
- Baseline (rig
kern.df3978_msrs):cs=0 esp=0 eip=0β DragonFly never programs these MSRs; a compat-modesysenterconsumer child dies with SIGBUS (status 138), guest fine. - Arm β unprivileged
testnv(uid 1002, group nvmm;/dev/nvmmroot: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. - 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). - 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. - uid=0: the payload (no SMEP/SMAP on this CPU) walked
p_ucred(rig-resolvedstruct proc *, offsets offsetof-validated in-kernel) and zeroed cr_uid/cr_ruid/cr_svuid, theniretq-ed back to CPL3:uid=0(root) gid=1002(testnv)+/tmp/df3978_pwnedowned byroot:wheel, guest healthy afterwards (clean privesc, no crash). - 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, defaultqemu64) exposes neither SVM nor VMX:kldload nvmmfailsnvmm: cpu not supported(MOD_LOAD error 45) β verified live. Pass-1's-cpu hostguest 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_vmloadloadssysenter_cs/esp/eipinto the live CPU state; the #VMEXIT host-restore list does not include them. In this modelvmloadandvmrunexecuted 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 stocksvm_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-timereturn falseonly. 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.shrecipe β staged baseline/arm/consume astestnv, feeding the rig-resolvedstruct 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.
Fix verification
fixedfix.diff applied to /usr/src (patch -p1, hunks 583/1521/1540), nvmm.ko rebuilt (fix_build.log) and reloaded, machine rebooted to reset MSRs, IDENTICAL campaign re-run as testnv: after the armed VCPU_RUN the rig reads cs=0 esp=0 eip=0 (baseline build: cs=0x8 esp=0x50000800 eip=0x40000000) and the consumer child dies with the clean-host #GP (SIGBUS status 138) instead of entering ring 0; no /tmp/df3978_pwned, uid unchanged, guest healthy. Fix validated on the same TCG+SVM substrate as the reproduction (with the same disclosed test_relax.diff present in BOTH builds, so only the fix differs).
fix_run.log (full fixed-module campaign with the decisive MSR/child-status contrast vs run.log); fix_build.log; fix.diff.
Confirmed kernel references
- sys/dev/virtual/nvmm/x86/nvmm_x86_svmfunc.S:180
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:1524
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:1513
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:1646
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:1793
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:2005
- sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:2345
- sys/dev/virtual/nvmm/x86/nvmm_x86_vmx.c:3021
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:392
Detail
Exploit chain
unprivileged testnv -> open /dev/nvmm O_RDONLY (0640 root:nvmm) -> MACHINE_CREATE -> HVA_MAP/GPA_MAP 2MB -> VCPU_CREATE -> VCPU_SETSTATE with msrs[SYSENTER_CS]=0x08 msrs[SYSENTER_ESP]=0x50000800 msrs[SYSENTER_EIP]=0x40000000 (RWX user page with hand-encoded CPL0 payload) -> one VCPU_RUN (vmload in svmfunc.S:180 installs the values; any exit suffices) -> ioctl returns, armer may even exit (corruption persists; rig rdmsr proves it) -> ANY process executes sysenter in compat mode (fork child that never opened nvmm: lretq to GUCODE32_SEL, 0F 34) -> CPU enters CPL0 at CS=0x08 RIP=payload RSP=attacker stack -> payload reads CR0 (proof), loads struct proc* from payload slot (rig/pfind-resolved), reads p_ucred, zeroes cr_uid/cr_ruid/cr_svuid, writes 0x3978 marker -> builds iretq frame (SS=0x23 RSP=0x50002000 RFLAGS|IF CS=0x2b RIP=trampoline) -> iretq -> ret into child_after0 -> child exits 0x42 -> arming process observes uid=0 euid=0, writes root-owned artifact; guest keeps running; MSR state stays corrupted for further pivots until the next NVMM run.
Evidence (decisive lines)
run.log (full vulnerable campaign: baseline SIGBUS 138 / rig MSR flip to armed values persisting past armer exit / '[C5] RETURNED FROM RING 0: cr0=0x80050033 marker=0x3978' / '*** UID 0 ***' / root:wheel artifact / guest alive); fix_run.log (identical campaign on fix.diff build: MSRs stay zero, child status 138, no artifact); intlog-excerpt.txt (QEMU -d int cpl=0 IP=0008:00000000400000xx SP=0010:000000005000080x lines from debug iterations + the exception-free final run); env.txt (private TCG instance cmdline, backend x86-svm attach, /dev/nvmm perms, testnv identity, rig offsets); build.log/fix_build.log (module builds); test_relax.diff (the disclosed relaxation).
PoC changes
Pass-1 trigger sysenter_hijack.c extended into a staged full-chain exploit: (1) added ring-0 payload (hand-encoded x86-64: cr0 proof, ucred patch via rig-resolved struct proc*, iretq return); (2) sysenter consumer moved to a sacrificial forked child pivoting from a 32-bit compat CS because QEMU's translator implements the Intel SDM #UD rule for 64-bit sysenter (AMD silicon allows long-mode sysenter; compat form valid on both); (3) cow_pre_touch() forces the child's private COW copies of payload/stack pages (kernel-mode writes cannot resolve COW faults); (4) null-guard in payload so a bad pointer skips the patch instead of faulting at CPL0; (5) new KLD rig (df3978_rig.c) for rdmsr evidence, pfind() proc resolution and offsetof validation; (6) test-only NRIPS ident-gate relaxation (test_relax.diff) so the stock driver accepts the TCG SVM CPU -- init-time gate only, applied identically to baseline and fixed builds; (7) during bring-up, three crash iterations were root-caused via QEMU -d int logs (COW write at CPL0; x86 encoding bugs in the byte emitter) before the clean uid0 run.
Verified recommended fix
Save host IA32_SYSENTER_{CS,ESP,EIP} in svm_vcpu_guest_misc_enter() and restore them in svm_vcpu_guest_misc_leave() alongside FSBASE/KERNELGSBASE (fix.diff, validated A/B on the guest).
Verdict
REPRODUCED end-to-end and FIX VALIDATED. Unprivileged user testnv (uid 1002, group nvmm, /dev/nvmm 0640 opened O_RDONLY) arms the VMCB SYSENTER fields via NVMM_IOC_VCPU_SETSTATE (nvmm_x86_svm.c:2005-2010 writes them verbatim) and issues one NVMM_IOC_VCPU_RUN; svm_vmrun's asm prologue executes vmload (nvmm_x86_svmfunc.S:180) which installs those values into the live IA32_SYSENTER_CS/ESP/EIP MSRs; #VMEXIT does not restore them (VMLOAD-managed, absent from the host save area) and svm_vcpu_guest_misc_leave (nvmm_x86_svm.c:1524-1537, called at :1793) restores only STAR/LSTAR/CSTAR/SFMASK/FSBASE/KERNELGSBASE -- verified live by an in-kernel rdmsr rig: MSRs flip from cs=0 esp=0 eip=0 to cs=0x8 esp=0x50000800 eip=0x40000000 and PERSIST after the arming process exits. A fresh forked process that never touched /dev/nvmm then executes sysenter (compat-mode CS; 2-byte user instruction, no enable bit) and enters ring 0 at the armed CS/RIP/RSP -- QEMU -d int traces from the debug iterations show cpl=0 IP=0008:
No comments yet.