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=
Timeline
- 2026-09-02 Discovered during pass-2 audit of nfs_serv.c (GLM 5.3); remote pre-auth panic reproduced.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2994 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
-publicandindex=<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).
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
fixedPatched kernel answers the identical public-fh LOOKUP with a normal reply; no panic, guest stays up. Bad behavior gone.
['run.fixed.log', 'panic.txt']
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=
No comments yet.