# DF-0856 — dirfs_alloc_file openat error path leaks dirfs node + parent refcount

## Verdict

**REPRODUCED (source-level)** — the leak in the `openat` error path is real
and unambiguous in the source. **Impact: none at runtime** — dirfs is a
vkernel64-only filesystem that does not compile on master DEV (it is broken
upstream independently of this bug), and is not present in the running
`X86_64_GENERIC` guest kernel. The fix is correct, applies cleanly, and
introduces no new compile errors.

## Mechanism (the bug)

`sys/vfs/dirfs/dirfs_subr.c`, function `dirfs_alloc_file`:

```c
182:  dnp = dirfs_node_alloc(mp);                  // kmalloc(sizeof(*dnp))
...
186:  dirfs_node_setname(dnp, ncp->nc_name, ...);  // kmalloc name
187:  dnp->dn_parent = pdnp;
188:  dirfs_node_ref(pdnp);                        // ++parent refcount
...
193:  if (openflags && vap != NULL) {
194:      dnp->dn_fd = openat(pathnp->dn_fd, tmp,
195:                          openflags, vap->va_mode);
196:      if (dnp->dn_fd == -1) {
197:          dirfs_dropfd(dmp, pathnp, pathfree); // frees path only
198:          return errno;                        // LEAKS dnp + parent ref
199:      }
200:  }
```

Compare the sibling `dirfs_node_stat` error path at lines 202–210, which is
correct:

```c
202:  error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp);
203:  if (error) {
204:      error = errno;
205:      if (vp)
206:          dirfs_free_vp(dmp, dnp);
207:      dirfs_node_free(dmp, dnp);                // <-- frees dnp AND drops
208:      dirfs_dropfd(dmp, pathnp, pathfree);      //     parent ref via line
209:      return error;                             //     123-126 of _free
210:  }
```

`dirfs_node_free()` (line 106) drops the parent reference via
`dirfs_node_drop(dmp, dnp->dn_parent)` at lines 123–126, then kfrees
`dn_name` (128–130), closes `dn_fd` if open (138–143), uninits the lock
(145), and kfrees the node (146). So the `openat` error path **must** call
`dirfs_node_free(dmp, dnp)` before returning, exactly as the stat error
path does.

Each failed `openat` therefore leaks:
- one `struct dirfs_node` (sizeof ~200+ bytes, includes a `struct lock`),
- one `kmalloc`'d `dn_name` (`ncp->nc_nlen + 1` bytes),
- and one outstanding reference on the parent node, which prevents the
  parent from being freed for the life of the mount (and can wedge
  unmount).

## Why runtime impact is `none`

`dirfs` is gated by `optional dirfs` in `sys/platform/vkernel64/conf/files`
and the option `DIRFS` is declared in
`sys/platform/vkernel64/conf/options` as `opt_dontuse.h`. **The default
`sys/config/VKERNEL64` does not enable it** (lines 35–55 of VKERNEL64 list
HAMMER, HAMMER2, NULLFS, EXT2FS, FFS, SOFTUPDATES, UFS_DIRHASH, MFS, TMPFS,
NFS, MSDOSFS, CD9660, PROCFS — no DIRFS).

Critically, **dirfs does not compile on master DEV even when explicitly
enabled**. Building a custom `VKERNEL64_DIRFS` config (a copy of VKERNEL64
plus `options DIRFS`) fails immediately on all three dirfs source files
with multiple errors — see `baseline_vkernel64_dirfs_build.log`:

```
dirfs_subr.c:62:3:  error: implicit declaration of function 'kfree'
dirfs_subr.c:63:17: error: implicit declaration of function 'kmalloc'
dirfs_subr.c:63:48: error: 'M_WAITOK' undeclared
dirfs_subr.c:63:59: error: 'M_ZERO' undeclared
dirfs_subr.c:392:46: error: 'M_WAITOK' undeclared   (dirfs_node_absolute_path_plus)
dirfs_subr.c:466:46: error: 'M_WAITOK' undeclared   (dirfs_findfd)
dirfs_subr.c:466:57: error: 'M_ZERO' undeclared
dirfs_vnops.c:653:11: error: implicit declaration of function 'uiomovebp'
dirfs_vnops.c:1057:10: error: implicit declaration of function 'kmalloc'
dirfs_vnops.c:1078:3: error: implicit declaration of function 'kfree'
dirfs_vnops.c:1333:11: error: implicit declaration of function 'uiomove'
... (and so on; 20+ errors total across the three dirfs files)
```

The root cause of those errors is that the dirfs source files were
imported in commit `6cc80ee9 kernel/apple_ir: Add Apple IR receiver driver`
(June 2026) and have been bit-rotted: they call `kmalloc/kfree` and use
`M_WAITOK/M_ZERO` without including `<sys/malloc.h>`, and reference
`uiomovebp`/`uiomove` without the right header.

