# DF-3241 VERDICT — ufs_check_export / ufs_fhtovp NULL mnt_data window (dounmount)

**Status: REPRODUCED (panic) — fix validated (6000/6000 rounds clean)**

## What was reproduced

On the stock INVARIANTS kernel (`DragonFly 6.5-DEVELOPMENT #0`, X86_64_GENERIC),
an `fhstat(2)` that resolves a UFS mount's fsid inside the `dounmount()` window
between `VFS_UNMOUNT()` (which frees `struct ufsmount` and clears
`mp->mnt_data`, sys/vfs/ufs/ffs_vfsops.c:862-866) and `mountlist_remove(mp)`
(sys/kern/vfs_syscalls.c:1040) faults deterministically:

```
Fatal user address access from kernel mode from fhbomb at ffffffff80910b3b
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
```

0x18 == `offsetof(struct ufsmount, um_fs)` (ufsmount.h:69-74). Reproduced on
the **first driver run** (ROUNDS=3, NBOMB=4) within ~35 s; the guest sat in
DDB (`db>`).

## Why it happens (path:line)

1. `sys_fhstat` / `nfsrv_fhtovp` resolve a user/wire fsid with
   `vfs_getvfs()` (sys/kern/vfs_mount.c:413-424), which only RB-looks-up the
   mounttree — it does **not** check `MNTK_UNMOUNT` (stock kernel).
2. `dounmount()` order: `dounmount_interlock` sets MNTK_UNMOUNT
   (vfs_syscalls.c:831) → `VFS_UNMOUNT` frees ump, `mnt_data = 0`
   (vfs_syscalls.c:1006 → ffs_vfsops.c:862-866) → **then**
   `mountlist_remove` (vfs_syscalls.c:1040). Throughout that span the mount
   is still fsid-resolvable.
3. `ffs_fhtovp` (ffs_vfsops.c:1216-1218) and `ufs_check_export`
   (ufs_vfsops.c:242-246) deref `VFSTOUFS(mp)` unguarded → NULL read.

## Reachability / threat

* PoC path (used here): `fhstat(2)` — root-gated (same accepted methodology
  as the DF-3236 devfs twin).
* Real-world unprivileged path: `nfsrv_fhtovp()` (sys/vfs/nfs/nfs_subs.c:
  1235-1246) — **unauthenticated** NFS traffic to a running nfsd while any
  exported UFS is force-unmounted; `VFS_CHECKEXP` → `ufs_check_export`
  crashes the same way (`NE_LOCK` on NULL+0x78 when MNT_EXPORTED sticks,
  since ffs_unmount never clears it, ffs_vfsops.c:867).
* `kern_checkpoint` (kern_checkpoint.c:496) has the same shape.

Impact ceiling: kernel NULL-deref **panic (DoS)** — not a corruption
primitive; no escalation chain exists for this bug class (fixed-address
read of a zeroed pointer field).

## Fix iteration (important engineering result)

* **fix v1** — `vfs_getvfs()` refuses mounts with MNTK_UNMOUNT:
  **INSUFFICIENT**. The rebuilt kernel still trapped at `ffs_fhtovp+0xb`
  within ~20 s: the check-and-use is a TOCTOU — an fhstat thread can pass
  the flag check (flag still clear), be preempted at `lwkt_reltoken`, and a
  full `umount -f` completes before the thread loads `mnt_data`.
* **fix v2 (validated)** — the airtight interlock is `vfs_busy()`:
  it checks MNTK_UNMOUNT under `mnt_token` AND takes `mnt_lock` shared,
  which excludes the *entire* dounmount body (exclusive `mnt_lock` from
  vfs_syscalls.c:842 through 1091, including VFS_UNMOUNT).
  1. sys/kern/vfs_mount.c — keep the vfs_getvfs MNTK_UNMOUNT filter
     (semantics: a dying mount must not be handed out; defense in depth).
  2. sys/kern/vfs_syscalls.c `sys_fhstat` — vfs_busy(mp, LK_NOWAIT) across
     the VFS_FHTOVP; ESTALE on failure; vfs_unbusy+mount_drop on the way
     out.
  3. sys/vfs/nfs/nfs_subs.c `nfsrv_fhtovp` — same vfs_busy/unbusy wrap
     around the CHECKEXP+FHTOVP pair (the unauthenticated threat path).
  4. sys/vfs/ufs/ufs_vfsops.c `ufs_check_export` — NULL mnt_data guard
     returning EACCES.
  5. sys/vfs/ufs/ffs_vfsops.c `ffs_fhtovp` — NULL mnt_data guard returning
     ESTALE.

  Baseline vs patched, identical driver:
  * stock kernel: `Fatal trap 12 @ 0x18 in ffs_fhtovp` ≤ 3 rounds (twice:
    stock and fix-v1).
  * fix-v2 kernel (`#1 Wed Sep 9 05:56:10`): **6000/6000 mount+umount -f
    rounds with 8 fhstat spinners, SURVIVED, no panic**; fhstats return
    ESTALE during teardown instead of crashing.

## Residual notes

* The same fsid-window pattern exists for `fhopen`/`fhstatfs`/`fhstatvfs`
  (root-gated) and `kern_checkpoint`; fix v2's vfs_busy pattern should be
  applied there too (mechanical, same shape). This PoC validated the
  pattern on sys_fhstat + the NFS path is protected by the same patch.
* `ffs_unmount` never calls `vfs_free_addrlist(&ump->um_export)` — radix
  heads leak on unmount of an exported fs (root-gated; separate low-sev
  issue, ffs_vfsops.c territory).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
