# DF-3241 — ufs_check_export / ufs_fhtovp reachable with mnt_data == NULL in the dounmount fsid window

**File under audit:** `sys/vfs/ufs/ufs_vfsops.c` (pass 2)
**Kernel:** DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), stock in-guest build

## What the bug is

`dounmount()` (sys/kern/vfs_syscalls.c) tears a mount down in this order:

1. `VFS_UNMOUNT()` — ffs_unmount() frees the `struct ufsmount`, sets
   `mp->mnt_data = 0` (sys/vfs/ufs/ffs_vfsops.c:862-866)
2. … journal cleanup, vnode-op teardown, cache drops …
3. `mountlist_remove(mp)` (sys/kern/vfs_syscalls.c:1040) — only NOW does the
   mount leave the fsid RB-tree.

Between (1) and (3) the mount is still resolvable **by fsid** through
`vfs_getvfs()` (sys/kern/vfs_mount.c:413-424), which does *not* check
`MNTK_UNMOUNT`. Every fsid consumer then feeds the still-listed mount into
the UFS file-handle path:

* `nfsrv_fhtovp()` (sys/vfs/nfs/nfs_subs.c:1235-1246) — **unauthenticated
  remote/local** whenever an exported UFS exists and nfsd is running:
  `VFS_CHECKEXP` → `ufs_check_export()` derefs
  `VFSTOUFS(mp)->um_export` on a NULL `mnt_data`
  (sys/vfs/ufs/ufs_vfsops.c:242-246), then `VFS_FHTOVP` →
  `ffs_fhtovp()` reads `VFSTOUFS(mp)->um_fs` at NULL+0x18
  (sys/vfs/ufs/ffs_vfsops.c:1216-1218).
* `fhopen/fhstat/fhstatfs/fhstatvfs` (sys/kern/vfs_syscalls.c:4856, 5024,
  5072, 5133) — root-gated in-kernel repro path (used by this PoC).
* `kern_checkpoint` (sys/kern/kern_checkpoint.c:496).

Result: deterministic `Fatal trap 12`, supervisor read at 0x18 in
`ffs_fhtovp` (kernel NULL-deref panic). This is the UFS twin of DF-3236
(devfs fhtovp NULL lock) and DF-3207 (nullfs mnt_data).

## Files

* `getfh.c`      — capture an fhandle_t (root; getfh(2))
* `fhbomb.c`     — spin fhstat(2) on the captured handle (root; fhstat is root-gated)
* `fhstat_once.c`— sanity: fh must resolve while mounted
* `race.sh`      — vn(4) + newfs UFS setup, N fhstat spinners, mount/umount -f loop
* `build.sh`     — host-side: push + compile in guest
* `run.sh`       — host-side: run the race, watch for the panic
* `panic.txt`    — serial console capture of the fatal trap (baseline)
* `build.log`    — kernel build log for the fix validation
* `run.fix.log`  — patched-kernel rerun (must survive)
* `fix.diff`     — root-cause fix: vfs_getvfs() refuses mounts with MNTK_UNMOUNT

## Reproduce

```
# build
./build.sh
# run (as root, in guest):     ROUNDS=4000 NBOMB=8 sh /root/poc/race.sh
# host-side driver:
./run.sh
```

**Expected (stock kernel):** panic within seconds-to-minutes:

```
Fatal user address access from kernel mode from fhbomb at ...
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x18
...
Stopped at      ffs_fhtovp+0xb: movq    0x18(%rax),%r9     # rax == mnt_data == NULL
```

**Expected (patched kernel):** the race survives all rounds; fhstat gets
ESTALE during the window instead of a torn-down mount.

## Notes

* The panic reproduced on the first driver run (ROUNDS=3, NBOMB=4) within
  ~35 s — the window is hit easily because every in-window fhstat traps.
* **Fix engineering result:** a `vfs_getvfs()` MNTK_UNMOUNT filter ALONE is
  NOT sufficient — the fix-v1 kernel panicked identically within ~20 s
  (check-and-use is preemptible). The validated fix (fix.diff) adds the
  `vfs_busy(mp, LK_NOWAIT)` interlock (shared `mnt_lock` excludes the whole
  dounmount body) to `sys_fhstat` and `nfsrv_fhtovp`, keeps the vfs_getvfs
  filter, and NULL-guards `ufs_check_export`/`ffs_fhtovp`. Patched kernel:
  6000/6000 rounds survived, zero panics.
* The unprivileged-real-world variant needs nfsd + an exported UFS mount
  (NFS traffic is unauthenticated); the fh* syscalls used here are
  root-gated and only demonstrate the kernel-side primitive (same
  methodology as DF-3236's accepted repro).
