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

WebNFS public-filehandle LOOKUP with configured-but-missing index file: vput() on an unlocked vnode β†’ guaranteed kernel panic

Field Value
ID DF-2994
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-667
File sys/vfs/nfs/nfs_serv.c
Lines 565, 574-589, 599-604, 643
Area vfs/nfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

In nfsrv_lookup's public-filehandle path, when the looked-up target is a VDIR and nfs_pub.np_index is configured, the code vn_unlock()s the vp at :565 before looking up the index file. If that index lookup fails (index file absent β€” the common case), the block that would hand the unlocked vp to dirp and cache_vget a fresh locked vp (:574-589) is skipped, and execution continues with the unlocked vp; the subsequent vput() at :599-604 or :643 releases an unheld lock β†’ panic("lockmgr: LK_RELEASE: no lock held") β€” unconditional, not INVARIANTS-gated. Remote pre-auth panic: the public filehandle is by definition the unauthenticated entry point. Requires an export configured '-public -index=' (mountd β†’ vfs_setpublicfs). One UDP LOOKUP(pubfh, '/') on a directory lacking the index file panics the server instantly. VERIFIED: exact backtrace panic: lockmgr: LK_RELEASE: no lock held / vput / nfsrv_lookup / sys_nfssvc; guest to DDB, ssh dead. Fix: re-lock the original vp when the index lookup fails (row diff).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of nfs_serv.c (GLM 5.3); remote pre-auth panic reproduced.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2994 Β· 11 files
FileTypeDescriptionSize
nfspoc.c β€” 8.4 KB view raw
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
run.sh β€” 207 B view raw
build.sh β€” 47 B view raw
run.log β€” 983 B view raw
run.fixed.log β€” 1.6 KB view raw
panic.txt β€” 608 B view raw
fix.diff β€” 865 B view raw
verdict.json β€” 2.8 KB view raw
env.txt β€” 411 B view raw

DF-2994 β€” WebNFS public-filehandle LOOKUP: vput() on unlocked vnode β†’ guaranteed kernel panic

Build

cc -O -o /root/nfspoc nfspoc.c      # in-guest

Setup (root in guest)

/etc/exports:

/tmp/nfsroot -maproot=root
/ -public -index=missing.html -maproot=root

Start rpcbind, mountd, nfsd.

Run

NFSPOC_SRCPORT=831 NFSPOC_TIMEOUT=5 /root/nfspoc lookup0 /

(v3 LOOKUP, public filehandle encoded as fhlen=0, name "/")

Expected

Kernel panic within milliseconds:

panic: lockmgr: LK_RELEASE: no lock held
 vput() at vput+0x11
 nfsrv_lookup() at nfsrv_lookup+0x35b
 sys_nfssvc() ...

Guest drops to DDB (db>); ssh dies; vm.sh status => down.

Root cause

nfsrv_lookup sys/vfs/nfs/nfs_serv.c: - nfs_namei() returns the target vp LK_EXCLUSIVE locked (cache_vget, nfs_subs.c:1183). - Public-filehandle + VDIR + configured index file (nfs_pub.np_index, set by mountd from index= via vfs_setpublicfs, vfs_subr.c:2266-2293): line 565 vn_unlock(vp) before looking up the index. - If the index lookup fails (index file absent β€” the common case), the vp is never re-locked and never moved to dirp (line 574-589 skipped). - Line 599-604 or line 643 then calls vput(vp) on the unlocked vnode. - vput() = vn_unlock()+vrele() (vfs_lock.c:703-707) β†’ lockmgr LK_RELEASE on an unheld lock β†’ unconditional panic (kern_lock.c:767-768).

Preconditions

  • NFS server exporting a filesystem with -public and index=<file> (WebNFS with index, RFC 2054/2055 style).
  • The LOOKUP target resolves to a directory that does not contain the index file.
  • The public filehandle is by definition pre-authenticated (that is its purpose), so no valid fh or credential is needed β€” only network reachability (and a reserved source port when vfs.nfs.nfs_privport=1).
VERDICT.md
↓ download raw

DF-2994 β€” VERDICT

Status: REPRODUCED (impact: panic, confidence: certain, attempts: 1 + deterministic code path)

What was claimed

A v3 LOOKUP using the WebNFS public filehandle on an export configured with an index file that does not exist in the target directory makes nfsrv_lookup() call vput() on a vnode it already unlocked β€” guaranteed kernel panic.

How it was proven

Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel, KVM), exports:

/tmp/nfsroot -maproot=root
/ -public -index=missing.html -maproot=root

