nvmm MOD_UNLOAD tears down the hypervisor (nvmm_fini: all machine/vcpu locks lockuninit'd, impl freed, nvmm_impl=NULL) while /dev/nvmm is still dispatching ioctls β destroy_dev() runs last; unprivileged ioctl storm races kldunload into NULL-deref panic (UAF paths code-proven)
Summary
The EBUSY gate is unsynchronized and nvmm_fini runs BEFORE destroy_dev (the only draining op): in-flight ioctls deref NULL/freed nvmm_impl, machines created in the window survive with destroyed locks, and a held-open fd later runs the dtor through unmapped module text. VERIFIED live on guest (vmx backend): unpriv racer vs root kldunload crashed 2/2 at nvmm_ioctl+0x444 (rax=nvmm_impl=NULL); fix (open-count gate + destroy_dev-first + re-check) validated dynamically - 40/40 EBUSY, zero panics across 36M ioctls/684k machine cycles, clean final unload. Group-nvmm reachable (O_RDONLY suffices).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4926 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| nvmm_race.c | β | 4.0 KB | view raw | |
| race_unload.sh | β | 1.2 KB | view raw | |
| run.sh | β | 183 B | view raw | |
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| verdict.json | β | 4.1 KB | view raw | |
| build.log | β | 80 B | view raw | |
| run.log | β | 589 B | view raw | |
| panic.txt | β | 1.7 KB | view raw | |
| fix.diff | β | 2.7 KB | view raw | |
| fix_build.log | β | 12.1 KB | view raw | |
| fix_run.log | β | 615 B | view raw | |
| race_driver_fixed.log | β | 5.8 KB | view raw | |
| run_fixed_racer.log | β | 1.6 KB | view raw | |
| env.txt | β | 693 B | view raw |
DF-4926 β nvmm module unload tears down the hypervisor before the device is dead
nvmm_dragonfly.c destroys the NVMM implementation state while /dev/nvmm
is still live, inverting the required destroy_dev()-first order:
nvmm_detach()(nvmm_dragonfly.c:366-374): unsynchronizednmachines == 0check at :369, thennvmm_fini()at :372 β whichlockuninit()s all 128Γ128 machine/vcpu locks (nvmm.c:998-1003), runsnvmm_impl->fini()(VMXON regions, MSR bitmaps freed) and setsnvmm_impl = NULL(nvmm.c:1005-1006).nvmm_modevent()MOD_UNLOAD (nvmm_dragonfly.c:399-405) callsdestroy_dev(dev)only afternvmm_detach()has already done all of that β the device is open and dispatching ioctls the entire time.
Consequences (all reachable by an unprivileged user in group nvmm, given
an admin running kldunload nvmm):
- In-flight ioctl during
nvmm_fini():nvmm_capability()(nvmm.c:245,251) dereferencesnvmm_implon every call β NULL-page kernel deref; any machine ioctl useslockuninited locks or freed impl state (UAF). - Machine-create racing the
nmachinescheck: a machine allocated between :369 andnvmm_fini()survives into destroyed-lock land. - Open fd across a successful unload:
dfbsd_nvmm_dtor()(nvmm_dragonfly.c:325-335) is a module-text function pointer stored in the file's cdevpriv; closing the fd after unload callsnvmm_kill_machines()through unmapped text.
Build (on the DragonFly guest, as root)
cc -O2 -pthread -o nvmm_race nvmm_race.c
Run
As root, end-to-end (creates the testnv group-nvmm user if missing,
builds the racer, loads nvmm, races 40 unload/reload cycles, then a final
unload; the racer keeps the fd open for 75 s and closes it at exit):
./run.sh # panics are expected; watch the serial log
Note: /dev/nvmm is 0640 root:nvmm, so the unprivileged racer opens it O_RDONLY β DFly does not require FWRITE for ioctls, which is exactly how an unprivileged group-nvmm user drives every NVMM_IOC_* command.
The guest must be booted with the host CPU exposed so nvmm attaches
(QEMU_EXTRA="-cpu host" ./vm.sh up).
Expected output
- racer
/tmp/race.log:t=1ds cap=... create=... destroy=...counters - kernel:
Fatal trap 12: page fault while in kernel modewithnvmm_capabilityin the trace (NULLnvmm_impl), or a lockmgr/freed-text panic from the other detonation paths; guest goes down.
DF-4926 VERDICT β REPRODUCED (unload-race kernel crash; fix validated: FIXED)
Finding
sys/dev/virtual/nvmm/nvmm_dragonfly.c tears the NVMM hypervisor down in
the wrong order on module unload:
nvmm_detach()(:366-374): an unsynchronizednmachines == 0check at :369, thennvmm_fini()at :372 β whichlockuninit()s all 128Γ128 machine/vcpu locks (nvmm.c:998-1003), runsnvmm_impl->fini()(VMXON regions / MSR bitmaps freed) and setsnvmm_impl = NULL(nvmm.c:1005-1006).nvmm_modevent()MOD_UNLOAD (:399-405) callsdestroy_dev(dev)only afternvmm_detach()has done all of that β the device is open and dispatching ioctls the entire time.destroy_dev()is precisely the operation that revectors ops todead_dev_ops, drains in-flight calls and blocks new ones (kern/kern_conf.c:334-349, devfs destroy path); it must come FIRST.
Three detonation paths for an unprivileged user in group nvmm (device is
crw-r----- root nvmm; DFly ioctls do not require FWRITE, so an O_RDONLY
open drives every NVMM_IOC_*):
- in-flight ioctl during
nvmm_fini()βnvmm_implderef (NULL or freed impl) β e.g.nvmm_capability()nvmm.c:245,251; NVMM_IOC_MACHINE_CREATElanding between the :369 check andnvmm_fini()β machine alive withlockuninited locks and freed impl state (UAF);- fd kept open across a successful unload β
dfbsd_nvmm_dtor()(:325-335, module text, stored as the cdevpriv dtor at :315) runsnvmm_kill_machines()through unmapped text at close.
Reproduction (2026-09-25, guest DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC, backend x86-vmx, QEMU -cpu host)
- Racer (unprivileged
testnv, uid 1002, group nvmm, O_RDONLY open): ~1.1MNVMM_IOC_CAPABILITY/s + ~20k machine create/destroy cycles/s (standalone calibration in run.log header; fixed-run totals: 36M caps, 684k cycles). - Admin side:
kldunload nvmm(root) while the racer runs.
Baseline (stock /boot/kernel/nvmm.ko): TWO runs, both panicked the guest with the identical crash site:
Fa[tal trap ...] Stopped at nvmm_ioctl+0x444: movq 0x48(%rax),%rax
Run 2 panicked on the first kldunload. %rax == nvmm_impl == NULL
(just set by nvmm_fini() at nvmm.c:1006 during the in-flight unload);
movq 0x48(%rax) is the nvmm_impl->capability function-pointer load in
the dispatch (offset 0x48 of struct nvmm_impl, nvmm_internal.h:106-133).
Guest down both times; vm.sh status β down. (The "Fatal trap 12" banner
was truncated by the crashing serial console β the Fa fragment β but the
faulting RIP/instruction and the dead guest are on the serial log; see
panic.txt.)
Patched (fix.diff applied to guest /usr/src, module rebuilt, same race):
- all 40
kldunloadattempts during the storm returned EBUSY (rc=1Γ 40,rc=0Γ 0) β the newnvmm_opensgate held; - no panic; guest survived the full storm (36M ioctls, 684k machine cycles);
- after the racer died (fd closed β dtor ran against the LIVE module),
the final
kldunloadreturned 0 and the module unloaded cleanly.
Impact ceiling
Race-condition kernel memory-safety bug at the hypervisor boundary: the
demonstrated result is a NULL-deref panic from unprivileged-steerable
ioctls racing an admin unload; paths (2)/(3) give UAF on freed
implementation state (VMXON regions, MSR bitmaps, machine/vcpu locks) and
execution through unmapped module text β plausible corruption/escalation
material for the group-nvmm user, not weaponized here (weaponization needs
the admin to run kldunload concurrently, hence Medium rather than High).
CVSS: CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:L/A:H (UI:R = the required
concurrent kldunload by root).
Fix
fix.diff (git-apply clean against sys/):
- an
nvmm_openscounter (inc at successful open, dec at dtor β afternvmm_kill_machines(), so a concurrent detach observesnmachines==0beforeopens==0); nvmm_detach()refuses while any fd is open or any machine exists, thendestroy_dev()first, then (stable, drained) re-check with device restore on bail, and only thennvmm_fini();- MOD_UNLOAD no longer destroys the dev after the teardown.
Validated end-to-end on the guest (baseline panic vs patched EBUSY/clean unload under the identical storm; race_driver_fixed.log, run_fixed_racer.log, fix_build.log).
PoC changes vs any seed
No executable seed (fresh racer + driver authored). Practical fixes made
while bringing it up: su -m testnv -c '... >/tmp/race.log 2>&1 &' runs
via root's csh β "Ambiguous output redirect"; replaced with a #!/bin/sh
wrapper script executed via su -m testnv -c /home/testnv/goracer.sh &.
The racer opens /dev/nvmm O_RDONLY (0640 gives group read only; DFly
ioctls need no FWRITE β first attempt with O_RDWR got EACCES).
Guest hygiene
vm.sh reset with-src performed at the end of the run (guest left clean;
the testnv user, /root packs and /usr/src patch are on the reverted disk
state only).
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
fixedFIXED dynamically: baseline (stock nvmm.ko) panicked the guest twice at nvmm_ioctl+0x444 under the race; with fix.diff applied to guest /usr/src (module rebuilt, fix_build.log) the identical workload produced 40/40 kldunload EBUSY (open-count gate), zero panics across 36M capability ioctls and 684k machine create/destroy cycles, and a clean final unload after the racer's fd closed (race_driver_fixed.log, run_fixed_racer.log, fix_run.log).
fix.diff (git-apply clean); fix_build.log (BUILD_RC=0); fix_run.log (40x EBUSY vs baseline panic, clean final unload, guest up); panic.txt (baseline crashes)
Confirmed kernel references
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:325
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:366
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:369
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:372
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:399
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:404
- sys/dev/virtual/nvmm/nvmm.c:994
- sys/dev/virtual/nvmm/nvmm.c:1005
- sys/dev/virtual/nvmm/nvmm.c:245
- sys/kern/kern_conf.c:346
Detail
Evidence (decisive lines)
panic.txt (two baseline serial-console crash excerpts, both 'Stopped at nvmm_ioctl+0x444: movq 0x48(%rax),%rax', guest down); run.log (racer calibration: 1.1M cap ioctls/s, 20k machine cycles/s as uid 1002); race_driver_fixed.log + run_fixed_racer.log + fix_run.log (patched module: 40/40 EBUSY, no panic, clean final unload); fix_build.log (patched module build)
PoC changes
No executable seed (fresh racer + driver authored). Fixes made while bringing it up: (1) su -m executes -c via root's csh -> 'Ambiguous output redirect'; replaced with a #!/bin/sh wrapper script; (2) racer opens /dev/nvmm O_RDONLY β 0640 gives group read-only and DFly ioctls need no FWRITE (O_RDWR got EACCES); (3) driver tolerant of an already-loaded module.
Verified recommended fix
Count open descriptors, refuse MOD_UNLOAD while any are open (or machines exist), and destroy_dev() BEFORE nvmm_fini() β see fix.diff.
Verdict
REPRODUCED twice (identical crash site) and FIX VALIDATED dynamically. nvmm_dragonfly.c tears down the hypervisor BEFORE killing the device: nvmm_detach() checks nmachines unsynchronized (:369) and runs nvmm_fini() (:372) β lockuninit of all 128x128 machine/vcpu locks, impl->fini(), nvmm_impl=NULL β while /dev/nvmm is still dispatching ioctls; destroy_dev() only runs afterwards (:404). An unprivileged group-nvmm user storming NVMM_IOC_CAPABILITY (~1.1M/s) + machine create/destroy cycles (~20k/s) against a root kldunload panicked the stock guest twice at nvmm_ioctl+0x444: movq 0x48(%rax),%rax (rax = nvmm_impl = NULL). With fix.diff (open-count gate + destroy_dev-first ordering), the identical race produced 40/40 EBUSY, zero panics over 36M ioctls / 684k machine cycles, and a clean final unload.
No comments yet.