β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3978

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3978 Β· 18 files
FileTypeDescriptionSize
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.diff to /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+load nvmm.ko (build.log), build+load the rig (df3978_rig.c), create testnv in group nvmm (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).

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-3978

Confirmed kernel references

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: SP=, and the final clean run proves CPL0 execution in-guest (payload read CR0=0x80050033, CPL3-unreadable, and wrote the 0x3978 marker). The ring-0 payload (user page; no SMEP/SMAP on this CPU) zeroes cr_uid/cr_ruid/cr_svuid of the arming process's struct ucred (rig/pfind-resolved) and iretq's back to CPL3: uid=0 euid=0, system("id") prints uid=0(root), /tmp/df3978_pwned owned by root:wheel, guest SURVIVES (clean privesc, no crash). Fix (fix.diff: save/restore SYSENTER_CS/ESP/EIP in misc_enter/misc_leave) validated by identical A/B campaign: after arm the MSRs stay cs=0 esp=0 eip=0, the consumer child dies with the clean-host #GP (SIGBUS, status 138), no artifact. CAVEATS, disclosed: (a) executed on a private QEMU 7.2.22 TCG instance (-accel tcg -cpu qemu64,+svm,+npt -smp 1, throwaway overlay; harness disk untouched) because the Intel audit host has no SVM -- the DragonFly kernel code under test ran for real; only the CPU is the APM-conformant TCG model, and every hardware-behavior claim is independently backed by AMD APM vol.2 plus QEMU/old-KVM/modern-KVM/Xen implementations; (b) test_relax.diff relaxes ONLY the init-time NRIPS CPUID gate (nvmm_x86_svm.c:2612-2616, 'return false') because QEMU 7.2 TCG does not expose CPUID.8000000A:EDX.nrip-save and the stock driver refuses the emulator CPU before the vulnerable path can run -- applied identically to baseline and fixed builds so the differential is unaffected, and the vulnerable save/restore path is bit-identical to stock; (c) QEMU's translator follows the Intel SDM rule that 64-bit-mode SYSENTER is #UD (observed SIGILL in phase A0) while AMD silicon allows it in long mode -- the PoC consumer therefore pivots from a 32-bit compat CS (valid on both vendors); on AMD hardware the 64-bit form works too, and the MSR-corruption step is mode-independent. NetBSD upstream nvmm_x86_svm.c has the identical omission. Stock harness guest note: with -enable-kvm/default qemu64 neither VMX nor SVM is exposed, kldload nvmm fails 'nvmm: cpu not supported' (MOD_LOAD error 45) -- verified live.