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)
PoC verification
Evidence pack
findings/poc/DF-3238 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-3238 VERDICT β REPRODUCED (panic), fix validated
What was run
- PoC:
df3238_mapwalk_null.cβ unprivileged (uid 1001 maxx), single process, two pthreads: reader hammersread()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->timestampchange (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/belowe_startis gone it setsreentry = NULL, which is assigned toentry. - tree.h:1037 RB_FOREACH increments
entry = RB_NEXT(name, root, entry)=vm_map_rb_tree_RB_NEXT(NULL); tree.h:817RB_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
fixedBaseline 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
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.
No comments yet.