# DF-3196 — `nfs_nget_nonblock` revalidation restart keeps a stale `vp` across `vput(vp); goto loop` → double-vput / refcount underflow (latent; preemption-window race)

## What this is

In `nfs_nget_nonblock()` (sys/vfs/nfs/nfs_node.c), after a successful
`vget(vp, LK_EXCLUSIVE|LK_NOWAIT)` the scan restarts with `goto loop`
(:269). If, on the restarted scan, the first matching node's vnode differs
from the carried `vp` (:271), the code does:

```c
if (NFSTOV(np) != vp) {
        vput(vp);          /* :272 — ref dropped ...          */
        goto loop;         /* :273 — ... but vp NOT reset      */
}
```

`vp` stays non-NULL across the restart, so the next matching node either
(a) triggers `vput(vp)` a second time (double-vput → `vrele` refcount
underflow → vnode freed/cached while still referenced → use-after-free
potential, in practice a `vrele` assert/panic), or (b) if the list then
exhausts, the `if (vp) { vput(vp); ... }` cleanup at :284-287 vputs the
stale `vp` again — deterministically, once the branch is entered.
Contrast the retry path two pages down, whose comment states the rule
explicitly: *"vp state is retained on retry/loop so we must NULL it out
here or fireworks may ensue"* (:344-348) — the :272-273 restart forgot it.

Reachability of :271 itself requires the bucket to mutate between the
NOWAIT-vget restart and the rescan — possible only because `lwkt_gettoken`
holds no critical section and `lwkt_switch()` releases all tokens
(sys/kern/lwkt_thread.c:636-638): a preempted scanner can return to a
bucket whose matching node was reclaimed and re-created (different vnode)
by another thread. This is the same class of stale-view window as DF-3194,
narrower (needs preemption inside a ~2-instruction window plus a
re-creation by a third thread).

nfs_nget_nonblock is called from `nfs_readdirplusrpc`
(sys/vfs/nfs/nfs_vnops.c:2891) — i.e. the trigger surface is concurrent
`getdents` on NFS directories racing vnode reclaim/recreation, an
unprivileged local operation (with server cooperation for fh stability).

## Status

Not dynamically verified — the preemption window is far narrower than
DF-3194's (which itself could not be triggered in ~620k attempts on the
QEMU guest). Verdict: `untested`, severity Low, confidence likely
(the defect is directly visible in the code; the fix is the one-line
`vp = NULL;` mirror of :348).

## Fix

Included as hunk 3 of findings/poc/DF-3194/fix.diff:

```diff
 		if (NFSTOV(np) != vp) {
 			vput(vp);
+			/* vp is stale after vput(): NULL it or the restart
+			 * double-vputs the same vnode (refcount underflow). */
+			vp = NULL;
 			goto loop;
 		}
```

That fix compiled and booted as part of the DF-3195 validation kernel.
