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

NULL-deref kernel panic: procfs_domap() ignores vm_map_lookup_entry() failure and feeds NULL to RB_NEXT() during /proc/pid/map revalidation

Summary

procfs_domap drops the vm_map read lock per entry (:143) to format lines and resolve paths; on re-lock it revalidates the RB_FOREACH iterator when map->timestamp changed, but ignores vm_map_lookup_entry()'s return. When the entry just processed and everything below it was unmapped in the window, the lookup finds no predecessor and sets *entry = NULL (vm_map.c:1215-1232); the RB_FOREACH increment then evaluates RB_NEXT(...,NULL) whose first statement dereferences elm (tree.h:817) - kernel-mode load at 0x8, fatal trap, DDB halt. REPRODUCED first run (<4 min) unpriv: reader loops lseek(0)+read() on /proc/self/map while churn thread mmaps/munmaps a 32-entry file-backed block below the text base (vnode objects widen the per-entry window via vn_fullpath): 'fault virtual address = 0x8', 'Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax'. Also reachable non-maliciously: reading /proc/pid/map of any process while it exits (stage-1 vm_map_remove over the entire user map under the reader). Fix validated (one-line break on NULL reentry): identical PoC 240s/596k iterations, no panic. Ceiling panic/DoS - fixed NULL+8 address, no uid=0 route. Distinct from DF-0923 (revalidation logic, not freed object). Note: DF-0921/0922/0923/0924 all remain UNFIXED in-tree (residual assessed, not re-reported).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3238 Β· 11 files
FileTypeDescriptionSize
df3238_mapwalk_null.c β€” 3.9 KB view raw
build.sh β€” 122 B view raw
build.log β€” 13 B view raw
run.log β€” 331 B view raw
run.2.log β€” 607 B view raw
panic.txt β€” 789 B view raw
env.txt β€” 229 B view raw
fix.diff β€” 732 B view raw
fix_validation.log β€” 662 B view raw
VERDICT.md β€” 3.6 KB ↓ raw
verdict.json β€” 3.3 KB view raw
VERDICT.md
↓ download raw

DF-3238 VERDICT β€” REPRODUCED (panic), fix validated

What was run

  • PoC: df3238_mapwalk_null.c β€” unprivileged (uid 1001 maxx), single process, two pthreads: reader hammers read() on /proc/self/map; churner mmaps/munmaps a 32-entry file-backed block at 0x100000 (lowest mappings, vnode objects -> wide per-entry unlock window via vn_fullpath).
  • Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (stock, Jul 2 2026), QEMU/KVM, 6 vCPUs.

