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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3054 Β· 14 files
FileTypeDescriptionSize
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
README.md readme how to reproduce
↓ download 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 commands
  • build.log, run.log, run.2.log, run.3.log β€” full untrimmed output
  • fix.diff β€” NULL checks + ENAMETOOLONG (applies cleanly, compile-neutral, harness-validated)
  • fix_base_*.log, fix_p3054_*.log β€” baseline vs patched compile comparison
  • VERDICT.md β€” full narrative
  • manifest.json / verdict.json β€” machine-readable results
VERDICT.md verdict full narrative
↓ download raw

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_nresolve NEVER hold an fd: dirfs_alloc_file only calls openat when openflags && vap != NULL (subr.c:193-200) and nresolve passes NULL, 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_limit default 100, dirfs_vfsops.c:77; dirfs_node_setpassive closes 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_plus NULL β†’ 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 --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: 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_testable
baseline reproduced→ patch + rebuild →patched reproduced

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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 for fstatat. Findfd loop and all consumer derefs are verbatim transcriptions with fork/SIGSEGV detection.

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.