NVMM VMX: host NMIs are silently swallowed while a vCPU is resident on the CPU
Summary
vmx_exit_exc_nmi() handles NMI-caused VM exits by setting exit->reason = NVMM_VCPU_EXIT_NONE, after which the run loop immediately re-enters the guest. No host-side NMI dispatch occurs, so for the entire time a vCPU is resident, DragonFly's NMI handling (trap() T_NMI -> isa_nmi() diagnostics, ddb_on_nmi, panic_on_nmi) never runs for that CPU. NetBSD's nvmm_x86_vmx.c added nmi_dispatch() for exactly this defect; the June-2021 DragonFly port does not carry it and DragonFly has no equivalent hook (the identical hole exists in nvmm_x86_svm.c). An unprivileged user in group nvmm can keep a CPU ~100% guest-resident (verified: /dev/nvmm 0640 root:nvmm still grants full ioctl access via open(O_RDONLY)), suppressing hardware-NMI telemetry (watchdog, ECC/parity/SERR logging, panic-on-hardware-failure). Extensive LIVE campaign on the nested-KVM guest (VMX backend fully live): raw-ioctl driver ran a real guest through the entire exit dispatcher, hostile state, hostile injection, garbage-instruction fuzz, and 600k concurrent state/inject ioctls against 6 resident vCPUs - ZERO kernel panics; all CVE-2022-30123/30124-family sanitizations present and live-verified. The NMI differential is masked under QEMU's all-CPU NMI injection (one always lands in a host window and DDB-stops); root cause established by source + NetBSD's upstream fix. No user->root route.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3829 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| nvmm_drive.c | β | 21.9 KB | view raw | |
| vctest5.c | β | 2.5 KB | view raw | |
| send_nmi.py | β | 510 B | view raw | |
| run.log | β | 3.3 KB | view raw | |
| run.2.log | β | 515 B | view raw | |
| run.seq.log | β | 2.8 KB | view raw | |
| nmi_ddb_evidence.txt | β | 1.8 KB | view raw | |
| env.txt | β | 354 B | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| build.sh | β | 196 B | view raw | |
| run.sh | β | 148 B | view raw | |
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw |
DF-3829 β NVMM VMX: host NMIs swallowed while a vCPU is resident
File under audit: sys/dev/virtual/nvmm/x86/nvmm_x86_vmx.c (pass 2).
What this pack contains
nvmm_drive.cβ raw-ioctl NVMM driver used for the whole pass-2 live verification campaign (exit-dispatcher tests, hostile state, hostile event injection, concurrent stress, vCPU-residency spin). It contains no exploit; it is a correctness/robustness exerciser for the guest->host boundary.vctest5.cβ minimal machine/hva/gpa/vcpu/setstate reproducer used while debugging the ioctl harness (kept for reference).send_nmi.pyβ sendsnmito the guest via the QEMU monitor socket.run.logβ full sequential sub-test output (tests A..H).run.2.logβ full concurrent-stress output (6 vCPUs + 2 hammer threads, ~600k racing ioctls).run.seq.logβ earlier sequential run (with the accidental garbage-instruction fuzz evidence).nmi_ddb_evidence.txtβ live-captured serial-console evidence of the NMI trial (guest dropped to DDB fromvmx_vcpu_run+0x522).env.txtβ guest uname, compiler, NMI sysctls, /dev/nvmm mode.fix.diffβ proposed fix for the swallowed-NMI window.
Build (on the DragonFly guest)
cc -O2 -o nvmm_drive nvmm_drive.c -I/usr/src/sys -lpthread
Prerequisites: kldload nvmm (requires VMX; on the audit guest the VM
was booted with qemu-system-x86_64 ... -cpu host and the host KVM has
nested=Y), and read access to /dev/nvmm (group nvmm).
Run
# sequential boundary tests (expect: rc=0, no kernel panic) ./nvmm_drive run # keep 6 vCPUs resident (for the NMI trial) and concurrent stress ./nvmm_drive spin 24 6 ./nvmm_drive stress 15 6 # NMI injection from the QEMU monitor (host side) python3 send_nmi.py
Expected results
run: every sub-test ends cleanly (HALTED / SHUTDOWN / INVALID), hostile injections are rejected with EINVAL or produce clean VMentry-failure exits, the kernel never panics.stress: all threads complete, system alive.- NMI trial: see
nmi_ddb_evidence.txtβ under nested KVM the differential for the swallow window cannot be isolated (QEMUnmihits all CPUs at once); verdict for DF-3829 isinconclusivelive, root cause established by source analysis.
DF-3829 β VERDICT
Finding: vmx_exit_exc_nmi() in sys/dev/virtual/nvmm/x86/nvmm_x86_vmx.c
(lines 1252β1271) swallows NMI-caused VM exits. When the exit interruption
information is a valid NMI, the handler sets exit->reason =
NVMM_VCPU_EXIT_NONE and the run loop immediately re-enters the guest
(nvmm_x86_vmx.c:2394β2399, 2467β2469). No host-side NMI dispatch happens:
the NMI telemetry (DragonFly trap() T_NMI path β isa_nmi() β
ddb_on_nmi/panic_on_nmi, sys/platform/pc64/x86_64/trap.c:759β776) is
never invoked for an NMI that arrives while the CPU is executing a guest.
Upstream correlation: NetBSD's nvmm_x86_vmx.c added nmi_dispatch()
(precisely so the NMI watchdog keeps working while guests run); the
DragonFly port (June 2021) predates/omits it and DragonFly has no
equivalent API.
Reproduction attempt (live)
Guest: DragonFly 6.5-DEVELOPMENT, 6 vCPUs, QEMU/KVM with -cpu host,
host KVM nested=Y; kldload nvmm β "nvmm: attached, using backend
x86-vmx" (sys/dev/virtual/nvmm/x86/nvmm_x86_vmx.c backend fully live).
Attack-surface confirmation (relevant context, nvmm_dragonfly.c):
/dev/nvmm is root:nvmm 0640. A non-root user in group nvmm cannot
open(O_RDWR), but open(O_RDONLY) + full ioctl access works β
CAPABILITY, MACHINE_CREATE, HVA_MAP, GPA_MAP, VCPU_CREATE, SETSTATE,
RUN all succeed for the unprivileged group member (verified live with
user testnv). The entire VMX backend is therefore reachable
unprivileged (for group members), confirming the threat model.
Trial: nvmm_drive spin 24 6 (6 vCPUs continuously resident) + 3Γ
QEMU-monitor nmi. Result (serial console, captured before reboot):
CPU0 stopping CPUs: 0x0000003e stopped Stopped at vmx_vcpu_run+0x522: movl %dr0,%edx db>
The NMI that froze the machine landed while the CPU was in nvmm's
host-side run loop (vmx_vcpu_run+0x522 = x86_get_dr0() region,
nvmm_x86_vmx.c:2131) β i.e. host-window NMIs are delivered normally.
The swallow window (CPU in non-root) cannot be isolated on this guest:
QEMU nmi injects into all 6 vCPUs simultaneously, so at least one
NMI essentially always lands in a host-side scheduling window and the
resulting DDB stop masks the differential. On real hardware (or with a
per-CPU NMI source) the differential is: idle β "NMI ... going to
debugger"; vCPU resident β nothing, guest keeps running.
Status: inconclusive (live), root cause certain from source.
Impact ceiling: while any vCPU is resident on a CPU, hardware-NMI
telemetry for that CPU is lost β NMI watchdog, ECC/parity/SERR
reporting (isa_nmi() LOG_CRIT paths), and (with panic_on_nmi)
intentional panic-on-hardware-failure are suppressed for the residency
period. A guest (or an attacker running a CPU-saturating VM) can keep
a CPU resident ~100% of the time. No memory-safety impact; availability
of diagnostics only. Severity: Low.
Adjacent verification performed during the same run (no findings)
The same campaign exercised, under a live VMX backend, with zero kernel panics:
- Exit dispatcher end-to-end (
vmx_vcpu_run, nvmm_x86_vmx.c:2393β 2461): a real 16-bit guest driven through CPUID (in-kernel handler incl. >max/hypervisor-leaf fallbacks),mov cr0with CD|NW (in-kernel CR0 handler, nvmm_x86_vmx.c:1646β1703), port-IO exits (simple and, accidentally via mis-encoded guest code, dozens of wild ports/sizes/STR forms β a de-facto fuzz session), RDMSR/WRMSR exits (kernel-handled MSR_IA32_MISC_ENABLE/MSR_TSC/MSR_CR_PAT and user-delivered 0xDEADBEEF), EPT-violation handling (gpa fault-in vianvmm_do_vcpu_runβos_vmspace_fault), HLT/HALTED, triple-fault SHUTDOWN, and VMentry-failure (exit 33) paths. All exits returned well-formed data to the emulator. - Hostile state (
vmx_vcpu_setstate, 2612β2776): PAT=0xFFβ¦F, EFER=0xD, CR3=0xDEAD000, DR7=0xFFβ¦F β clean VMentry failure (hwcode 33), no panic. FPU state all-0xFF β accepted with MXCSR/MXCSR_MASK sanitization (2762β2764) and the guest ran to HALT β confirming the CVE-2022-30123/30124-family FPU/XCR0 fixes are present and effective (gxcr0 masked at 2674β2677, xstate_bv forced at 2766β2769; the xsave maskvmx_xcr0_maskis only X87|SSE soxsave/xrstornever touch the 576-bytegxsaveout of bounds). - Hostile event injection (
vmx_vcpu_inject, 1125β1181): EXCP vector 0xFF β EINVAL (bounds checks at 1144β1147 live-confirmed); INTR vector 0xFF β clean VMentry failure; EXCP vector 13 witherror=0xFFFFFFFF00000000β accepted without a "VMX fail valid" panic, confirming VMWRITE ignores upper bits for 32-bit VMCS fields (no kernel-reachable vmwrite failure from user error codes). - Concurrency: 6 vCPUs resident 15 s + 2 threads performing ~600k racing GETSTATE/SETSTATE/INJECT ioctls against running vCPUs β all succeeded, system alive (vcpu-lock serialization holds).
- Garbage guest code: wild instruction streams (mis-encoded 32-bit operands in 16-bit mode) produced hundreds of arbitrary exits; every one handled cleanly.
Non-findings recorded for completeness (both analyzed and killed):
* Post-exit vmresume returning VMfail (-1 INVALID exits) seen under
nested KVM after HLT/entry-failure; logic is identical to NetBSD
current, impact is per-vCPU availability of an emulator's own making
(possibly a nested-KVM artifact) β not filed.
* pmap_add_cpu() called without matching pmap_del_cpu() (XXX
comment at 2305β2312) β over-inclusive invalidation masks are
conservative-safe.
* vmx_htlb_flush/pm_invgen EPT coherence protocol (2206β2236,
DragonFly-specific) β reasoned through: double-bump generation +
interrupt-forced VMEXIT + pre-launch generation check is sound.
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
not_testablefix.diff proposes dispatching isa_nmi() from vmx_exit_exc_nmi(); not kernel-built/validated because the baseline itself could not be cleanly reproduced on this nested-KVM guest (no isolatable NMI source)
findings/poc/DF-3829/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
nmi_ddb_evidence.txt (DDB stop at vmx_vcpu_run+0x522 during the NMI trial); run.log + run.2.log (boundary tests and 6-vCPU concurrent stress, all clean); nvmm_drive.c (driver); fix.diff (proposed isa_nmi dispatch)
PoC changes
driver built from scratch (no seed); initial ioctl harness passed cmds through int-typed params (sign-extension broke >2^31 cmds -- userspace bug, kernel correctly EINVALs); guest code needed 0x66 operand prefixes for 16-bit real mode; per-test fresh vCPUs to avoid a nested-KVM vmresume-failValid artifact
Verified recommended fix
Dispatch host NMI handling from vmx_exit_exc_nmi() on NMI-typed exits (isa_nmi() + panic on hardware failure; or add an nmi_dispatch()-style hook like NetBSD) in both nvmm_x86_vmx.c and nvmm_x86_svm.c
Verdict
vmx_exit_exc_nmi() (sys/dev/virtual/nvmm/x86/nvmm_x86_vmx.c:1252-1271) swallows NMI-caused VM exits: a valid NMI in EXIT_INTR_INFO yields NVMM_VCPU_EXIT_NONE and the run loop re-enters the guest, so host NMI telemetry (NMI watchdog, isa_nmi() hardware-failure logging, panic/ddb-on-NMI) is suppressed for the entire time any vCPU is resident on the CPU. Root cause is certain from source; NetBSD added nmi_dispatch() upstream for exactly this and the DragonFly port lacks it. Live differential under nested KVM is inconclusive: QEMU monitor 'nmi' injects into all 6 guest CPUs simultaneously and at least one NMI always lands in a host-side window, dropping the box into DDB from vmx_vcpu_run+0x522 (x86_get_dr0 region) -- proving host-window NMIs ARE delivered, and masking the non-root-window swallow. Adjacent live campaign on the same fully-live VMX backend (exit dispatcher end-to-end, hostile state, hostile injection, 600k concurrent ioctls, garbage-instruction fuzz) produced zero panics.
No comments yet.