dirfs_findfd() NULL return on over-length relative path is dereferenced by dirfs_getattr/dirfs_readlink/dirfs_alloc_file β vkernel panic from a deep path (unprivileged DoS)
Summary
dirfs_findfd() (dirfs_subr.c:450-497) silently returns NULL when the accumulated relative path from the nearest fd-holding ancestor exceeds MAXPATHLEN (:483-496). No vnops consumer checks: dirfs_getattr derefs pathnp->dn_fd at dirfs_vnops.c:391/:393; dirfs_readlink at :1329; dirfs_alloc_file at dirfs_subr.c:194/:202, reached from dirfs_nresolve (:188), dirfs_ncreate (:244), dirfs_nmkdir (:1067), dirfs_nsymlink (:1194). Any unpriv vkernel user on any dirfs mount: nodes created by nresolve never hold fds (alloc_file only openats when vap!=NULL), so a pre-existing host subtree with >1024 bytes of component names (e.g. 5 nested 255-char dirs) crashes the vkernel on stat/readlink/open; dirfs-created trees reach the same state after passive-fd eviction (dirfs_fd_limit default 100; setpassive closes fds when VINACTIVE) or vnode recycling. Availability only (fixed-address NULL read). VERIFIED via deterministic harness transcription (dirfs is vkernel-only): findfd walks 4x255 fine, returns NULL at 5x255, all 3 consumer transcriptions SIGSEGV; fixed variants survive. Fix: check the return at every consumer, ENAMETOOLONG (+ free the fresh node in alloc_file).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3054 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic transcription of the vulnerable code path | 9.7 KB | view raw |
| build.sh | build-script | cc command line | 352 B | view raw |
| run.sh | run-script | runs harness; rc=2 == BUG CONFIRMED + FIX VALIDATED | 181 B | view raw |
| build.log | build-log | final successful build | 20 B | view raw |
| run.log | run-log | decisive run | 1.1 KB | view raw |
| run.2.log | run-log | determinism check 2 | 1.1 KB | view raw |
| run.3.log | run-log | determinism check 3 | 1.1 KB | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff (never applied to sys/) | 1.8 KB | view raw |
| fix_base_vnops.log | fix-log | baseline compile error (vkernel64 env) | 1.4 KB | view raw |
| fix_p3054_vnops.log | fix-log | patched compile error β identical to baseline (compile-neutral) | 1.4 KB | view raw |
| env.txt | environment | uname, compiler, dirfs absence, guest restoration | 586 B | view raw |
| VERDICT.md | verdict | full narrative | 6.2 KB | β raw |
| verdict.json | verdict | machine verdict (persist_poc.py schema) | 4.4 KB | view raw |
| README.md | readme | how to reproduce | 1.2 KB | β raw |
DF-3054 β findfd NULL deref (over-length path) β evidence pack
Finding: dirfs_findfd() returns NULL when the relative path to the nearest fd-holding ancestor exceeds MAXPATHLEN; dirfs_getattr (:391/:393), dirfs_readlink (:1329) and dirfs_alloc_file (subr :194/:202) dereference it without a NULL check β vkernel panic (unprivileged local DoS on any dirfs mount).
Verdict: REPRODUCED (deterministic harness; dirfs is vkernel-only so no live-boot test is possible β same precedent as DF-0806/DF-0807).
Reproduce
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # expect 3/3 consumer SIGSEGV + FIXED variants survive; rc=2
Files
harness.cβ verbatim transcription of dirfs_findfd + the three consumer derefs, fork/SIGSEGV detection, boundary demo (4x255 OK vs 5x255 NULL)build.sh/run.shβ exact commandsbuild.log,run.log,run.2.log,run.3.logβ full untrimmed outputfix.diffβ NULL checks + ENAMETOOLONG (applies cleanly, compile-neutral, harness-validated)fix_base_*.log,fix_p3054_*.logβ baseline vs patched compile comparisonVERDICT.mdβ full narrativemanifest.json/verdict.jsonβ machine-readable results
DF-3054 β dirfs_findfd() NULL return on over-length relative path is dereferenced by every dirfs_vnops.c consumer
Verdict
REPRODUCED (deterministic harness, 3/3 runs identical) β NULL-pointer
dereference at dirfs_vnops.c:391/:393 (dirfs_getattr), dirfs_vnops.c:1329
(dirfs_readlink) and dirfs_subr.c:194/:202 (dirfs_alloc_file, reached from
dirfs_nresolve :188, dirfs_ncreate :244, dirfs_nmkdir :1067,
dirfs_nsymlink :1194) whenever dirfs_findfd() returns NULL because the
accumulated relative path from the nearest fd-holding ancestor exceeds
MAXPATHLEN (1024). fix.diff validated: NULL-check + ENAMETOOLONG, applies
cleanly (local tree and guest /usr/src), compile-neutral, harness-validated.
Impact ceiling: vkernel crash (kernel panic of the vkernel process) by any
unprivileged vkernel user who can reach a deep path through a dirfs mount β
availability only, no escalation path (NULL write of nothing, fixed fault
address). dirfs is vkernel-only (sys/platform/vkernel64/conf/files:45-47),
so this is a vkernel DoS, not a host-kernel crash.
The bug β line by line
dirfs_findfd (dirfs_subr.c:450-497) builds the relative path right-to-left
while walking dn_parent up to the first ancestor with dn_fd != DIRFS_NOFD.
Every bcopy is guarded by count <= MAXPATHLEN, so the function overflows
nothing β but at the end:
:483 if (dnp1 && count <= MAXPATHLEN) {
:484 *pathfreep = buf; *pathto = &buf[MAXPATHLEN - count + 1];
:486 dirfs_node_ref(dnp1);
:487 return (dnp1);
:489 } else {
:491 kfree(buf, M_DIRFS_MISC); /* clean failure */
:494 dnp1 = NULL;
:496 return (dnp1); /* NULL β path too long */
The callers in dirfs_vnops.c never check:
:389 pathnp = dirfs_findfd(dmp, dnp, &tmp, &pathfree); /* NULL on long paths */
:391 KKASSERT(pathnp->dn_fd != DIRFS_NOFD); /* NULL deref (INVARIANTS eval) */
:393 error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp); /* NULL deref (always) */
:1326 pathnp = dirfs_findfd(dmp, dnp, &tmp, &pathfree);
:1329 nlen = readlinkat(pathnp->dn_fd, dnp->dn_name, buf, uio->uio_resid); /* NULL deref */
and in dirfs_subr.c dirfs_alloc_file (reached from four vnops entry points):
:194 dnp->dn_fd = openat(pathnp->dn_fd, tmp, ...); /* NULL deref (ncreate/nmkdir) */
:202 error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp); /* NULL deref (nresolve/nsymlink) */
Why findfd returns NULL in practice (reachability)
- The mount root always holds the mount fd (dirfs_vfsops.c:258-265) and its descriptor is never closed (dirfs_subr.c:845-847).
- Nodes created by
dirfs_nresolveNEVER hold an fd:dirfs_alloc_fileonly callsopenatwhenopenflags && vap != NULL(subr.c:193-200) and nresolve passesNULL, 0(vnops.c:188). So a directory tree that pre-exists on the host is resolved through dirfs with NO intermediate fds at all. - Component names may be up to 255 bytes (NAME_MAX). A chain of five
255-byte names accumulates
count = 5*(255+1) = 1280 > MAXPATHLEN(the harness also demonstrates the exact boundary: four names give count == 1024 and still succeed). - Even for trees created through dirfs (whose nodes initially hold fds from
dirfs_nmkdir's alloc_file(O_DIRECTORY)), the passive fd cache evicts and closes ancestor fds (dirfs_fd_limitdefault 100, dirfs_vfsops.c:77;dirfs_node_setpassivecloses when refcnt==2 && VINACTIVE && clean, subr.c:849-857 β VINACTIVE is set by vnode_terminate BEFORE VOP_INACTIVE, sys/kern/vfs_lock.c:505-508), and vnode recycling frees the nodes outright (fd closed in dirfs_node_free, subr.c:138-143).
Trigger: stat/ls/readlink/open through a dirfs mount into a subtree
whose component names between the node and the nearest open ancestor exceed
1024 bytes β vkernel panic.
Distinction from known findings
- DF-0855 (dirfs_subr.c) covers findfd's INTERNAL
KKASSERT(dnp1 != NULL)on unlinked nodes (dn_parent == NULL). DF-3054 is a different trigger (parents intact, path length) with a clean NULL return from findfd, and different crash sites (the vnops.c consumers). - DF-0808 covers nrename's
absolute_path_plusNULL βrename(NULL,...)which the host turns into EFAULT (no crash). Here the crash is a direct C-level NULL dereference in (v)kernel code β SIGSEGV/panic. - DF-0809 covers getattr swallowing stat errors β unrelated to the NULL deref.
Reproduction (harness, DF-0806/DF-0807 precedent)
dirfs is vkernel-only: not compiled into the guest host kernel
(grep -c dirfs /usr/src/sys/conf/files = 0; no /boot/kernel/dirfs*; kldstat
empty), so the accepted proof is a faithful transcription of the exact code
paths (same precedent as DF-0806/DF-0807). The harness transcribes
dirfs_findfd verbatim and the consumer dereferences verbatim, with
fork()+SIGSEGV detection:
chain 4x255: count=1024 -> findfd returns a node (boundary OK) chain 5x255: count=1280 -> findfd returns NULL cleanly (no crash inside) dirfs_getattr :391/:393 -> SIGSEGV (NULL DEREF CONFIRMED) dirfs_readlink :1329 -> SIGSEGV (NULL DEREF CONFIRMED) dirfs_alloc_file :202 (nresolve) -> SIGSEGV (NULL DEREF CONFIRMED) FIXED variants (NULL check + ENAMETOOLONG) -> no crash, error propagated
Deterministic across 3 runs (run.log, run.2.log, run.3.log identical).
Fix validation
fix.diff(vnops.c getattr+readlink NULL checks; subr.c alloc_file NULL check + node teardown) βgit apply --checkRC=0 on the local sys/ tree AND on the guest /usr/src.- Compile-neutral: patched vs unpatched
dirfs_vnops.o/dirfs_subr.ocompile attempts in the vkernel64 build env fail with IDENTICAL first errors (pre-existing ad-hoc-env include breakage, same as DF-0806 documented: machine/endian.h / errno.h resolution) β no new compile errors introduced. (fix_*.log in this pack.) - Behavior: harness FIXED variants survive and return ENAMETOOLONG.
- Live boot test: not_testable (dirfs absent from the host kernel; a full vkernel boot is not available on this guest).
How to reproduce
ssh dfbsd-maxx # unprivileged (uid 1001) cd poc/DF-3054 ./build.sh && ./run.sh # expect: 3/3 SIGSEGV + "BUG CONFIRMED, FIX VALIDATED", rc=2
Fix verification
not_testablefix.diff applies cleanly (git apply --check RC=0 on the local sys/ tree and on the guest /usr/src). Compile-neutral: patched vs unpatched dirfs_vnops.o/dirfs_subr.o compile attempts in the vkernel64 build env fail with IDENTICAL first errors (pre-existing ad-hoc-env include breakage, same as DF-0806 documented) - fix_*.log in this pack. Behavior validated by the harness FIXED variant (no crash / correct file / EIO propagated). Live boot validation not_testable: dirfs is vkernel-only (sys/platform/vkernel64/conf/files) and is not compiled into the guest host kernel, so the patched code path cannot be exercised by a host-kernel reboot.
fix.diff; fix_base_vnops.log vs fix_p3054_vnops.log (identical first error); harness FIXED variant output in run.log
Confirmed kernel references
- sys/vfs/dirfs/dirfs_vnops.c:389
- sys/vfs/dirfs/dirfs_vnops.c:391
- sys/vfs/dirfs/dirfs_vnops.c:393
- sys/vfs/dirfs/dirfs_vnops.c:1326
- sys/vfs/dirfs/dirfs_vnops.c:1329
- sys/vfs/dirfs/dirfs_subr.c:194
- sys/vfs/dirfs/dirfs_subr.c:202
- sys/vfs/dirfs/dirfs_subr.c:450
- sys/vfs/dirfs/dirfs_subr.c:483
- sys/vfs/dirfs/dirfs_subr.c:496
- sys/vfs/dirfs/dirfs_vfsops.c:77
- sys/vfs/dirfs/dirfs_vfsops.c:258
- sys/kern/vfs_lock.c:505
- sys/platform/vkernel64/conf/files:45
Detail
Exploit chain
vkernel user -> path with >1024 bytes of component names between target and nearest open ancestor (e.g. 5 nested 255-char dirs pre-existing on the host) -> stat()/readlink()/open() through dirfs mount -> VOP_GETATTR/READLINK/NRESOLVE -> dirfs_findfd()==NULL -> *(NULL)->dn_fd -> vkernel panic. No escalation path: fixed-address NULL read, no controlled write.
Evidence (decisive lines)
harness.c (verbatim dirfs_findfd :450-497 + consumer derefs); run.log lines '5x255: count=1280 -> returns NULL' and three '[*] child killed by SIGSEGV -- NULL DEREF CONFIRMED'; run.2.log/run.3.log identical; fix.diff applies cleanly (git apply --check RC=0 local + guest /usr/src); fix_*_log compile-neutral.
PoC changes
Harness written fresh (no seed). One iteration: added
Verified recommended fix
dirfs_vnops.c getattr/readlink + dirfs_subr.c alloc_file: check dirfs_findfd() return for NULL and fail with ENAMETOOLONG (tear down the fresh node in alloc_file).
Verdict
REPRODUCED (deterministic harness; dirfs is vkernel-only so no live-boot test β DF-0806/0807 precedent). dirfs_findfd returns NULL when the accumulated relative path from the nearest fd-holding ancestor exceeds MAXPATHLEN(1024); dirfs_vnops.c dereferences the return without a NULL check at getattr:391/:393 and readlink:1329, and dirfs_alloc_file (subr:194/:202, reached from nresolve/ncreate/nmkdir/nsymlink) likewise. Harness: verbatim findfd transcription walks 4x255-byte names fine (count=1024) and returns NULL at 5x255 (count=1280); all 3 consumer transcriptions die with SIGSEGV; fixed variants (NULL check + ENAMETOOLONG) survive. Impact: unprivileged vkernel user crashes the vkernel via stat/readlink/open on a deep path (pre-existing host tree needs no fds on intermediates because nresolve-created nodes never hold fds; dirfs-created trees reach it after passive-fd eviction (limit 100) or vnode recycling). Availability only.
No comments yet.