# DF-2997 — PoC evidence pack

**nfs_lookup/nfs_mkdir error paths vrele() vnodes that nfs_nget returned
LK_EXCLUSIVE-locked — permanent vnode-lock leak; unkillable processes,
wedged recycler (vnlru), unreclaimable vnode growth from malformed NFS
server replies.**

## What is in here

| file | what |
|---|---|
| `fakesrv.c` | userland malicious NFSv3/UDP server (rpcbind :111, mountd :779, nfs :2049 on 127.0.0.1). Four exports keyed by root filehandle: `/ct` control (honest), `/lk` LOOKUP-truncation (leak via nfs_vnops.c:1272), `/mk` MKDIR-truncation (leak via nfs_vnops.c:2361), `/nd` MKNOD-truncation (drives the STOCK kernel already-vput error path in nfs_mknodrpc — used only to prove the umount panic pre-exists) |
| `probe.c` | one-shot syscall probe (mkdir / open-O_CREAT / stat / mknod-FIFO) printing errno |
| `build.sh` | `cc -O2 -Wall -o fakesrv fakesrv.c; cc -O2 -Wall -o probe probe.c` (in guest) |
| `run.sh` | baseline run on the stock kernel: control, both leak triggers, 80-name growth, wedge probes (repeat syscall → D-state), umount attempt, vnlru pressure |
| `run_fixed2.sh` | fix validation on the patched kernel (same rig; wedge probes must FAIL FAST; vnlru must stay healthy) |
| `run_stocknod.sh` | stock-kernel proof that the `unmount: dangling vnode` panic pre-exists (MKNOD path, no DF-2997 site involved) |
| `fix.diff` | the verified fix: `vput()` for nfs_nget-sourced newvp at nfs_vnops.c:1272 / :2348 / :2361 (vrele kept for the dvp-echo cases) |
| `run.raw.baseline` | FULL baseline log (stock): 82x errno=72 leaks, NVN 599→672 never reclaimed, probes `D2 wchan vnode` surviving kill -9, `umount -f` wedged D0, `mount` wedged D0 |
| `fakesrv.log.baseline` | server log: per-name LOOKUP #1 (nresolve, NOENT), #2 (compat-shim old lookup, truncated reply = the leak), #3 (wedge attempt) |
| `ps.now.baseline`, `vnlru.before/after.baseline`, `vn.now.baseline` | process/vnode state snapshots; `vnlru.after` shows the recycler kthread wedged (`B5 vnode vnlru`) and cron wedged (`D5Vs vnode`) after maxvnodes pressure |
| `run_fixed.raw`, `run_fixed2.raw` | FULL patched-kernel logs: all symptoms gone |
| `run_stocknod.raw` | stock MKNOD→umount panic proof log |
| `panic_fixed.txt`, `panic_stocknod.txt` | serial-console panic transcripts (pre-existing VNON/vflush defect) |
| `env.txt`, `VERDICT.md`, `manifest.json`, `verdict.json` | environment, narrative, catalog, machine verdict |

## How to reproduce (baseline, stock kernel)

```sh
dfbsd-qemu/vm.sh reset with-src
sh push.sh
dfbsd-qemu/vm.sh run_root 'cd /root/df2997 && sh build.sh && sh run.sh > run.raw 2>&1'
```

Expected (stock): every malicious trigger fails fast with
`errno=72 (RPC struct is bad)`; `debug.numvnodes` climbs permanently
(599→672, zero reclaim after 20s); the SECOND syscall on a leaked name
hangs in `D2` state, wchan `vnode`, immune to `kill -9`; `umount -f`
and even `mount` (listing) wedge in D-state; after
`sysctl kern.maxvnodes=<85% of numvnodes>` the `vnlru` kernel thread
wedges (`B5 vnode vnlru`) and innocent processes (cron) wedge at exit.
The only recovery is a reboot.

## How to reproduce (fix validation)

```sh
# apply fix.diff to the guest /usr/src copy (NOT to the audit tree)
dfbsd-qemu/vm.sh run_root 'cd /usr/src && git apply /root/df2997/fix.diff'
dfbsd-qemu/vm.sh run_root 'cd /usr/src && make nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC'
# reboot, then:
dfbsd-qemu/vm.sh run_root 'cd /root/df2997 && sh run_fixed2.sh > run_fixed2.raw 2>&1'
```

Expected (patched): identical control behavior; the same errno=72
errors (protocol behavior unchanged); repeat syscalls FAIL FAST and the
processes exit (no D-state); vnodes are reclaimed (665→629→481);
vnlru stays `vlruwt` under the same pressure; cron survives. The final
`umount -f` of the still-poisoned mount panics with
`unmount: dangling vnode` — a PRE-EXISTING latent defect
(`vmntvnodescan` skips `VNON` vnodes, sys/kern/vfs_mount.c:931/:978),
independently reproduced on the STOCK kernel via `run_stocknod.sh`
through the stock nfs_mknodrpc error path that already vput()s. On the
stock kernel the same umount does not even get that far: it hangs
forever on the wedged vnode locks.

## Attack preconditions (honest)

The trigger requires the client kernel to accept NFS replies from an
attacker: a malicious/compromised NFS server, an on-path UDP spoofer
(NFSv3/UDP + AUTH_SYS provides no integrity), or an unprivileged local
user on a client that mounts attacker-influenced exports. The mount
itself must exist (root/automount). Every trigger syscall runs as an
unprivileged user (uid 1001 in the demo).