Baseline result (stock kernel #0)

Panic. Serial console (dfbsd-qemu/boot.log, copy in panic.txt):

Fatal user address access from kernel mode from df3238_mapwalk_n at ffffffff809a2540
Fatal trap 12: page fault while in kernel mode
cpuid = 5; lapic id = 5
fault virtual address  = 0x8
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff809a2540
current process        = 995
CPU5 stopping CPUs: 0x0000001f
Stopped at  vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax
db>

0x8 = offsetof(struct vm_map_entry, rb_entry.rbe_right), %rdi = NULL: exactly RB_NEXT(vm_map_rb_tree, root, NULL) from the RB_FOREACH increment after procfs_domap() stored a failed vm_map_lookup_entry() result (*entry = NULL, sys/vm/vm_map.c:1215-1232) into the loop variable at sys/vfs/procfs/procfs_map.c:236-240. Reproduced on the first run (< ~4 min).

Root cause (source, line-accurate)

  • procfs_map.c:142-143: per-entry vm_map_unlock(map) (vn_fullpath + sbuf_printf run unlocked).
  • procfs_map.c:236-240: on map->timestamp change (any munmap bumps it, vm_map.h:440-461), vm_map_lookup_entry(map, e_start, &reentry)'s return value is ignored; when everything at/below e_start is gone it sets reentry = NULL, which is assigned to entry.
  • tree.h:1037 RB_FOREACH increments entry = RB_NEXT(name, root, entry) = vm_map_rb_tree_RB_NEXT(NULL); tree.h:817 RB_RIGHT(elm, field) dereferences elm -> read at 0x8 -> fatal kernel-mode fault.

Exploitability

Unprivileged local DoS (hard panic to DDB; all CPUs stopped). Not an escalation primitive: the dereferenced address is fixed NULL+8; page 0 is never mapped/predictable. Same crash is reachable against ANY dying process whose map is being read (exit1's stage-1 vmspace teardown empties the whole tree under the reader, kern_exit.c:433 -> vm_map.c:494-496), so "read /proc/pid/map of a process that exits" is a realistic accidental trigger, not just a synthetic churn race.

Fix validation

  • fix.diff: if (reentry == NULL) break; in the revalidation block.
  • Applied in guest /usr/src, make nativekernel KERNCONF=X86_64_GENERIC (BUILD_RC=0) + make installkernel, rebooted into #1: Wed Sep 9 04:36:32 UTC 2026.
  • Exact same PoC re-run for 240 s: 596,000+ read iterations, reader+churner healthy, POC_EXIT=0 (timeout), guest up, dmesg clean. No panic. Baseline panicked on the first run. -> fix_status=fixed.

Artifacts

  • panic.txt β€” serial console panic block (kernel #0 baseline)
  • run.log β€” baseline run narrative (ssh session died at panic)
  • run.2.log β€” patched-kernel run log (no panic)
  • fix_validation.log β€” build/install/reboot/re-run transcript summary
  • fix.diff β€” git-apply-able unified diff (validated)
  • env.txt β€” guest uname

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline kernel #0 panics on first PoC run (panic.txt). Patched kernel #1 (reentry==NULL -> break) survives the identical PoC for 240s / 596k+ iterations with no panic; guest healthy. fix validated.

fix_validation.log; run.2.log; fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 9 04:36:32 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["panic.txt: 'Fatal user address access from kernel mode from df3238_mapwalk_n', fault virtual address = 0x8, Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax (stock kernel #0)", 'run.2.log: patched kernel #1 (Wed Sep 9 04:36:32 2026), same PoC, 596k+ reader iterations over 240s, POC_EXIT=0, guest up', 'fix_validation.log: make nativekernel BUILD_RC=0, installkernel INSTALL_RC=0, reboot, re-run clean', 'source chain: sys/vfs/procfs/procfs_map.c:236-240 + sys/vm/vm_map.c:1215-1232 + sys/sys/tree.h:815-817,1037']

PoC changes

Seed sketch (none existed) authored fresh: reader must lseek(fd,0) per iteration (fd offset otherwise EOFs); churn block placed below the non-PIE text base at 0x100000 with alternating file offsets so entries do not merge; one big munmap deletes all lowest entries during the reader's per-entry unlock window.

Verified recommended fix

In the revalidation block, break out of the RB_FOREACH when vm_map_lookup_entry() yields a NULL predecessor instead of assigning it to the iterator (see fix.diff).

Verdict

Unprivileged two-thread process (reader on /proc/self/map + munmap churn of the lowest file-backed mappings) panics the stock kernel: procfs_domap's timestamp revalidation ignores vm_map_lookup_entry's failure and stores *entry=NULL into the RB_FOREACH iterator; RB_NEXT(NULL) executes movq 0x8(%rdi),%rax -> fatal kernel-mode fault at address 0x8, DDB halt. Reproduced on the first run against DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC. Same crash is reachable against any dying process whose map is read (exit stage-1 teardown empties the tree under the reader). Panic is a hard local DoS; not an escalation primitive (fixed NULL+8 address). fix.diff (break on reentry==NULL) was built as kernel #1 and the identical PoC ran 240s/596k iterations with no panic: fix validated.