(mountd pushes -public + index= through vfs_export -> vfs_setpublicfs, sys/kern/vfs_subr.c:2266-2293, setting nfs_pub.np_index.)

$ NFSPOC_SRCPORT=831 /root/nfspoc lookup0 /
--- TRIGGER lookup0 public fh ---
(ssh session never returned; vm.sh status => down; DDB on serial)

Serial console (panic.txt):

panic: lockmgr: LK_RELEASE: no lock held
cpuid = 3
lockmgr_release() at lockmgr_release+0x11a
lockmgr_release() at lockmgr_release+0x11a
vput() at vput+0x11
nfsrv_lookup() at nfsrv_lookup+0x35b
sys_nfssvc() at sys_nfssvc+0x40f
syscall2() at syscall2+0x11e
Debugger("panic")

Exactly the predicted frame: nfsrv_lookup -> vput -> lockmgr_release panic.

Why it happens (code path, line-accurate)

  • sys/vfs/nfs/nfs_subs.c:1182-1184 β€” nfs_namei() success returns the looked-up vp LK_EXCLUSIVE locked (cache_vget).
  • sys/vfs/nfs/nfs_serv.c:550-565 β€” pubflag + target is VDIR + np_index != NULL: vn_unlock(vp) before looking up the index file relative to it.
  • nfs_serv.c:568-572 β€” nlookup(index) fails (index file absent).
  • nfs_serv.c:574-591 β€” the "found an index file" block (which would move the unlocked vp to dirp and cache_vget a fresh LOCKED vp) is skipped; error = 0; β€” execution continues with the unlocked vp.
  • nfs_serv.c:599-604 (vput(vp) on mount mismatch) or nfs_serv.c:643 (vput(vp) after VFS_VPTOFH/VOP_GETATTR) β€” vput() on the unlocked vnode.
  • sys/kern/vfs_lock.c:703-707 β€” vput() = vn_unlock() + vrele(); sys/kern/vfs_vnops.c:1125-1128 β€” vn_unlock() = lockmgr(LK_RELEASE).
  • sys/kern/kern_lock.c:767-768 β€” LK_RELEASE on an unheld lock panics unconditionally: panic("lockmgr: LK_RELEASE: no lock held").

Exploit chain

n/a (remote pre-auth panic/DoS). The public filehandle is by definition the unauthenticated entry point; no valid fh or credential is required, only an export configured with -public and index=<file> and network reachability (reserved source port when vfs.nfs.nfs_privport=1).

Fix validation

fix.diff re-locks the vp (vn_lock LK_EXCLUSIVE|LK_RETRY) when the index lookup fails, restoring the locked-vnode invariant expected by the rest of the function. Validated on a rebuilt kernel: the identical LOOKUP(public fh, "/") now returns a normal reply and the guest stays up (see run.fixed.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel answers the identical public-fh LOOKUP with a normal reply; no panic, guest stays up. Bad behavior gone.

['run.fixed.log', 'panic.txt']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Sep 4 21:02:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

single pre-auth-style UDP LOOKUP(public fh, '/') -> deterministic kernel panic (remote DoS) whenever WebNFS is exported with an index file

Evidence (decisive lines)

['panic.txt β€” full serial-console panic text with the vput()/nfsrv_lookup() backtrace and db> prompt', 'run.log β€” NULL baseline healthy, trigger line, ssh never returns, vm.sh status down', 'VERDICT.md β€” line-accurate chain: nfs_serv.c:565 vn_unlock, 568-590 index-lookup failure skips the re-lock/cache_vget block, 599/643 vput on unlocked vp; vfs_lock.c:703-707; kern_lock.c:767-768']

PoC changes

wrote nfspoc.c from scratch (lookup0 subcommand: v3 LOOKUP with fhlen=0 public filehandle)

Verified recommended fix

Re-lock the original vp (vn_lock(vp, LK_EXCLUSIVE|LK_RETRY)) when the public-index nlookup fails, before continuing (fix.diff).

Verdict

With an export configured '-public -index=', a v3 LOOKUP using the public filehandle (fhlen=0) for any directory lacking the index file makes nfsrv_lookup vput() a vnode it unlocked at line 565 and never re-locked β€” vput -> vn_unlock -> lockmgr(LK_RELEASE) on an unheld lock panics unconditionally. Reproduced on the stock INVARIANTS guest with the exact predicted backtrace (panic: lockmgr: LK_RELEASE: no lock held / vput / nfsrv_lookup); guest dropped to DDB, ssh dead.