# 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:

```c
: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:

```c
: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):

```c
: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
```