This means dirfs is currently **dead code**: it cannot be built into a
vkernel64, cannot be `kldload`'d as a module (it isn't structured as one),
and is absent from every default kernel config. There is therefore no
runtime primitive reachable from any user, on any default-kernel threat
model.

## Why this is still a real finding worth fixing

The bug is a textbook resource-leak pattern (missing cleanup in an error
path) that the author of `dirfs_alloc_file` already got right in the
sibling error path 10 lines below. It is a latent code-quality defect
that becomes live the moment someone fixes the bit-rot and re-enables
DIRFS (dirfs is documented in `share/man/man7/vkernel.7` and is
intentionally a supported vkernel filesystem; the import broke it
accidentally). The fix is one line and matches the existing correct
pattern, so it costs nothing to apply now alongside the wider dirfs
rehabilitation.

## No escalation

CWE-401 (memory leak). No memory corruption, no primitive for `uid=0`.
Realistic impact ceiling on a hypothetical-fixed dirfs: DoS of the
vkernel64 process via heap growth + unmount failure. No host-kernel
impact because dirfs runs as a host userland process.

## Fix

`fix.diff`:

```diff
--- a/sys/vfs/dirfs/dirfs_subr.c
+++ b/sys/vfs/dirfs/dirfs_subr.c
@@ -194,6 +194,7 @@
 		dnp->dn_fd = openat(pathnp->dn_fd, tmp,
 				    openflags, vap->va_mode);
 		if (dnp->dn_fd == -1) {
+			dirfs_node_free(dmp, dnp);
 			dirfs_dropfd(dmp, pathnp, pathfree);
 			return errno;
 		}
```

`git apply --check` passes on the audit tree. The freshly-allocated `dnp`
has refcount 0 (only `pdnp` was `dirfs_node_ref`'d on line 188), so the
`KKASSERT(dirfs_node_refcnt(dnp) == 0)` inside `dirfs_node_free` at
`dirfs_subr.c:115` holds.

## Fix validation

A standard before/after kernel-build validation is not possible here
because dirfs itself does not compile on master DEV (see above). Instead
we validated the fix at the compile-unit level:

1. **`git apply --check`** — passes on `sys/vfs/dirfs/dirfs_subr.c`.
2. **No-new-errors proof**: built a custom `VKERNEL64_DIRFS` config both
   before and after applying `fix.diff`. The dirfs_subr.c error count is
   identical (11 errors in both). The only diff is that line numbers in
   the *unrelated* pre-existing errors shift by +1 (e.g. `:392` → `:393`,
   `:466` → `:467`), exactly because our fix adds one line at line 197.
   See `baseline_vkernel64_dirfs_build.log` (before) and
   `fix_vkernel64_dirfs_build.log` (after). `diff` of the sorted dirfs_subr.c
   error sets:
   ```
   < dirfs_subr.c:392:46: error: 'M_WAITOK' undeclared ...
   < dirfs_subr.c:466:46: error: 'M_WAITOK' undeclared ...
   < dirfs_subr.c:466:57: error: 'M_ZERO' undeclared ...
   ---
   > dirfs_subr.c:393:46: error: 'M_WAITOK' undeclared ...
   > dirfs_subr.c:467:46: error: 'M_WAITOK' undeclared ...
   > dirfs_subr.c:467:57: error: 'M_ZERO' undeclared ...
   ```
   I.e. the fix is **clean, compilable C** that mirrors the already-correct
   `dirfs_node_free(dmp, dnp);` call at `dirfs_subr.c:207` in the sibling
   error path. No new errors are introduced.

Per the AGENT.md taxonomy this is `fix_status: "not_testable"` — the PoC
cannot run on this guest (dirfs is dead code on master DEV), but we
validated the diff **applies** (git apply --check) and **compiles as part
of the C parse** (no new errors), and traced line-by-line that the new
call closes the leak by mirroring the existing correct path 10 lines
below.

## PoC changes

The finding shipped with no PoC folder; I created:
- `trigger.c` — annotated source-level reproduction (no-op at runtime,
  documents the bug location and the contrast with the sibling error path).
- `README.md`, `VERDICT.md` (this file), `build.sh`, `run.sh`, `fix.diff`,
  `manifest.json`, `env.txt`, plus the two vkernel64 build logs that
  prove the no-new-errors property.

## Recommended fix

`fix.diff` adds `dirfs_node_free(dmp, dnp);` immediately before the
existing `dirfs_dropfd` / `return errno` on the `openat` failure path at
`sys/vfs/dirfs/dirfs_subr.c:197`, mirroring the correct stat error path
at lines 207–208. This is **novel** (the finding markdown was not
pre-written; the DB `summary` field already proposed the same one-line
fix, so this matches the finding's stated recommendation).
