# DF-3207 — nullfs VFS ops race forced unmount: `mnt_data` NULL-deref / UAF

## What

`kern_statfs()` (`sys/kern/vfs_syscalls.c:1428-1436`), `kern_fstatfs()`
(`:1495-1512`) and `sys_quotactl()` (`:1244-1247`) call
`VFS_STATFS`/`VFS_QUOTACTL` on a mount pointer obtained from an nchandle or
`fp->f_nchandle` **without** `vfs_busy()` or an `MNTK_UNMOUNT` interlock
(unlike `kern_mountctl()` at `:1401` and `mountlist_scan()` at
`sys/kern/vfs_mount.c:763`, which both interlock).

Under `umount -f`, `dounmount()` is allowed to proceed to `VFS_UNMOUNT`
while mount references are still outstanding: after its 50x250ms retry
loop exhausts it just sets `freeok = 0` and calls `VFS_UNMOUNT` anyway
(`sys/kern/vfs_syscalls.c:964-1006`). `nullfs_unmount()`
(`sys/vfs/nullfs/null_vfsops.c:261-267`) then does

```c
xmp = (void *)mp->mnt_data;
mp->mnt_data = 0;
...
kfree(xmp, M_NULLFSMNT);
```

unconditionally. Any VFS op that reaches the nullfs mount after that
dereferences `MOUNTTONULLMOUNT(mp)->nullm_vfs`
(`nullfs_statfs` at `null_vfsops.c:308`, `nullfs_quotactl` at `:294`,
`nullfs_checkexp` at `:341`) — a read from kernel VA 0 (deterministic
panic) or a use-after-free read of the freed `null_mount`.

A mount reference pins only `struct mount`, never `mnt_data`. This is the
gap the nullfs pass-2 note ("every VOP caller holds nch + mount ref") missed.

## Deterministic trigger (verified)

The retry-loop race is hard to win (an in-flight statfs holds the mount
ref that blocks `dounmount` from proceeding until the statfs finishes),
but the exhausted-force path makes it **deterministic** with a mount
reference that is invisible to the unmount kill-scan:

1. `mount_null /tmp/t /mnt/a` (root).
2. Unprivileged process (`test`): open `/mnt/a/f1`, `sendmsg()` the fd
   over an `AF_UNIX` socketpair with `SCM_RIGHTS`, close it. The parked
   `struct file` keeps `fp->f_nchandle.mount` == nullfs mp (a mount ref)
   **in the socket receive buffer**, where `process_uses_mount()`
   (`vfs_syscalls.c:727-757`) cannot see it (it only scans fd tables and
   cwd/root/jail dirs) — so the `umount -f` SIGINT/SIGKILL scans pass it.
3. Root: `umount -f /mnt/a` — exhausts 50x250ms retries (refs==2:
   ours is the invisible one), prints `forcing unmount`, runs
   `VFS_UNMOUNT` → `mnt_data = 0`, `kfree(xmp)`, returns 0. The mount
   structure stays allocated (leaked, `freeok == 0`).
4. Unprivileged process: `recvmsg()` the fd back, call `fstatfs(2)`:
   `kern_fstatfs` takes `mp = fp->f_nchandle.mount` (dead mount),
   `VFS_STATFS` → `nullfs_statfs` → `MOUNTTONULLMOUNT(mp)->nullm_vfs`
   → read at VA 0 → **fatal trap 12, kernel panic**.

Baseline result (stock kernel #0):

```
Fatal user address access from kernel mode from ghost at ffffffff807105e2
Fatal trap 12: page fault while in kernel mode
fault virtual address     = 0x0
current process           = 6405        (ghost, uid=test)
Stopped at nullfs_statfs+0x32: movq (%rax),%rdi
```

(see `panic.txt`; guest sat in DDB, ssh dead, `vm.sh status` => down)

With `vfs.usermount=1` (non-default) an unprivileged user owning the mount
can run the `umount -f` step too, making the whole chain unprivileged.
The in-flight race variant (statfs/quotactl straddling step 3) is the same
bug with a tighter window.

## Ceiling

- Deterministic outcome is the **NULL-deref panic** (kernel DoS).
- The UAF-read variant (thread loads `mnt_data` before `:262`, dereferences
  after `kfree`) reads a freed 88-byte `M_NULLFSMNT` chunk. That malloc
  type is nullfs-private: reallocation content is only ever written by
  `nullfs_mount` (M_ZERO + two kernel pointers), so an attacker cannot
  plant controlled data; `nullm_vfs` resolves to a valid mount or garbage
  → panic. No path to uid=0 was found; the primitive is not a controlled
  write. Classified DoS/panic, not privesc.

## Files

- `ghost.c`      unprivileged trigger (park fd via SCM_RIGHTS, recover, fstatfs)
- `det_driver.sh` root-side driver (mount_null / umount -f / signal)
- `spam.c`, `driver.sh` — brute-force race variant (2400 rounds, no hit;
  the retry loop self-serializes against in-flight calls — see VERDICT.md)
- `fix.diff`     interlock fix (`vfs_busy`/`vfs_unbusy` in
                 kern_statfs / kern_fstatfs / sys_quotactl)
- `panic.txt`, `fix_run.log`, `fix_run2.log`, `env.txt`

## Reproduce

```
# in guest, as root:
/root/det_driver.sh 1
# round 1: parked; umount -f (takes ~13s)...
# round 1: umount rc=0; firing fstatfs
#   -> stock kernel: Fatal trap 12, Stopped at nullfs_statfs+0x32
#   -> patched kernel: "fstatfs returned errno=16 (no panic)"
```
