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

UAF of vm_map_backing and vm_map across per-iteration unlock in /proc/<pid>/map

Field Value
ID DF-0923
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-416 Use After Free
File sys/vfs/procfs/procfs_map.c
Lines 65-242
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

procfs_domap() drops the vm_map read lock and the target's p_token on every iteration, then continues to dereference a cached ba (pointing into a map entry) and the cached map (pointing into the vmspace) without any reference on the vmspace or the entry. A concurrent exec or exit on the target frees either the individual map entry (dangling ba) or the entire vmspace (dangling map), causing use-after-free reads and β€” when the vmspace goes away β€” a lockmgr operation on freed memory. Reliable kernel panic; plausible cross-process corruption primitive via slab grooming (not proven in this audit).

Root cause

The function caches map = &p->p_vmspace->vm_map once at procfs_map.c:65 β€” no vmspace_ref/vmspace_hold (contrast procfs_rwmem() at sys/vfs/procfs/procfs_mem.c:84-94 which explicitly does vmspace_hold(vm) before touching the map). It then enters the loop with vm_map_lock_read() (procfs_map.c:86) β€” which is only lockmgr(&map->lock, LK_SHARED) (sys/vm/vm_map.h:484-485) and does NOT increment vm_holdcnt β€” and immediately releases p_token (procfs_map.c:87).

Inside the loop it captures ba under the lock (procfs_map.c:101, :134-136), records last_timestamp, and drops the map lock at procfs_map.c:142-143 (vm_map_unlock(map)). After the drop it:

  • (a) re-reads ba->object at procfs_map.c:175-178 to fetch flags/ref_count, and
  • (b) passes ba->object to sbuf_printf at procfs_map.c:216.

Between the unlock at :143 and the re-lock at :230 a concurrent vmspace_exec() (sys/vm/vm_map.c:4298, called from sys/kern/kern_exec.c:957/967) or vmspace_unshare() (sys/vm/vm_map.c:4338) or the exit path (sys/kern/kern_exit.c:432-433, vmspace_relexit()) can delete the entry (vm_map_remove/vm_map_delete run with the exclusive lock the reader just released) and, when the old vmspace's refcount hits 0, complete stage-1 + stage-2 termination (sys/vm/vm_map.c:480-545) and objcache_put(vmspace_cache, vm) at sys/vm/vm_map.c:543 β€” freeing the very struct that map points into.

The reader then calls vm_map_lock_read(map) at procfs_map.c:230 on freed memory, and RB_FOREACH at the next iteration dereferences map->rb_root.

The per-entry ba is similarly dangling: vm_map_backing is embedded in vm_map_entry (sys/vm/vm_map.h:170-205,228) and is not independently reference-counted, so any concurrent munmap/mprotect/exec that removes the entry frees ba while lines :175-178 and :216 still read through it.

The timestamp re-validation at procfs_map.c:236-240 only protects the entry cursor for RB_NEXT; it does nothing for the already-captured ba/map. procfs_domap() also does not check P_WEXIT/P_INEXEC (contrast procfs_rwmem() at procfs_mem.c:87), widening the exit/exec race window.

Threat model & preconditions

  • Attacker position: Local, unprivileged, default config, procfs mounted.
  • Privileges gained or impact: 1. Self-race / DoS β€” attacker opens /proc/<child>/map, reads with a large buffer (forcing many drop/re-acquire cycles), and has the child loop on exec() or exit() β€” reliably panics the kernel via lockmgr/RB-tree walks on freed memory. 2. Cross-process corruption primitive (combined with DF-0921) β€” attacker reads /proc/<victim>/map while the victim exec's; the UAF reads at procfs_map.c:175-178 and the lockmgr call at :230 operate on attacker-influenceable reused memory. Demonstrated impact is a kernel panic (A:H); privilege escalation via heap grooming of the freed vmspace/entry slab is plausible but not proven in this audit (hence confidence: likely rather than certain).
  • Required config or capabilities: procfs mounted (common).
  • Reachability: open("/proc/<pid>/map"); pread(...) while target exec/exits.

Proof of concept

