nfs_nget_nonblock revalidation restart retains a stale vp across vput(vp); goto loop β any execution of the NFSTOV(np) != vp branch double-vputs the same vnode (refcount underflow β premature vnode recycling / assert panic); the adjacent retry path's own comment states the rule this restart forgot
Summary
In nfs_nget_nonblock() the post-vget rescan restart (:265-278) does vput(vp); goto loop at :271-273 without resetting vp. On the restarted scan, the next matching node (or the list-exhausted cleanup at :284-287) vputs the stale vp AGAIN - double-vput -> vrele refcount underflow -> vnode enters cached state while still lock-held/dirty -> assert panic or use-after-free on reuse. The branch requires a stale bucket view (preemption of the token-holding scanner inside the restart window plus reclaim+recreation by a third thread) - the same window class as DF-3194 but narrower. Trigger surface: concurrent getdents racing vnode recycle (called only from nfs_readdirplusrpc). No standalone PoC - filed on inspection; the one-line fix (vp=NULL before goto) compiled and booted as hunk 3 of DF-3194's fix.diff in the DF-3195 validation kernel.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3196 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| verdict.json | β | 2.5 KB | view raw |
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:
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:
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.
Fix verification
not_testablefix.diff hunk 3 compiled and booted as part of the DF-3195 validation kernel; behavioral comparison impossible (baseline race not triggerable in-guest).
findings/poc/DF-3194/fix.diff hunk 3; DF-3195 pack run.fixed.log ran on that kernel.
Confirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3196/README.md (full analysis)', 'findings/poc/DF-3194/fix.diff hunk 3 (the fix)', 'sys/vfs/nfs/nfs_node.c:271-273 vs :344-348']
PoC changes
n/a (no standalone PoC; would require the same racing harness as DF-3194 plus readdirplus traffic)
Verified recommended fix
vp = NULL; before the goto loop at nfs_node.c:273 (hunk 3 of DF-3194's fix.diff).
Verdict
Latent defect visible by inspection: nfs_nget_nonblock()'s revalidation restart (nfs_node.c:271-273) executes vput(vp) and goto loop without resetting vp, so any execution that takes that branch double-vputs the stale vnode (second vput either at the next matching node or at the :284 cleanup) -> refcount underflow -> premature vnode recycling / vrele assert panic. The branch requires a stale bucket view (preemption of the token-holding scanner inside the restart window plus re-creation of the matching node by a third thread) - the same window class as DF-3194 but narrower; not dynamically triggerable on the QEMU guest within this run (the wider DF-3194 window already failed to fire in ~620k races). The one-line fix (vp = NULL before goto loop, mirroring the :344-348 comment's own rule) is included as hunk 3 of DF-3194's fix.diff and was compiled+booted in the DF-3195 validation kernel.
No comments yet.