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)
PoC verification
Evidence pack
findings/poc/DF-3239 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 fullread()s of /proc/child/map across each child's exit; 508,000+ rounds in 360 s (~1,400 rounds/s; each round performs dozens ofvn_fullpath()calls that executep->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)
- procfs_map.c:87 releases
p->p_tokenfor the whole walk; procfs_map.c:143 releases the vm_map lock per entry. - procfs_map.c:181
vn_fullpath(p, vp, &fullpath, &freepath, 1)withpthe target proc (only PHOLD'd; PHOLD does not stop exit1). - vfs_cache.c:5188
kmalloc(MAXPATHLEN, M_TEMP, M_WAITOK)β may block β then vfs_cache.c:5194fd_nrdir = p->p_fd->fd_nrdir;β no token, no spin, no NULL check. (DF-3235 capture-pointer-then-block class.) - kern_exit.c:382
fdfree(p, NULL)runs during exit1 regardless of the reader's PHOLD; kern_descrip.c:2722-2726 storesp->p_fd = NULLunder p_spin, then kern_descrip.c:2756kfree(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_testableNo 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
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.
No comments yet.