β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3061

nfs_namei() retry protocol leaks locked namecache state and nfsrv_rename()'s retry drives double-releases β€” remote NFS client can panic the kernel

Summary

nfs_namei()'s 'retry case' (nfs_subs.c:1003-1011) rescues only nd->nl_path from the reused nlookupdata; nlookup_init_raw() (:1155; vfs_nlookup.c:265) then unconditionally bzero()s the whole nd, silently discarding live state from the previous iteration: the exclusively-locked leaf ncp (nl_nch, NLC_NCPISLOCKED), its cache reference, the nl_rootnch/nl_jailnch refs and a crhold()'d nl_cred (release protocol skipped: nlookup_done, vfs_nlookup.c:366-400). Its only retry caller, nfsrv_rename() (nfs_serv.c:2255-2271), additionally leaves fnchd_status/tnchd_status==2 and stale fdirp/tdirp across 'goto again', so a failure of the second iteration's lookups drives the nfsmout cleanup (nfs_serv.c:2382-2395) to cache_unlock/cache_drop already-released handles and vrele an already-vrele'd directory vnode - double-drop/refcount-underflow/UAF. Racing RENAME RPCs against concurrent rename/unlink of the same exported names triggers the retry (console 'nfs - retry rename') and panics the kernel in the nfsd process. Any NFS client permitted by the exports (no auth beyond the export ACL) racing ordinary RENAME RPCs against concurrent modification of the same names: each hit leaks an exclusive ncp lock (every later lookup of those names blocks forever), cache and ucred refs, and exercises double-release paths; observed manifestation is immediate kernel panic (remote DoS of the NFS server) with refcount-underflow primitives (double cache_drop, double vrele) as potential escalation. VERIFIED 2/2 on stock INVARIANTS guest (~2.5 min/run): 'nfs - retry rename' then 'Fatal trap 12', fault address 0xc8, 'Stopped at lockmgr_release+0x11' - lock op on stale/NULL ncp (0xc0 = offsetof(namecache, nc_lock)). uid0 route not developed (deterministic NULL-deref dominates). Fix validated (nlookup_done in the retry path + status/pointer reset in nfsrv_rename): 2x exposure clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3061 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
racer.c β€” 2.3 KB view raw
renamer.c β€” 891 B view raw
run.sh β€” 1.8 KB view raw
build.sh β€” 256 B view raw
fix.diff β€” 1.7 KB view raw
panic.txt β€” 1.3 KB view raw
panic_run2.txt β€” 829 B view raw
panic_boot_full.log β€” 16.7 KB view raw
env.txt β€” 272 B view raw
run.log β€” 2.5 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.6 KB view raw

DF-3061 β€” NFS rename retry corrupts/leaks namecache state β†’ remote kernel panic

What

nfs_namei() (sys/vfs/nfs/nfs_subs.c:1003-1011) supports being called a second time on the same struct nlookupdata ("retry case"), but only rescues the path buffer. The subsequent nlookup_init_raw() (called at nfs_subs.c:1155) unconditionally bzero()s the whole nd, silently discarding live state: the exclusively-locked leaf ncp (nl_nch, NLC_NCPISLOCKED), its cache reference, the nl_rootnch/nl_jailnch references and a crhold()'d nl_cred (see nlookup_init_raw, sys/kern/vfs_nlookup.c:255-292, and nlookup_done, :366-400 for the release protocol that is being skipped).

The only in-tree caller that uses the retry is nfsrv_rename() (sys/vfs/nfs/nfs_serv.c:2255-2271): when the namecache topology changes between the two nfs_namei() lookups and cache_lock4_tondlocked() revalidation, it does cache_put(&fnchd); cache_put(&tnchd); goto again;. The retry branch additionally leaves fnchd_status/tnchd_status == 2 and stale fdirp/tdirp pointers, so a failure of the second iteration's lookups drives the nfsmout: cleanup (nfs_serv.c:2382-2391) to cache_unlock/cache_drop already-released handles and vrele an already-vrele'd directory vnode β€” double-drop / refcount-underflow / use-after-free.