PoC source: findings/poc/DF-0923/race_map.c, noop.c

Build & run

cc -o race_map race_map.c
cc -o noop noop.c
./race_map

Expected output

A kernel panic within seconds. Typical signatures: - mutex/lockmgr corruption diagnostics. - NULL deref in vm_map_lookup_entry/RB_NEXT. - "freed vmspace" symptoms.

Capture the panic from dmesg / serial console into run.log. To prove the dangling-ba read specifically, run with WITNESS/INVARIANTS and capture the vm_map_entry UAF diagnostic. The race is tightest when the reader's pread forces many iterations (large resid) so the unlock window at procfs_map.c:143 is hit repeatedly.

Impact

Reliable kernel panic from an unprivileged local user. Plausible escalation to cross-process memory corruption via slab grooming of the freed vmspace/entry (not proven here; flagged for PoC-runner verification).

Two independent corrections, both required:

  1. Hold a reference on the vmspace for the whole call so the cached map cannot be freed out from under the reader β€” mirror procfs_rwmem().
  2. Read object fields through the already-held obj (held at procfs_map.c:138), never through ba, since ba is only valid under the map lock which is dropped per iteration.
  3. (Optional) Refuse P_WEXIT/P_INEXEC targets, again mirroring procfs_rwmem().
--- a/sys/vfs/procfs/procfs_map.c
+++ b/sys/vfs/procfs/procfs_map.c
@@ -60,8 +60,10 @@
    struct proc *p = lp->lwp_proc;
    ssize_t buflen = uio->uio_offset + uio->uio_resid;
    struct vnode *vp;
    char *fullpath, *freepath;
-   int error;
-   vm_map_t map = &p->p_vmspace->vm_map;
+   int error;
+   struct vmspace *vm;
+   vm_map_t map;
    vm_map_entry_t entry;
    struct sbuf *sb = NULL;
    unsigned int last_timestamp;
@@ -70,6 +72,15 @@
    if (uio->uio_offset < 0 || uio->uio_resid < 0 || buflen >= INT_MAX)
        return EINVAL;
+   /*
+    * Pin the target vmspace: a concurrent exec/exit can otherwise swap
+    * p->p_vmspace and free our cached map across the per-iteration unlock.
+    */
+   vm = p->p_vmspace;
+   if (p->p_stat == SIDL || p->p_stat == SZOMB ||
+       (p->p_flags & (P_WEXIT | P_INEXEC)) || vmspace_getrefs(vm) < 0)
+       return EFAULT;
+   vmspace_hold(vm);
+   map = &vm->vm_map;
    sb = sbuf_new (sb, NULL, buflen+1, 0);
    if (sb == NULL) {
+       vmspace_drop(vm);
        return EIO;
    }
@@ -173,9 +184,10 @@
-       if (ba->object) {
-           flags = ba->object->flags;
-           ref_count = ba->object->ref_count;
-       }
+       /* Read through the held obj; `ba` may be stale once the map
+        * read lock was dropped at vm_map_unlock() above. */
+       if (obj) {
+           flags = obj->flags;
+           ref_count = obj->ref_count;
+       }
        vm_object_drop(obj);
@@ -241,6 +253,7 @@
    vm_map_unlock_read(map);
+   vmspace_drop(vm);
    if (sbuf_finish(sb) == 0)

