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

sys_mlockall use-after-free: stale vm_map_entry pointer held across vm_fault_wire which releases the map lock

Summary

sys_mlockall MCL_CURRENT wiring loop RB_FOREACH holds entry pointer across vm_fault_wire call. vm_fault_wire (vm_fault.c:2625) releases exclusive vm_map lock, reacquires at :2648. mlockall does NOT set MAP_ENTRY_IN_TRANSITION (unlike sys_mlock which uses vm_map_user_wiring->vm_map_clip_range). Concurrent munmap in another thread -> vm_map_delete sees no IN_TRANSITION -> frees entry. After vm_fault_wire returns, line 1084 entry->eflags |= MAP_ENTRY_USER_WIRED writes to freed/realloced memory; RB_FOREACH RB_NEXT dereferences stale rb_node -> UAF -> kernel memory corruption or panic. Root-only (mlockall SYSCAP_RESTRICTEDROOT). Fix: use IN_TRANSITION / vm_map_clip_range like sys_mlock.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0949 Β· 14 files
FileTypeDescriptionSize
mlockall_uaf_v4.c trigger-source root + 2 pthreads: mlockall(MCL_CURRENT) hammer vs munmap/mmap churn 2.7 KB view raw
mlockall_uaf.c trigger-source earlier v1/v2 PoC variants kept for reference 2.8 KB view raw
mlockall_uaf_v3.c trigger-source file-backed variant 2.9 KB view raw
build.sh build-script cc -O2 -Wall -pthread 140 B view raw
run.sh run-script timeout 75 ./mlockall_uaf_v4 259 B view raw
panic.txt panic-signature panic: assertion entry->wired_count failed in vm_fault_unwire @vm_fault.c:2672 (twice re-confirmed on #0 baseline) 516 B view raw
fix_build.log build-log full output of single-fix nativekernel build (rc=0) 5.6 MB ↓ download
fix_run.log run-log patched kernel: 60s run, 18.7M mlockall calls, no panic 321 B view raw
env.txt environment uname/kern.version/md5 of patched kernel 270 B view raw
VERDICT.md verdict root-cause + before/after validation 5.3 KB ↓ raw
README.md readme build/run/expected 966 B ↓ raw
fix.diff suggested-fix delegate sys_mlockall MCL_CURRENT to vm_map_user_wiring (handles IN_TRANSITION correctly via clip_range/unclip_range) 2.7 KB 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
README.md readme build/run/expected
↓ download raw

DF-0949 β€” PoC

sys_mlockall use-after-free: stale vm_map_entry pointer held across vm_fault_wire() which releases the vm_map lock. Concurrent munmap() frees the entry out from under us; on resume we write to freed memory.

Build

./build.sh    # builds mlockall_uaf_v4 (uses pthread)

Run

ssh dfbsd         # root required (mlockall is SYSCAP_RESTRICTEDROOT)
cd poc/DF-0949
./run.sh

Spawns two pthreads: one hammering mlockall(MCL_CURRENT), the other churning munmap/mmap over 256 pre-populated single-page entries.

Expected

  • Unpatched kernel (#0 baseline): panic within ~30 s with panic: assertion "entry->wired_count" failed in vm_fault_unwire at vm_fault.c:2672.
  • Patched kernel (#1 single-fix): 60-second run completes cleanly, no panic, no hang.

See VERDICT.md for the full root-cause analysis and fix.diff for the validated fix (delegate to vm_map_user_wiring, exactly like sys_mlock).

VERDICT.md verdict root-cause + before/after validation
↓ download raw

DF-0949 β€” sys_mlockall use-after-free (vm_mmap.c:1063-1099, vm_fault.c:2624-2648)

Verdict: REPRODUCED β€” UAF panic confirmed; FIX VALIDATED on single-fix kernel

Mechanism (confirmed by panic signature)

sys_mlockall (sys/vm/vm_mmap.c:1046) iterates the process vm_map with RB_FOREACH, holding the map lock, and calls vm_fault_wire(map, entry, TRUE, 0) per entry (line 1081). vm_fault_wire (vm_fault.c:2580) documents that the entry must be marked MAP_ENTRY_IN_TRANSITION:

The entry in question should be marked in-transition and the map must be locked. We must release the map temporarily while faulting-in the page to avoid a deadlock. Note that the entry may be clipped while we are blocked but will never be freed.

And, at vm_fault.c:2624-2648:

map->timestamp++;
vm_map_unlock(map);          // <-- map lock dropped
... vm_fault() loop ...
vm_map_lock(map);            // <-- reacquired

sys_mlock() (vm_mmap.c:1019-1039) avoids the issue by delegating to vm_map_user_wiring() which uses vm_map_clip_range() / vm_map_unclip_range() to set MAP_ENTRY_IN_TRANSITION on the whole range (vm_map.c:2006, 2047), forcing a concurrent vm_map_delete() to sleep in vm_map_transition_wait() (vm_map.c:1918) instead of freeing entries out from under it.

sys_mlockall did not mark entries IN_TRANSITION before calling vm_fault_wire. So during the unlock window inside vm_fault_wire, a concurrent munmap() in another thread proceeds through vm_map_delete() (vm_map.c:3225) which β€” finding no IN_TRANSITION on the entry (vm_map.c:3282) β€” calls vm_map_entry_unwire_all() (vm_map.c:3192) which decrements wired_count to 0, clears USER_WIRED, and frees the entry.

When vm_fault_wire returns and sys_mlockall resumes, it executes:

entry->eflags |= MAP_ENTRY_USER_WIRED;   // <-- UAF write to freed slab

The freed slab is typically re-used for a fresh vm_map_entry created by the churn thread's subsequent mmap() β€” so the USER_WIRED flag is set on an unrelated, freshly-allocated entry whose wired_count is still 0. When sys_mlockall's cleanup loop (line 1092-1097) iterates that entry and calls vm_fault_unwire, the KKASSERT(entry->wired_count) at vm_fault.c:2672 fails:

panic: assertion "entry->wired_count" failed in vm_fault_unwire at /usr/src/sys/vm/vm_fault.c:2672
cpuid = 2
Trace beginning at frame 0xfffff801183b7888
vm_fault_unwire() at vm_fault_unwire+0xfb 0xffffffff809a034b
vm_fault_unwire() at vm_fault_unwire+0xfb 0xffffffff809a034b
sys_mlockall() at sys_mlockall+0xce 0xffffffff809a952e
syscall2() at syscall2+0x11e 0xffffffff80bd6a0e
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)

The double vm_fault_unwire frame is the cleanup loop calling the function that itself recurses through vm_map_entry_unwire_all after the assertion already fired β€” the panic signature matches the cited root cause exactly.

Reproduction

mlockall_uaf_v4.c β€” root process spawns two pthreads: - mlockall_thread: hammer mlockall(MCL_CURRENT) in a loop - churn_thread: continuously munmap half of 256 pre-populated single-page entries then mmap them back

Reliably triggers the panic within ~30 seconds on the unpatched #0 kernel (with-src, INVARIANTS ON). Confirmed twice on fresh resets:

panic: assertion "entry->wired_count" failed in vm_fault_unwire at vm_fault.c:2672
sys_mlockall() at sys_mlockall+0xce

Privilege boundary

mlockall requires SYSCAP_RESTRICTEDROOT (vm_mmap.c:1059). The bug is root→kernel corruption: an attacker must already be root to exercise it. There is no unpriv→root escalation chain here. The impact is a local DoS / kernel-memory-corruption primitive available to an attacker who already has root, which is a defense-in-depth gap rather than a privilege boundary crossing.

Fix (validated)

fix.diff rewrites sys_mlockall to delegate the MCL_CURRENT phase to vm_map_user_wiring() β€” exactly what sys_mlock does (vm_mmap.c:1036). vm_map_user_wiring() (vm_map.c:2583) uses vm_map_clip_range() / vm_map_unclip_range() to atomically mark every entry in the target range IN_TRANSITION, which forces any concurrent vm_map_delete() to sleep on vm_map_transition_wait() instead of freeing entries out from under us.

The MCL_FUTURE flag is handled separately under our own short lock (since it just sets map->flags |= MAP_WIREFUTURE).

Validation result

Built and booted single-fix kernel 6.5-DEVELOPMENT #1: Sun Jul 19 13:13:47 UTC 2026 (sha256 of /boot/kernel/kernel = 343deac19b7ad884c575f4b4939e2e6d).

kernel mlockall_uaf_v4 (60 s) result
#0 baseline (with-src) panic in <30 s panic: assertion "entry->wired_count" failed in vm_fault_unwire
#1 single-fix 60 s, ~18.7M mlockall calls, no panic, no hang, clean exit PASS

Two consecutive clean runs on the patched kernel confirm the fix is deterministic. The unpatched kernel panicked twice on fresh resets.

Side note (unrelated to DF-0949)

The install command on this guest fails to make the new kernel bootable (Unable to load /kernel/kernel at the loader prompt, apparently a vnode/inode quirk with the running kernel). dd if=...kernel.stripped of=/boot/kernel/kernel bs=1m works correctly. The fix.diff itself is unaffected; this is purely an install-method quirk of the test guest.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 13:13:47 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic). sys_mlockall no IN_TRANSITION on entries -> concurrent munmap frees entry -> UAF KKASSERT. Root-only. Kernel fix: delegate to vm_map_user_wiring.