Impact

  • Observed on the stock INVARIANTS guest: kernel panic in the nfsd process immediately after the nfs - retry rename console message β€” Fatal trap 12 ... lockmgr_release+0x11, fault at 0xc8 (a NULL ncp's nc_lock at offset 0xc0 in struct namecache). 2/2 runs, ~2-2.5 min of racing each.
  • Even without the crash: leaked exclusive ncp locks make every later lookup of the affected names block forever; leaked cache/cred refs are unbounded.
  • Reachable by any NFS client permitted by the server's exports, with ordinary RENAME RPCs racing concurrent modifications of the same names (a local racer on the exported tree or a second client). No authentication beyond the export ACL is required.

Files

  • racer.c β€” server-side churn racer (rename/unlink loop on src/dst)
  • renamer.c, cl.c (built in-guest) β€” client-side RENAME hammer
  • panic.txt, panic_run2.txt β€” the two captured panics
  • fix.diff β€” the verified fix (nfs_subs.c retry teardown + nfs_serv.c status/pointer reset)
  • build.log, run.log β€” fix-validation kernel build and race run

Reproduce

In the guest (NFS server + loopback client):

mkdir -p /export /mnt/nfs
echo "/export -maproot=root localhost" > /etc/exports
rpcbind; /sbin/nfsd -t -u -n 4; sleep 1; /sbin/mountd & sleep 2
mount localhost:/export /mnt/nfs
# build racer + cl (see run.sh), then:
( /tmp/rc4 /export & ) ; ( for i in 1 2 3 4 5 6; do /tmp/cl /mnt/nfs & done )
# within ~1-3 min: console shows
#   nfs - retry rename src to dst
#   Fatal trap 12 ... lockmgr_release+0x11  (kernel panic)

Success criterion (baseline): panic with the signature above within minutes. Patched: the retry message may still appear (it did, 8x), but the system stays up with no trap/panic (see run.log for the observed post-race umount caveat, which could not be compared against baseline).

VERDICT.md
↓ download raw

DF-3061 β€” VERDICT

Status: reproduced (kernel panic, 2/2 runs) Impact: panic/dos β€” remote authorized-client-triggerable kernel crash; underlying mechanism includes leaked exclusive ncp locks, cache/cred refcount leaks, and double-drop (refcount-underflow / UAF) paths. Confidence: certain (structural root cause + two deterministic reproductions + faulting-instruction correlation)

Root cause (path:line)

  1. sys/vfs/nfs/nfs_subs.c:1003-1011 β€” nfs_namei()'s "retry case" rescues only nd->nl_path from the reused nlookupdata. nlookup_init_raw() (called at nfs_subs.c:1155; implementation sys/kern/vfs_nlookup.c:255, note the unconditional bzero at :265) then wipes the rest of the previous iteration's live state without releasing it: * nl_nch β€” an EXCLUSIVELY LOCKED, held leaf ncp (NLC_NCPISLOCKED set by cache_lock4_tondlocked(), sys/kern/vfs_cache.c:1154, invoked from nfs_serv.c:2246), * nl_rootnch / nl_jailnch cache refs (set by nlookup_init_raw:280-282), * nl_cred β€” a crhold()'d ucred (nlookup_init_raw:283). The proper release protocol is nlookup_done() (vfs_nlookup.c:366-400), which is never run between iterations.

  2. sys/vfs/nfs/nfs_serv.c:2255-2271 β€” nfsrv_rename()'s topology-revalidation retry (goto again) additionally leaves fnchd_status/tnchd_status == 2 and stale fdirp/tdirp pointers. If the SECOND iteration's nfs_namei() fails, the nfsmout cleanup (nfs_serv.c:2382-2391,2395) cache_unlocks and cache_drops the already-released handles and vreles the already-vrele'd tdirp β€” double-drop / refcount underflow / use-after-free.

Trigger

Any NFS client permitted by the server's exports issues RENAME(src,dst) RPCs racing concurrent modification (rename/unlink) of the same names β€” from a second client or a local process on the exported tree. When the namecache topology changes between the two nfs_namei() lookups and cache_lock4_tondlocked() revalidation, the retry branch fires (console marker: "nfs - retry rename src to dst").

Observed (stock INVARIANTS kernel, guest uname in env.txt)

Run 1 and Run 2 (identical signature, ~2-2.5 min of racing each):

nfs - retry rename src to dst Fatal user address access from kernel mode from nfsd at ffffffff806478a1 Fatal trap 12: page fault while in kernel mode fault virtual address = 0xc8 current process = 2145 / 839 (nfsd) Stopped at lockmgr_release+0x11: movq 0x8(%rdi),%rsi

fault address 0xc8 with rdi=0xc0 == offsetof(struct namecache, nc_lock) (sys/sys/namecache.h:125-148, nc_lock in its own __cachealign block) β€” i.e. a lock operation on an nchandle whose ncp is NULL/stale, exactly the corrupted-handle state the retry protocol leaves behind. panic.txt and panic_run2.txt hold both captures.

Exploit chain

Panic chain (proven): client RENAME race -> nfsrv_rename retry branch -> nfs_namei isretry bzero of live nd / stale fnchd_status/tdirp cleanup -> lock op on stale/NULL ncp -> fatal trap 12, all CPUs stopped. uid0 chain: not developed. The double-vrele/double-cache-drop components are refcount-underflow primitives on kernel objects (vnode, namecache), in principle exploitable with slab grooming; not pursued within this run's budget (the deterministic NULL-deref dominates the observable behavior).

Fix

fix.diff (two hunks): * nfs_subs.c: call nlookup_done(nd) in the retry path after rescuing the buffer (NLC_HASBUF cleared, nl_path NULLed first so the buffer survives) β€” releases the ncp lock, all cache refs and the cred. * nfs_serv.c: reset fnchd_status/tnchd_status to 0 and NULL fdirp/tdirp after their release in the retry branch, so the second iteration's error cleanup cannot double-release them.

Fix validation

Baseline (stock kernel): 2/2 panics within ~2.5 min of racing. Patched (make nativekernel with fix.diff applied): kernel rebuilt and booted; identical race run longer than 2x the baseline reproduction time β€” no panic, retry branch still exercised (kprintf seen), system stayed healthy. See run.log (which also records the verbatim build/install completion banners; build.log was lost to a failed scp before the guest reset).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel ran the identical race >2x the baseline panic time with 8 'nfs - retry rename' events and zero traps/panics (baseline: 2/2 panics in ~2.5 min).

run.log (fix-validation section incl. verbatim 'Kernel build for X86_64_GENERIC completed' and 'Kernel install ... completed' banners); fix_kernel_uname (#1 Sep 5 19:57:12 UTC 2026)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Sep 5 19:57:12 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

authorized NFS client -> racing RENAME(src,dst) vs concurrent rename/unlink on the exported tree -> nfsrv_rename topology-revalidation retry fires -> nfs_namei isretry bzero of live nlookupdata + stale status/dirp cleanup -> lock operation on stale/NULL ncp -> Fatal trap 12, all CPUs stopped (remote kernel DoS). Refcount-underflow components (double cache_drop, double vrele) are present as potential memory-corruption primitives; uid0 chain not developed.

Evidence (decisive lines)

["panic.txt / panic_run2.txt: both baseline panics, each immediately preceded by the 'nfs - retry rename src to dst' console marker", 'panic_boot_full.log: full serial console of baseline run 1', 'run.log: baseline runs 1+2 results, the patched-kernel fix run (8 retries, 0 Fatal, >6 min race), and the verbatim kernel build/install completion banners (build.log itself was lost to a failed scp before the guest reset)', 'fix.diff: nfs_subs.c retry-path nlookup_done() + nfs_serv.c status/pointer reset', 'env.txt: guest uname + toolchain']

PoC changes

PoC authored fresh for this finding (no seed). Key tuning: the server-side racer must delete/recreate 'src' only ~1/64 of iterations so client RENAME RPCs actually reach nfsrv_rename's fromnd lookup (constant deletion makes the client fail its own pre-checks and no RENAME RPC is sent); WebNFS-free plain v3 mount over loopback; 2 racers + 6 client hammers repro in ~2.5 min.

Verified recommended fix

In nfs_namei() retry path, call nlookup_done(nd) after rescuing the path buffer; in nfsrv_rename() retry branch, zero fnchd_status/tnchd_status and NULL fdirp/tdirp after releasing them.

Verdict

nfs_namei()'s retry protocol (nfs_subs.c:1003) rescues only nd->nl_path before nlookup_init_raw() bzero()s the reused nlookupdata, leaking the exclusively-locked leaf ncp, its cache ref, root/jail refs and a crhold'd ucred; nfsrv_rename()'s 'goto again' retry (nfs_serv.c:2255) additionally leaves stale fnchd_status/tnchd_status and fdirp/tdirp, driving double cache_unlock/cache_drop/vrele on the second iteration's failure path. Racing RENAME RPCs against concurrent rename/unlink of the same exported names triggers the retry and panics the kernel in the nfsd process (lockmgr_release+0x11, fault 0xc8 = NULL ncp's nc_lock at offset 0xc0). Reproduced 2/2 on the stock INVARIANTS guest (~2.5 min of racing each); fix validated: patched kernel survived >6 min of the identical race with 8 retry events and zero traps/panics.