# DF-3180 — tmpfs_fhtovp hands an unpinned tmpfs_node to tmpfs_alloc_vp; the node can be reclaimed+freed under it (UAF window)

## What

`tmpfs_fhtovp` (sys/vfs/tmpfs/tmpfs_vfsops.c:434-468) finds a node in
`tm_nodes_used` under the mount token and immediately calls
`tmpfs_alloc_vp(mp, NULL, node, LK_EXCLUSIVE, vpp)` — with **no reference
pin** on the node. Compare:
  * `tmpfs_unmount` (tmpfs_vfsops.c:288, :347) bumps `tn_links` around
    every blocking step,
  * `tmpfs_alloc_file` (tmpfs_subr.c:568-577) is protected by the dirent
    link count taken before its `tmpfs_alloc_vp` call.

`tmpfs_alloc_vp` blocks (`getnewvnode` at tmpfs_subr.c:383; `vget` at
:432), and blocking releases the mount token (documented by the code's own
comment at tmpfs_vfsops.c:296-302). While the caller is blocked, the last
reference on the vnode can go away: `tmpfs_inactive` (DOOMED) →
`tmpfs_reclaim` (tmpfs_vnops.c:1844-1865) disassociates the vnode
(`node->tn_vnode = NULL; vp->v_data = NULL`) and — for an unlinked file
(tn_links == 0) — **frees the node** (`tmpfs_free_node` → `kfree_obj`).
`vget` then returns ENOENT for the reclaimed vnode (sys/kern/vfs_lock.c:595-601),
and `tmpfs_alloc_vp` executes `vdrop(vp); goto loop;` (tmpfs_subr.c:432-435)
which re-reads `node->tn_vnode` at :382 — freed memory — and can install the
stale pointer as `vp->v_data = node` at :468.

## Reachability

* NFS: for every client RPC on a tmpfs exported via
  `mountctl(MOUNTCTL_SET_EXPORT)` (supported — `tmpfs_mountctl`,
  tmpfs_vnops.c:1879-1904). A malicious client replays a file handle for a
  file whose last reference is disappearing (classic stale-FH race).
* Locally: `fhopen(2)` drives the identical code (root-only).

## Reproduce (root-driven analog of the NFS race)

```
build.sh   # cc -O2 -pthread -o fh_race fh_race.c
run.sh     # ./fh_race /tmp/fhrace 600    (4 racer threads fhopen()ing in a
           #  tight loop while main creates/unlinks/closes the target)
```

Observed on the stock INVARIANTS guest (within ~60 s):

```
panic: assertion "(vp) != NULL && (vp)->v_data != NULL" failed at
       /usr/src/sys/vfs/tmpfs/tmpfs.h:493
cpuid = 2
tmpfs_inactive() at tmpfs_inactive+0x129
vop_inactive() at vop_inactive() at vop_inactive+0x4b
vrele() at vrele+0x16e
tmpfs_alloc_vp() at tmpfs_alloc_vp+0x2e6
tmpfs_fhtovp() at tmpfs_fhtovp+0x9c
Debugger("panic")
```

The fhtovp-sourced vnode reached `tmpfs_inactive` with `v_data == NULL`:
reclaim had already torn down the node while `tmpfs_alloc_vp` (called from
`tmpfs_fhtovp`) was still operating on it — the exact unpinned-node window.
Note this is a *different, later* manifestation than DF-3072's
`KKASSERT(!TMPFS_VNODE_DOOMED)` at tmpfs_subr.c:402 (that assert fires in
the earlier DOOMED interval and did NOT fire here; the race ran past it
into reclaim).

## Impact

* INVARIANTS/debug kernels: remote-triggerable panic (NFS client) — proven.
* Stock (no-INVARIANTS) kernels: the two KKASSERTs guarding this window
  (tmpfs_subr.c:402, tmpfs.h:493) are compiled out; the same race then
  re-reads the freed node at tmpfs_subr.c:382 and can attach it to a live
  vnode as `vp->v_data` (:468) — heap use-after-free / type confusion,
  attacker-timed (traced in VERDICT.md; not demonstrated on a no-INVARIANTS
  build in this run).

## Fix

Pin the node across `tmpfs_alloc_vp` in `tmpfs_fhtovp` exactly the way
`tmpfs_unmount` does — bump `tn_links` before the call and drop it after
(rejecting ENOENT), or re-lookup under the token after a blocking failure;
see `fix.diff`.
