# DF-2668 VERDICT — REPRODUCED (mount hold-count underflow → KKASSERT panic; live-mount UAF on release kernels)

## Bottom line
`fhstatfs(2)` (and identically `fhstatvfs(2)`) on a file handle obtained
from a **nullfs-covered path** (an operation `sys_getfh()` explicitly
supports for NFS exports) makes `sys_fhstatfs()` call `mount_drop()` on the
**lower** filesystem's live `struct mount` without ever holding it. On the
stock INVARIANTS guest this is an immediate, 100%-reliable
`panic: assertion "mp->mnt_refs == 0" failed in mount_drop` with
`sys_fhstatfs()` on the stack (captured). On a release kernel the same path
executes `kfree(mp, M_MOUNT)` on a live mount — a use-after-free of
`struct mount` referenced by the mountlist, the fsid rb-tree, and every
vnode of that filesystem. The `vfs_getvfs()` hold on the nullfs mount is
leaked in the same call (permanent mount leak).

## Why it happens (line-accurate)
1. `sys_fhstatfs()` (`sys/kern/vfs_syscalls.c:5041`) takes a held mount:
   `mp = vfs_getvfs(&fh.fh_fsid)` (`:5063`). `vfs_getvfs()`
   (`sys/kern/vfs_mount.c:414-424`) does `mount_hold(mp)`; the caller owes
   one `mount_drop()` (`:393-405`), which frees the mount when `mnt_hold`
   hits 0 (with `KKASSERT(mp->mnt_refs == 0)`).
2. `VFS_FHTOVP(mp, ...)` on a nullfs mount is a pass-through returning a
   vnode of the lower filesystem: `nullfs_fhtovp()` at
   `sys/vfs/nullfs/null_vfsops.c:389-397` calls
   `VFS_FHTOVP(xmp->nullm_vfs, ...)` and returns the **lower** vp, whose
   `v_mount` is the lower mount.
3. `sys_fhstatfs()` then executes `mp = vp->v_mount;` (`:5074`): the held
   nullfs pointer is overwritten (its hold leaks), and the later
   `mount_drop(mp)` at `:5096` decrements the lower mount's `mnt_hold`
   without a matching hold. A live, mounted filesystem has
   `mnt_hold == 1` (set in `mount_init()`, consumed only by the final
   `dounmount()` drop at `sys/kern/vfs_syscalls.c:1117`), so the spurious
   drop drives it to 0 → assert/`kfree`.
4. `sys_fhstatvfs()` repeats the same two mistakes at `:5135` and
   `:5149`. (`sys_fhopen()` `:4850`/`sys_fhstat()` `:5021` keep the held
   pointer throughout — correct.)
5. Confirmed end-to-end on the guest: `mount -t null /tmp /tmp/nmtest`,
   `getfh("/tmp/nmtest/anchor_file")` (nullfs fsid reported:
   `89630026,0000000b`), `fhstatfs(fh)` → panic in `mount_drop` called
   from `sys_fhstatfs+0xd4`.

## Threat model
Triggering requires root (`caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)`
at `:5056`), so this is not an unprivileged escalation. The realistic
victims are privileged daemons following the documented
"nullfs mounts can be explicitly exported" workflow (mountd/NFS), plus any
root tooling using fhstatfs on synthetic handles. The corruption itself
(live mount freed while fully linked into the mountlist) is
heap-corruption-grade on release kernels.

## Fix validation
`fix.diff` deletes both `mp = vp->v_mount;` reassignments
(`sys/kern/vfs_syscalls.c:5074`, `:5135`) so the mount that was actually
held by `vfs_getvfs()` is the one statfs'd (nullfs `VFS_STATFS` already
passes through to the lower filesystem's stats) and dropped exactly once.
Applied to the guest's `/usr/src` together with DF-2667's fix, kernel
rebuilt (`make nativekernel KERNCONF=X86_64_GENERIC`, INVARIANTS),
rebooted:
- baseline (stock kernel): panic (above);
- patched kernel: identical setup, `fhstatfs` returns 0 with valid
  `f_mntonname`/`f_type`, no panic, subsequent mount/unmount of the
  nullfs and tmpfs lifecycle normal (`run.patched.log`,
  `fix_validation.log`).

## Artifacts
`fhstatfs_hold.c` (PoC), `build.sh`, `setup_and_run.sh`, `run.log`,
`panic.txt`, `env.txt`, `fix.diff`, `run.patched.log`, `fix_validation.log`,
`verdict.json`, `manifest.json`.
