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

p_fd NULL-deref/UAF in vn_fullpath()->cache_fullpath() against an exiting target read via /proc/pid/map

Summary

procfs_domap drops p->p_token (:87) and the map lock (:143), then calls vn_fullpath(p, vp, ..., 1) (:181). cache_fullpath() does an unguarded 'fd_nrdir = p->p_fd->fd_nrdir;' (vfs_cache.c:5194) immediately after a potentially-blocking kmalloc(MAXPATHLEN, M_WAITOK) - the DF-3235 capture-pointer-then-block class pointed at struct filedesc. The target's exit1 -> fdfree sets p->p_fd = NULL (kern_descrip.c:2722-2726) and kfrees the filedesc regardless of the reader's PHOLD (PHOLD only stalls the reaper). Reader seeing NULL -> NULL+offset panic; loading the old pointer just before disconnect -> UAF read of freed memory (pointer compares downstream). HONEST verdict: 508k+ rounds / ~30M in-window vn_fullpath executions over 6 minutes, no panic (two-instruction gap; DF-3238's much wider crash competes in the same overlap and dominated testing) - not_reproduced, finding stands on the line-accurate source chain. Unpriv (map nodes world-readable, no gate). Every vn_fullpath(p,...) caller in the kernel shares the vfs_cache.c:5194 exposure. Fix: p_token around vn_fullpath + NULL guard in cache_fullpath.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3239 Β· 8 files
FileTypeDescriptionSize
df3239_pfd_exit_race.c β€” 2.9 KB view raw
build.sh β€” 114 B view raw
build.log β€” 13 B view raw
run.log β€” 399 B view raw
env.txt β€” 87 B view raw
fix.diff β€” 967 B view raw
VERDICT.md β€” 2.7 KB ↓ raw
verdict.json β€” 2.7 KB view raw
VERDICT.md
↓ download raw

DF-3239 VERDICT β€” NOT REPRODUCED (bounded attempt; defect source-proven)

What was run

  • PoC df3239_pfd_exit_race.c (uid 1001 maxx) on DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (stock): parent forks children that map 16 file-backed regions then _exit(0); parent hammers 32 full read()s of /proc/child/map across each child's exit; 508,000+ rounds in 360 s (~1,400 rounds/s; each round performs dozens of vn_fullpath() calls that execute p->p_fd->fd_nrdir, vfs_cache.c:5194).

Result

  • No panic, no crash, no dmesg anomalies, guest stayed up, run exited 0.
  • Honest classification: not_reproduced / reproduced=0 / impact=none.

Why the finding still stands (source chain, line-accurate)

  1. procfs_map.c:87 releases p->p_token for the whole walk; procfs_map.c:143 releases the vm_map lock per entry.
  2. procfs_map.c:181 vn_fullpath(p, vp, &fullpath, &freepath, 1) with p the target proc (only PHOLD'd; PHOLD does not stop exit1).
  3. vfs_cache.c:5188 kmalloc(MAXPATHLEN, M_TEMP, M_WAITOK) β€” may block β€” then vfs_cache.c:5194 fd_nrdir = p->p_fd->fd_nrdir; β€” no token, no spin, no NULL check. (DF-3235 capture-pointer-then-block class.)
  4. kern_exit.c:382 fdfree(p, NULL) runs during exit1 regardless of the reader's PHOLD; kern_descrip.c:2722-2726 stores p->p_fd = NULL under p_spin, then kern_descrip.c:2756 kfree(fdp, M_FILEDESC). => reader sees NULL -> kernel NULL+offset load -> panic; or sees the old pointer -> UAF read of freed filedesc (only pointer-compared after).

Why it did not fire empirically

The NULL/UAF window is the two-instruction gap between loading p->p_fd and loading ->fd_nrdir (the M_WAITOK kmalloc normally returns from cache without sleeping). DF-3238's crash (which fires in the same overlap of a map read against process exit) has a per-window window and dominated the race space during testing; DF-3238 has since been fix-validated.

Fix recommendation (fix.diff)

Snapshot the root nchandle under the target's p_token (or take p_token around the vn_fullpath call) AND NULL-check p->p_fd in cache_fullpath. fix.diff carries both hunks. fix_status=not_testable (no baseline reproduction to diff against; the fix compiles by inspection against the same lines cited).

Artifacts

  • run.log β€” decisive 6-minute run summary (508k rounds, exit 0)
  • build.log β€” clean compile
  • fix.diff β€” two-hunk fix (procfs_map.c + vfs_cache.c)

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

not_testable
baseline no→ patch + rebuild →patched clean

No baseline reproduction to diff against; fix.diff is line-accurate against the cited code but was not kernel-build validated for this finding.

fix.diff
↓ fix.diffper-fix-DF-3239

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log: 508k rounds, EXIT=0, guest up throughout', 'source chain: procfs_map.c:87,143,181 -> vfs_cache.c:5188,5194 -> kern_exit.c:382 -> kern_descrip.c:2722-2726,2756', 'VERDICT.md: full line-accurate reasoning and why the window is 2 instructions wide']

PoC changes

Authored fresh: child signals readiness via pipe so the parent's read loop starts before the child's exit1 sets P_POSTEXIT (pfs_pfind would otherwise EINVAL the reads).

Verified recommended fix

Serialize the vn_fullpath call against fdfree (p_token) and NULL-check p->p_fd in cache_fullpath (see fix.diff).

Verdict

Source-proven race, empirically not hit in a bounded attempt. procfs_domap() drops p_token (procfs_map.c:87) and the map lock (143), then vn_fullpath(p,...) (181) reaches cache_fullpath's unguarded p->p_fd->fd_nrdir (vfs_cache.c:5194) right after a potentially-blocking kmalloc (5188); a concurrently-exiting target NULLs and kfrees p->p_fd via exit1->fdfree (kern_exit.c:382, kern_descrip.c:2722-2756) irrespective of the reader's PHOLD. On the guest, 508,000+ fork/read/exit rounds over 6 minutes (~30M in-window vn_fullpath executions) produced no panic (the p_fd load-to-deref gap is a few instructions; DF-3238's much wider crash competes in the same overlap). Defect stands on the source chain; fix.diff (p_token around vn_fullpath + NULL check in cache_fullpath) provided.