Note: the vmspace_hold fix protects the vmspace struct (and thus map), while switching ba->object to obj protects the per-entry reads; applying only one of the two still leaves a dangling dereference.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0923 Β· 14 files
FileTypeDescriptionSize
race_map.c trigger-source racer: victim tight-loops execve(self); N readers tight-loop read(/proc/<victim>/map) to race procfs_domap's per-iteration unlock window 5.1 KB view raw
build.sh build-script cc -O2 -o race_map race_map.c 116 B view raw
run.sh run-script ./race_map [seconds] [nreaders] 357 B view raw
build.log build-log clean racer compile output on guest 67 B view raw
run.log run-log 3x UAF reproductions on #0 with full panic signatures 2.6 KB view raw
panic.txt panic-signature Fatal trap 12 in vm_map_rb_tree_RB_NEXT (the UAF) 744 B view raw
env.txt environment guest uname/cc/kern.version/hardening 715 B view raw
fix.diff suggested-fix git-apply-able: vmspace_hold/drop mirroring procfs_rwmem, p_token held for LIFO order, EFAULT checks before sbuf_new 2.9 KB view raw
fix_build.log fix-build-log full make -j6 nativekernel output for the single-fix kernel 5.6 MB ↓ download
fix_run.log fix-run-log before/after contrast: #0 panics, #1 survives 90s/6r + 60s/8r clean 2.6 KB view raw
VERDICT.md verdict full narrative: mechanism, reproduction, Phase-6 escalation analysis (read-only UAF hard blocker), the fix and its validation 6.9 KB ↓ raw
README.md readme human-facing build/run/expected + threat model 2.4 KB ↓ 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 human-facing build/run/expected + threat model
↓ download raw

DF-0923 β€” PoC: UAF of vmspace/vm_map (and vm_map_backing) across the per-iteration unlock in /proc/<pid>/map

Bug (one line)

procfs_domap() (sys/vfs/procfs/procfs_map.c) caches map = &p->p_vmspace->vm_map without vmspace_hold(), drops the vm_map read lock per iteration (:142-143), then re-locks/iterates the now-stale cached map (:230, :89). A concurrent execve()/exit() on the target frees the vmspace β†’ use-after-free β†’ kernel panic.

Threat model

Unprivileged local user. /proc/<pid>/map is -r--r--r-- (world-readable). The maxx user (uid 1001, not in wheel) forks a child that tight-loops execve (freeing/replacing its vmspace) and reads /proc/<child>/map in a tight loop, racing the per-iteration lock-drop window.

Build & run

cc -O2 -o race_map race_map.c
./race_map [seconds] [nreaders]      # default 30 s, 4 readers

Run as an unprivileged user (e.g. maxx). The race is statistical but, on the unpatched kernel, panics within seconds every time. The PoC is self-contained: the --victim mode re-execs itself to drive vmspace churn; the --reader mode hammers /proc/<victim>/map.

