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

## Finding

- **Severity**: Low (CWE-401 Memory Leak)
- **CVSS**: 3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
- **File**: `sys/vfs/dirfs/dirfs_subr.c` lines 182–199
- **Function**: `dirfs_alloc_file`

## Summary

In `dirfs_alloc_file`, the `openat(2)` error path (lines 196–199) returns
`errno` after only calling `dirfs_dropfd`, leaking the dirfs node allocated
at line 182 and permanently inflating the parent's refcount (incremented at
line 188). The sibling `dirfs_node_stat` error path at lines 203–209
correctly calls `dirfs_node_free(dmp, dnp)` first; the `openat` error path
omits this.

## Build

Not applicable — dirfs is `optional dirfs` in
`sys/platform/vkernel64/conf/files` and is **not** present in the default
`VKERNEL64` config (and absent entirely from the running `X86_64_GENERIC`
guest: `nm /boot/kernel/kernel.debug | grep -i dirfs` returns 0). The
reproduction for this finding is **source-level**: see `trigger.c` for the
annotated source trace, and `VERDICT.md` for the full mechanism.

To build the (no-op) trigger for completeness:

```
cc -o trigger trigger.c
```

## Run / Expected

```
./trigger
```

`trigger.c` returns 0 with no side effects — it is a documented source
trace, not a runtime trigger. The bug is verified by reading
`sys/vfs/dirfs/dirfs_subr.c:193-200` against the matching pattern at
`sys/vfs/dirfs/dirfs_subr.c:202-210`.

To exercise the bug at runtime you would need to:
1. Build a vkernel64 with `options dirfs` enabled (test harness — the
   default `VKERNEL64` config does not include it).
2. Boot that vkernel on a host directory backing the dirfs mount.
3. As a vkernel user, `open(O_CREAT, ...)` against a directory the vkernel
   process cannot write (read-only / quota-exceeded / no-permission) so the
   host `openat(2)` inside dirfs returns -1.
4. Observe leaked dirfs_node slab growth in the vkernel process and a
   dangling refcount on the parent node.

This is out of scope for the default-kernel threat model: dirfs only ships
in vkernel64, and only when explicitly enabled.

## Fix

`fix.diff` adds `dirfs_node_free(dmp, dnp);` immediately before the existing
`dirfs_dropfd` / `return errno` on the `openat` failure path, mirroring the
correct stat error path. `dirfs_node_free` (line 106) drops the parent ref
via `dirfs_node_drop(dnp->dn_parent)` (lines 123–126), kfrees `dn_name`
(line 128–130), closes `dn_fd` if open (line 138–143), uninits the lock,
and kfrees the node (line 145–147). The freshly allocated node has refcount
0 (only the parent was ref'd), so `KKASSERT(dirfs_node_refcnt(dnp) == 0)`
at line 115 holds.
