# DF-3194 VERDICT — NOT REPRODUCED dynamically (race); static chain certain; ~620k attempts across 3 amplified configurations

## Bottom line

The "validate that we did not race another nfs_nget()" loser paths in
`nfs_nget()` (:189-202, drop at :194) and `nfs_nget_nonblock()` (:331-350,
drop at :336) release the freshly created vnode with a bare `vx_put(vp)`:

```c
vx_put(vp);                                    /* nfs_node.c:194 / :336 */
lockmgr(&nfsnhash_lock, LK_RELEASE);
if (np->n_fhsize > NFS_SMALLFH) kfree(np->n_fhp, M_NFSBIGFH);
np->n_fhp = NULL;
kfree_obj(np, nmp->nm_mnode);
```

At that instant `vp` is a stock `getnewvnode()` product: `v_type == VNON`
(set in `allocvnode`, sys/kern/vfs_lock.c:1213), `v_data == NULL`,
`v_mount == mntp` (insmntque), `v_ops` = NFS ops. DragonFly's `vx_put()`
**forces `VREF_FINALIZE` for VNON/VBAD vnodes** (sys/kern/vfs_lock.c,
vx_put), so `vrele()`'s 1→0 transition runs `vnode_terminate()` →
`VOP_INACTIVE(vp)` → `nfs_inactive()`:

```c
np = VTONFS(ap->a_vp);            /* NULL            :389 */
if (ap->a_vp->v_type != VDIR) {   /* VNON != VDIR    :392 */
        sp = np->n_sillyrename;   /* NULL+offset deref :393 → page fault → panic */
```

Every link is verified in source; there is no guard anywhere in the chain
(`vop_inactive` dispatch is a plain DO_OPS, `vnode_terminate` only checks
`v_mount != NULL`).

## Why the loser path is reachable (scheduling analysis)

`nfsnhash_lock` (exclusive) serializes insertion; a stale view therefore
requires the losing thread to observe the bucket *across* another
creator's insert. Two schedulings do it:

1. **Mid-scan deschedule.** `lwkt_gettoken()` does not hold a critical
   section over the protected region, and `lwkt_switch()` drops **all** of
   the thread's tokens (sys/kern/lwkt_thread.c:636-638). A creator
   preempted mid-`LIST_FOREACH` (nfs_node.c:116) transparently loses
   `nfsnhash_token`; a creator on another CPU then runs a complete
   nfs_nget and `LIST_INSERT_HEAD()`s the node *behind* the first thread's
   cursor. On resume the stale thread misses the new head, calls
   `lockmgr(... LK_SLEEPFAIL)` — which succeeds **without sleeping**
   because the other creator already released — allocates its own vnode,
   and the validation loop finds the duplicate → `vx_put(VNON vp)` →
   panic. Requirement: preemption duration p > (scan progress c) + winner
   create time τc; with a deep bucket and c≈0 this is p > τc ≈ 100-200 µs.
2. **Winner blocks mid-create.** The lockmgr winner can block between
   acquiring `nfsnhash_lock` and `LIST_INSERT_HEAD` (both allocations are
   M_WAITOK; `getnewvnode`→`allocvnode` has M_WAITOK/`kmalloc_obj`
   fallbacks under VM pressure). A contender then spins in the SLEEPFAIL
   retry loop (fail → `goto loop` → full-bucket rescan). If the winner's
   lockmgr-hold H ≥ one rescan D, its insert lands mid-rescan behind the
   contender's cursor; the contender's next lockmgr succeeds sleep-free and
   its validation loop finds the duplicate → panic. Requirement: one
   ≥ ~2 ms block of the winner (D ≈ 1.8 ms with a 76k-node bucket).

## Reproduction campaign (all on stock INVARIANTS kernel #0 Jul 2, 6 vCPU, 4 GB)

| run | bucket depth | amplifiers | races | result |
|---|---|---|---|---|
| 1 | 16k colliding fhs | 12 barrier-synced threads, host CPU hogs | ~200k | no panic |
| 2 | 58k | + loopback-UDP flood ×2 (netisr preemption) | ~216k | no panic |
| 3 | 76k | + memhog ×6 (VM pressure for mid-create blocks), 10 host hogs | ~288k | no panic |

Why it never fired: on this quiet QEMU guest, (a) LWKT preemptors that run
longer than ~100 µs are rare — per-packet netisr work is µs-scale and
user-level threads cannot preempt a thread in kernel mode mid-syscall; and
(b) the creation-phase allocations are objcache-backed (magazine hits), so
the winner essentially never sleeps mid-create. Both conditions are
ordinary on a real loaded NFS client (interrupt bursts, VM pressure, NFS
timer/callout storms) but could not be forced deterministically from
unprivileged userspace here.

**Status: not_reproduced (dynamically). Confidence in the defect: certain
(statically) / likely (to occur in the wild). The PoC infrastructure
(colliding-fh pool builder, alias-name racing driver, amplifiers) is left
in the pack for re-testing on noisier hardware.**

## Fix

`fix.diff` — connect `(np, vp)` before the drop in both loser paths so the
forced VOP_INACTIVE/VOP_RECLAIM teardown sees a consistent pair
(`nfs_reclaim` frees np because `n_hash.le_prev == NULL`), plus the stale
`vp = NULL` fix for the related double-vput restart in
`nfs_nget_nonblock` (:271-273, DF-3196).

Fix build: both hunks compiled into the DF-3195 validation kernel
(`make nativekernel` RC=0, `nfs_node.c` compile confirmed, installed,
booted, NFS mount/lookup lifecycle exercised — see DF-3195's run.fixed.log
which ran on the kernel containing this fix too). Behavioral
before/after comparison is not possible because the baseline race never
fired in-guest. fix_status: **inconclusive** (builds & boots; race not
triggerable to compare).

## Threat

Unprivileged local user on any NFS-mounted DragonFly system; trigger is
ordinary concurrent path resolution (stat/open of names aliasing to the
same file handle — the server controls fh identity for LOOKUP replies, so
even an honest server plus a flaky scheduler suffices). Outcome: kernel
page fault → panic (local DoS). No memory corruption on this path (NULL +
fixed-offset read).

## 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.
