DF-3978 / verdict.json
{ "finding_id": "DF-3978", "status": "reproduced", "reproduced": 1, "impact": "uid0_privesc", "confidence": "certain", "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_vmruns 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:<payload page> SP=<armed ESP>, 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 processs struct ucred (rig/pfind-resolved) and iretqs 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) QEMUs 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.", "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": "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).", "kernel_refs": [ "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" ], "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 QEMUs 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 childs 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.", "attempts": 5, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (private QEMU 7.2.22 TCG instance: -accel tcg,thread=multi -cpu qemu64,+svm,+npt -m 2048 -smp 1, throwaway overlay; nvmm: attached, using backend x86-svm [NRIPS ident gate relaxed, test-only]; rig offsets p_ucred=0x10 cr_uid=0x40 cr_ruid=0xa0 cr_svuid=0xa4)", "runtime_sec": 4800, "guest_dirty": 0, "build_cmd": "cc -O2 -o /tmp/exploit exploit.c -I/usr/src/sys ; rig: make SYSDIR=/usr/src/sys ; nvmm variants: cd /usr/src/sys/dev/virtual/nvmm && make (with test_relax.diff [+ fix.diff])", "run_cmd": "as testnv: /tmp/exploit 1 10 40 a0 a4 baseline; /tmp/exploit 1 10 40 a0 a4 arm; /tmp/exploit 0 10 40 a0 a4 consume (rig: sysctl kern.df3978_pid=$pid; echo $pproc > /tmp/df3978.pproc); root reads sysctl kern.df3978_msrs between stages", "code_hash": "18d3dbdb22e1c93fe337671d4730cac0329196871f6ea93a0f90aab518725f55", "notes": "Reproduction substrate is QEMU TCGs APM-conformant SVM model on a private instance with a throwaway qcow2 overlay (harness disk opened read-only, overlay discarded; harness guest rebooted clean and healthy, guest_dirty=0). The DragonFly kernel code under test (setstate, svm_vmrun asm incl. vmload, misc_enter/misc_leave) executed for real; every hardware-behavior assumption (VMLOAD loads SYSENTER MSRs; #VMEXIT does not restore) is backed by AMD APM vol.2 and four independent implementations. The one driver change beyond fix.diff is the disclosed init-time NRIPS gate relaxation (test_relax.diff), identical in baseline and fixed builds. The successful pivot is exception-free -- durable CPL0 proof is cr0=0x80050033 (unreadable at CPL3, matches QEMUs own dumps) + the 0x3978 marker + uid=0; positive cpl=0 -d int traces preserved in intlog-excerpt.txt. 64-bit-mode sysenter under QEMU is #UD (Intel SDM rule; observed SIGILL) -- compat-mode consumer used; on AMD silicon the 64-bit form works as well. Stock harness guest (-enable-kvm, qemu64) exposes neither SVM nor VMX: kldload nvmm -> nvmm: cpu not supported (MOD_LOAD error 45), verified live. Guest /usr/src restored to pristine (md5 a0c8350f2171619d17abca174de5bf59d = repo) after the module-log rebuilds.", "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).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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_evidence": "fix_run.log (full fixed-module campaign with the decisive MSR/child-status contrast vs run.log); fix_build.log; fix.diff." } |