Expected output

  • Unpatched kernel (#0): kernel panic within seconds (guest dies; ssh drops): Fatal trap 12: page fault while in kernel mode fault virtual address = 0x8 Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax
  • Patched kernel (#1, fix.diff applied): runs to completion cleanly: [parent] survived Ns without panic; tearing down [parent] done (no panic observed this run)

Notes

  • The reader uses a 4 KB read buffer deliberately (see the comment in race_map.c): procfs_domap sizes its sbuf from uio_offset + uio_resid, and a large resid under many parallel readers independently trips sbuf: malloc limit exceeded β€” an unrelated pre-existing issue that would mask the UAF. 4 KB still drives procfs_domap through every map entry (every per-iteration unlock window), which is all the UAF race needs.
  • Escalation to uid=0 is not achievable: read-dominant UAF of a dedicated-slab object (vmspace_cache); no attacker-controlled write to a victim object. See VERDICT.md for the full Phase-6 analysis.
  • The validated fix is fix.diff (add vmspace_hold/vmspace_drop mirroring procfs_rwmem, keep p_token held for LIFO token order, EFAULT checks before sbuf_new to avoid a leak).
VERDICT.md verdict full narrative: mechanism, reproduction, Phase-6 escalation analysis (read-only UAF hard blocker), the fix and its validation
↓ download raw

DF-0923 β€” VERDICT

Verdict

REPRODUCED β€” reliable kernel panic (DoS) from an unprivileged user. The use-after-free of vm_map (embedded in vmspace) cached in procfs_domap() is real, deterministic, and triggered by an unprivileged local user (maxx, uid 1001). Fix validated on a single-fix kernel (Phase 8 passed: panic gone). Escalation to uid=0 is not achievable: the primitive is a read-dominant UAF of a dedicated-slab object (vmspace_cache); see "Exploit chain / escalation" below.

The bug (confirmed path:line)

procfs_domap() in sys/vfs/procfs/procfs_map.c:

Line Code Problem
65 vm_map_t map = &p->p_vmspace->vm_map; caches map without vmspace_hold(). Compare procfs_rwmem() at procfs_mem.c:84-94 which does vmspace_hold(vm) before caching.
86 vm_map_lock_read(map); only lockmgr(&map->lock, LK_SHARED) (vm_map.h:484) β€” does NOT touch vm_holdcnt.
87 lwkt_reltoken(&p->p_token); the target's proc token is gone for the whole scan.
142-143 last_timestamp = map->timestamp; vm_map_unlock(map); per-iteration lock drop β€” opens the race window.
175-178, 216 ba->object->flags, ba->object->ref_count, ba->object stale ba dereferenced while unlocked.
230 vm_map_lock_read(map); re-locks the cached map β€” UAF if the vmspace was freed.
89 / RB_FOREACH vm_map_rb_tree_RB_NEXT advances entry walks freed/invalid RB nodes β€” this is where it faults.

A concurrent execve() on the target calls vmspace_exec() (sys/vm/vm_map.c:4298) which does vmspace_rel(oldvmspace) at :4330. With no hold/ref held by procfs_domap, the old vmspace (and its embedded vm_map, including map->lock and the RB tree) is freed (objcache_put(vmspace_cache, vm) at vm_map.c:543). The reader's re-lock/iterate at :230/:89 then operates on freed memory => UAF => panic. exit() is also sufficient: kern_exit.c:432-433 releases p_token around vmspace_relexit(vm), so the vmspace can be freed even while a reader holds p_token.

Reproduction

PoC: race_map.c β€” parent forks a victim that tight-loops execve(self,"--victim") (each exec replaces/frees the vmspace) and N readers that tight-loop open()+read() of /proc/<victim>/map (entering procfs_domap). 4 KB read buffer (deliberately small β€” see comment in the source β€” to avoid tripping an unrelated sbuf malloc-limit issue that would mask the UAF).

Result on default GENERIC #0 (INVARIANTS ON): deterministic kernel panic within seconds, every run, identical signature:

Fatal user address access from kernel mode from race_map at ffffffff809a2540
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8
current process = <reader pid>
Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax   <- RB_FOREACH through freed vm_map

Reproduced 3Γ— (4 readers/25 s and small-buffer variants). Guest down each time.

Exploit chain / escalation (Phase 6)

Outcome: blocked by a valid hard blocker β€” read-only UAF of a dedicated-slab object; no attacker-controlled write to a victim is derivable. Impact = panic.

  • Primitive: the reader's operations on the freed/stale map are read-dominant: ba->object reads (:175-178,216), map->timestamp read (:142,236), vm_map_lookup_entry/RB walk (:238, :89). The only write to freed memory is vm_map_lock_read(map) at :230 = lockmgr LK_SHARED, which increments the lockmgr shared-count at a fixed offset (offsetof(struct vm_map, lock) within the vmspace-embedded map) by a fixed delta (+1). It is not attacker-controlled in value or target.
  • Slab: vmspace uses a dedicated objcache (vmspace_cache, vm_map.c:121,233). A freed vmspace is returned to vmspace_cache and is reused only by vmspace_alloc() β€” never by struct file/ucred/a function-pointer object. Cross-type slab grooming into the freed slot is therefore impossible.
  • Consequence of the lone write: when the freed slot is reused by a new vmspace (the only possible reuser), map->lock is a valid lockmgr lock β€” the reader's LK_SHARED just takes a shared lock on a stranger's valid vmspace (logic confusion + a cross-process map info-leak into the reader's sbuf). No corruption; certainly no controlled write to a victim object.
  • No chain attempted is justified because there is no write primitive to convert. This is Phase 6 valid hard blocker #1 ("genuinely read-only").
  • Secondary impact: a cross-process info leak of another user's /proc/<pid>/map contents (kernel pointers, vnode paths) if the reader catches a slot reused by a victim's vmspace β€” bounded, not controllable, lower severity than the DoS.

The fix (fix.diff β€” validated)

Mirror procfs_rwmem(): vmspace_hold(vm) before caching map, vmspace_drop(vm) after the scan. Two DragonFly-specific constraints shaped the exact patch:

  1. lwkt tokens are a strict LIFO stack (sys/kern/lwkt_token.c:828-857, assertion at :842-853: lwkt_reltoken pops the top ref and requires it to match). vmspace_hold() acquires vm_map.token on top of p_token, so vm_map.token must be released first. Therefore p_token is held for the entire function (the original lwkt_reltoken(&p->p_token) at :87 and the re-acquire at :248 are removed), and vmspace_hold/vmspace_drop nest cleanly inside p_token's scope.
  2. p_token alone does not pin the vmspace: kern_exit.c:432-433 releases p_token around vmspace_relexit(). The vmspace_hold (which bumps vm_holdcnt) is what actually prevents termination.
  3. The vmspace-validity EFAULT checks (SIDL/SZOMB, P_WEXIT/P_INEXEC, vmspace_getrefs<0, mirroring procfs_rwmem:85-88) are placed before sb = sbuf_new() so the EFAULT paths do not leak the sbuf (an earlier fix iteration that put them after sbuf_new leaked the sbuf on every exec and tripped sbuf: malloc limit exceeded).

#include <vm/vm_extern.h> is added (declares vmspace_hold/vmspace_drop/ vmspace_getrefs; already included by procfs_mem.c).

This fix supersedes the finding markdown's initial proposal (which suggested vmspace_hold/vmspace_drop without addressing the LIFO token order, the p_token-held-throughout requirement, or the sbuf-leak hazard).

Phase 8 β€” fix validation

Kernel PoC Result
#0 unpatched baseline (Jul 2) ./race_map 25 4 PANIC vm_map_rb_tree_RB_NEXT within seconds (Γ—3)
#1 single-fix (Jul 7 12:07, sha db4fb534…) ./race_map 90 6 survived 90 s, RUN_EXIT=0, guest up, no panic
#1 single-fix ./race_map 60 8 survived 60 s, RUN_EXIT=0, guest up, no panic

Clean before/after => fix_status = fixed. (Two intermediate fix iterations failed first: v1 tripped lwkt_reltoken: illegal release (LIFO); v2 tripped sbuf: malloc limit exceeded (leaked sbuf). v3 = shipped fix.diff.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. The small-buffer racer deterministically panics the unpatched #0 baseline ('Stopped at vm_map_rb_tree_RB_NEXT') within seconds (3/3 runs, guest down), and does NOT panic the single-fix #1 kernel under equal-or-heavier racing: 90s/6 readers RUN_EXIT=0 guest-up, and 60s/8 readers RUN_EXIT=0 guest-up, boot.log panic grep empty on both. Clean before/after => fix.diff closes DF-0923. Two intermediate fix iterations failed first and were corrected: v1 released p_token mid-function -> 'lwkt_reltoken: illegal release' (LIFO violation); v2 placed EFAULT checks after sbuf_new -> leaked the sbuf on every P_INEXEC hit -> 'sbuf: malloc limit exceeded'. v3 (shipped fix.diff) is balanced on every return path.

BEFORE (#0 unpatched): 'Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax' panic within seconds (3/3 runs, guest down). AFTER (#1 single-fix): '[parent] survived 90s without panic; tearing down / RUN_EXIT=0' (6 readers) and '[parent] survived 60s without panic / RUN_EXIT=0' (8 readers), guest up, no panic in boot.log. => UAF gone.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 7 12:07:42 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (sha256 /boot/kernel/kernel = db4fb5345a9e7f04dac68c61c10a9d9acdd0b8109c77dd3b37a2ef57c0b4dff6)

Confirmed kernel references

Detail

Exploit chain

NOT ESCALATABLE to uid0 -- valid Phase-6 hard blocker #1 (genuinely read-only primitive). The primitive is read-dominant: stale ba->object reads (procfs_map.c:175-178,216), map->timestamp reads (:142,236), vm_map_lookup_entry/RB-FOREACH walks (:238,:89). The only write to freed memory is vm_map_lock_read(map) at :230 = lockmgr LK_SHARED, which increments a lockmgr shared-count at a FIXED offset (offsetof(struct vm_map,lock)) by a FIXED delta (+1) -- not attacker-controlled in value or target. Crucially the freed object is a vmspace, which uses a DEDICATED objcache (vmspace_cache, vm_map.c:121,233): a freed slot is reused ONLY by vmspace_alloc(), never by struct file/ucred/function-pointer objects, so cross-type slab grooming into the freed slot is IMPOSSIBLE. When the slot IS reused (by a new vmspace), map->lock is a valid lockmgr lock -- the reader's LK_SHARED merely locks a stranger's valid vmspace (logic confusion + a bounded cross-process map info-leak), no corruption. No write primitive to a victim is derivable, so no chain was attempted and none is justified. Honest impact = reliable panic (DoS) from an unprivileged user.

Evidence (decisive lines)

UNPATCHED #0 (3 reproductions, identical): 'Fatal user address access from kernel mode from race_map at ffffffff809a2540 / Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x8 / current process = <reader pid> / Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax' (guest down). PATCHED #1: '[parent] survived 90s without panic; tearing down / RUN_EXIT=0' (6 readers) and '[parent] survived 60s without panic / RUN_EXIT=0' (8 readers), guest up, boot.log panic grep empty.

PoC changes

Rewrote the broken scaffolding race_map.c into a clean 3-mode racer (parent/--victim/--reader): the victim tight-loops execve(self,'--victim') to drive vmspace_exec()->vmspace_rel churn, N readers tight-loop open()+read() of /proc//map to race procfs_domap's per-iteration unlock window. Used a deliberately SMALL 4KB read buffer (documented in-source): procfs_domap sizes its sbuf from uio_offset+uio_resid, so a large resid under many readers independently trips an unrelated pre-existing 'sbuf: malloc limit exceeded' that masks the UAF; 4KB still drives every per-entry unlock window. Added build.sh/run.sh, VERDICT.md, manifest.json, env.txt, and the validated fix.diff.

Verified recommended fix

Mirror procfs_rwmem(): add vmspace_hold(vm) before caching map and vmspace_drop(vm) after the scan in procfs_domap (procfs_map.c), plus #include . Three DragonFly-specific corrections over the naive fix: (1) lwkt tokens are a strict LIFO stack (lwkt_token.c:828-857), so since vmspace_hold acquires vm_map.token on top of p_token, p_token must be held for the WHOLE function (remove the lwkt_reltoken at :87 and the re-acquire at :248) and vmspace_drop (releasing vm_map.token) goes before any p_token release; (2) p_token alone does NOT pin the vmspace because kern_exit.c:432-433 drops p_token around vmspace_relexit -- the vmspace_hold (vm_holdcnt) is what actually prevents termination; (3) the vmspace-validity EFAULT checks (SIDL/SZOMB, P_WEXIT/P_INEXEC, vmspace_getrefs<0) must run BEFORE sb=sbuf_new() so the EFAULT paths don't leak the sbuf. This supersedes the finding markdown's initial proposal.

Verdict

REPRODUCED. The UAF is real and confirmed path:line: procfs_domap() caches map=&p->p_vmspace->vm_map at procfs_map.c:65 WITHOUT vmspace_hold() (contrast procfs_rwmem() at procfs_mem.c:93 which does vmspace_hold), drops the vm_map read lock per-iteration at procfs_map.c:142-143, then re-locks/iterates the now-stale cached map at :230 and :89. A concurrent execve() on the target calls vmspace_exec()->vmspace_rel(oldvmspace) at vm_map.c:4330; with no hold held by procfs_domap the old vmspace (and its embedded vm_map + RB tree) is freed to the dedicated vmspace_cache (vm_map.c:543). The reader's RB_FOREACH then walks freed memory. exit() also suffices (kern_exit.c:432-433 releases p_token around vmspace_relexit). Confirmed by a deterministic panic on default GENERIC #0 (INVARIANTS ON) triggered by unprivileged user maxx: 'Fatal trap 12 ... Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax' (the RB_FOREACH at procfs_map.c:89), reproduced 3x, identical signature, guest down each time.