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

vm_object reference leak on fork() of wired (mlock'd) map entries β€” permanent swap/kernel-memory exhaustion by unprivileged users

Field Value
ID DF-2672
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H
CWE CWE-401 Missing Release of Memory before Effective Removal
File sys/vm/vm_map.c
Lines 3627-3634 (acquire at :3510-3513 via :3882/:3889)
Area vm
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

vm_map_copy_entry()'s wired path detaches and NULLs dst_entry->ba.map_object without dropping the reference that vm_map_backing_replicated(flags==0) took on it moments earlier in vmspace_fork_normal_entry(). The stale comment "its ref-count has not yet been adjusted" predates the vm_map_backing rework. Each fork() of a wired entry orphans one vm_object reference; the object is never terminated so its swap space is never freed.

Threat model & preconditions

Unprivileged local user: mlock() needs no privilege (only RLIMIT_MEMLOCK, default free/3). Repeat mmap+dirty+mlock+fork+exit cycles; orphaned objects accumulate, and under memory pressure their dirty pages page out to swap that can never be reclaimed β€” permanent system-wide memory/swap exhaustion DoS surviving the death of the offending processes.

Proof of concept

VERIFIED (findings/poc/DF-2672/leak_fork.c): child mmaps 4MB MAP_ANON|MAP_PRIVATE, memsets (dirty), mlocks, forks again, all exit. Stock guest: vm_object count 1.01Kβ†’1.21K for 200 leak iterations (control β‰ˆ +10); 3.4GB pressure β†’ 1,033,784 swap blocks (~504MB) with zero anon-owning processes, growing to ~844MB and persisting after idle; only reboot reclaims. Fix (vm_object_deallocate before NULLing, mirroring the :1132-1135 pattern) validated on a rebuilt kernel.

--- a/sys/vm/vm_map.c
+++ b/sys/vm/vm_map.c
@@ -3626,6 +3626,15 @@
        if (dst_entry->ba.map_object != NULL) {
            vm_map_backing_detach(dst_entry, &dst_entry->ba);
+           /*
+            * Drop the reference vm_map_backing_replicated()
+            * acquired for the cloned entry (the stale comment
+            * claiming the ref-count was not yet adjusted
+            * predates the backing_ba rework).  Without this
+            * the object and its swap space leak permanently
+            * whenever a wired entry is forked.
+            */
+           vm_object_deallocate(dst_entry->ba.object);
            dst_entry->ba.map_object = NULL;
            vm_map_entry_dispose_ba(dst_entry,

Timeline

  • 2026-08-29 Discovered during pass-2 audit of vm_map.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2672 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
leak_fork.c β€” 1.8 KB view raw
hog.c β€” 472 B view raw
build.sh β€” 117 B view raw
run.sh β€” 1.2 KB view raw
build.log β€” 50 B view raw
run.log β€” 1.3 KB view raw
fix_run.log β€” 1.0 KB view raw
fix.diff β€” 654 B view raw
env.txt β€” 320 B view raw

DF-2672 β€” vm_object reference leak on fork() of wired (mlock'd) VM entries

What

vm_map_copy_entry() (sys/vm/vm_map.c:3600) handles the fork of a wired (src_entry->wired_count != 0) map entry by scrapping the cloned child entry's object and copying the pages into a fresh one. Its comment claims "its ref-count has not yet been adjusted so we can just NULL out the field", but that stopped being true with the vm_map_backing rework: vmspace_fork_normal_entry() (sys/vm/vm_map.c:3882) calls vm_map_backing_replicated(new_map, new_entry, 0) before vm_map_copy_entry() (sys/vm/vm_map.c:3889), and with flags == 0 that function takes a reference on the base object (vm_object_reference_quick(), sys/vm/vm_map.c:3510-3513).

The wired path detaches and NULLs dst_entry->ba.map_object (sys/vm/vm_map.c:3627-3634) without dropping that reference β†’ one vm_object reference leaks per fork() of a wired entry.

Impact

Unprivileged local user (mlock works within RLIMIT_MEMLOCK, default = free_memory/3, no privilege needed β€” sys/vm/vm_mmap.c:1020-1031, sys/kern/kern_plimit.c:131-132):

  • leaked vm_objects are never freed (vm_object_deallocate never sees 0);
  • their dirty anonymous pages, once paged out under memory pressure, keep their swap space allocated forever (swap_pager_freespace only runs at object termination);
  • permanent kernel-memory + swap exhaustion β†’ system-wide DoS that survives the death of the offending processes. No RLIMIT bounds the accumulated leak across processes.

Reproduce (on the audit QEMU guest)

./build.sh   # cc -O2 -o /tmp/leak_fork leak_fork.c    (+ hog.c)
./run.sh     # control vs leak phases + memory pressure

Success criterion: vmstat -m "vm_object" Count grows by ~1 per leak iteration and never returns; control run returns to baseline. After pressure, swapinfo shows hundreds of MB permanently consumed with no processes owning anonymous memory.

Fix

fix.diff β€” call vm_object_deallocate() before NULLing the field (mirrors vm_map_entry_dispose(), sys/vm/vm_map.c:1132-1135). Validated by kernel rebuild + PoC re-run (see VERDICT.md).

VERDICT.md
↓ download raw

DF-2672 VERDICT β€” REPRODUCED (leak), fix validated

Question

Does fork() of a wired (mlock'd) entry leak a vm_object reference, and does that translate into permanently unreclaimable kernel memory / swap?

Root cause (path:line)

  • vmspace_fork_normal_entry() clones the child entry and calls vm_map_backing_replicated(new_map, new_entry, 0) at sys/vm/vm_map.c:3882 β€” with flags == 0 this executes vm_object_reference_quick(object) on the base object (sys/vm/vm_map.c:3510-3513).
  • vm_map_copy_entry() (sys/vm/vm_map.c:3600) then takes the wired path because src_entry->wired_count != 0 (:3608) and discards the cloned object: vm_map_backing_detach() + ba.map_object = NULL (:3627-3629) without vm_object_deallocate(). The stale comment at :3613-3615 ("its ref-count has not yet been adjusted") is wrong since the backing_ba rework β€” the reference IS taken before the call. One reference is orphaned per fork.
  • The orphaned object is never terminated, so swap_pager_freespace() never runs for it: dirty pages paged out under pressure keep their swap allocation forever.

Reproduction (guest: DragonFly 6.5-DEVELOPMENT #0, INVARIANTS kernel)

leak_fork.c: mmap(ANON|PRIVATE) + memset (dirty) + mlock + fork(); child exits; owner exits. Control omits the inner fork.

Decisive observations (run.log):

phase vm_object count swap used (512B blocks)
baseline 1.01K 0
control 100x4MB + pressure 1.02K (+~10 noise) 0
leak 200x4MB 1.21K (+~190 for 200 forks) 0 (no pressure yet)
hog 3.4GB pressure 1.21K (unchanged) 1,033,784 (~504MB)
+leak 50x4MB + pressure 1.25K 1,727,816 (~844MB)
after 30s idle, no anon-owning procs 1.25K 1,720,384 (persists)
  • Object growth β‰ˆ 1 per fork-of-wired-entry, exactly as the code path predicts; control adds only kernel noise.
  • ~844MB of swap permanently consumed with no processes owning anonymous memory (every leak_fork/hog exited). This swap can never be freed except by reboot: the owning vm_objects are unreachable.

Unprivileged: mlock() needs no privilege, only RLIMIT_MEMLOCK (default = free memory / 3, sys/kern/kern_plimit.c:131-132). The PoC ran as root on the guest but uses no privilege; mlock(256KB) succeeds for normal users within that rlimit.

Impact: permanent, unbounded, aggregation-across-processes consumption of kernel memory and swap by an unprivileged local user β†’ system-wide memory-exhaustion DoS (Medium).

Fix validation

fix.diff adds vm_object_deallocate(dst_entry->ba.object) before NULLing (mirrors vm_map_entry_dispose(), sys/vm/vm_map.c:1132-1135). Kernel rebuilt in-guest (make nativekernel), rebooted, PoC re-run: vm_object count flat across leak iterations, no residual swap. See fix section in run.log / verdict.json fix_* fields.

Not a UAF

The bug only leaks references (object never freed early); there is no premature-free / type-confusion angle. Severity Medium (availability).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt kernel with fix.diff (plus DF-2673/DF-2674 fixes, independent hunks): 300 leak iterations + 3.4GB pressure leave vm_object at 998 (baseline 917) and swap at 0; stock kernel showed 1:1 object leak and 844MB permanent swap under the identical workload.

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Sun Aug 30 11:26:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unprivileged user: repeat { mmap ANON|PRIVATE -> memset (dirty) -> mlock -> fork -> let both processes exit }; each cycle orphans one vm_object reference; under memory pressure the dirty pages page out to swap which is never reclaimed -> permanent global kernel-memory/swap exhaustion -> system-wide DoS surviving process exit (only reboot reclaims).

Evidence (decisive lines)

['run.log: baseline vm_object 1.01K / swap 0; control 100x4MB -> 1.02K / 0; leak 200x4MB -> 1.21K; pressure -> swap 1,033,784 blocks (~504MB); +leak 50x4MB+pressure -> 1,727,816 (~844MB); persists after 30s idle with no anon-owning processes', 'fix_run.log: patched kernel #2, 300 leak iterations + 3.4GB pressure -> vm_object 998 (baseline 917, +81 system noise vs 1:1 leak growth on stock), swap 0', 'VERDICT.md: full path:line trace and A/B tables']

PoC changes

seed sketch replaced: wrote leak_fork.c (mmap+memset+mlock+fork churn with leak/control modes) and hog.c for memory pressure; first pressure attempt was a no-op because the hog byte argument was passed as MB-like small integers - fixed by passing real byte counts

Verified recommended fix

vm_object_deallocate(dst_entry->ba.object) before NULLing the field in vm_map_copy_entry()'s wired path (fix.diff, validated)

Verdict

vm_map_copy_entry()'s wired path NULLs dst_entry->ba.map_object without dropping the reference vm_map_backing_replicated() (flags==0) took for the cloned entry at fork time - the stale comment 'ref-count has not yet been adjusted' predates the backing_ba rework. One vm_object reference leaks per fork() of a wired (mlock'd) entry; the orphaned objects are never terminated so their swap allocations are never freed. Verified on the guest: vm_object count +~190 for 200 leak iterations (control +10), and 844MB of swap permanently retained after memory pressure with zero anon-owning processes, requiring nothing beyond an unprivileged mlock() within RLIMIT_MEMLOCK.