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)
PoC verification
Evidence pack
findings/poc/DF-0949 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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).
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
fixedvalidated
see evidence pack
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.
No comments yet.