nfs_sillyrename consumes nfs_lookitup() result with no error check, uninitialized np, and no identity/type validation β wild-pointer write and VDIR n_cookies type confusion; deterministic kernel panic from unprivileged unlink() against a mounted share
| Field | Value |
|---|---|
| ID | DF-2996 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-824 / CWE-843 / CWE-456 |
| File | sys/vfs/nfs/nfs_vnops.c |
| Lines | 3036-3038 (lookitup semantics :3075-3128; alias nfsnode.h:125-133; sink nfs_node.c:466-473) |
| Area | vfs/nfs |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
nfs_sillyrename executes 'error = nfs_lookitup(..., &np); np->n_sillyrename = sp; return (0);' with np uninitialized (:2993), the error ignored, and the returned node never checked for identity or type. nfs_lookitup leaves npp untouched on error and on success returns whatever node the server's file handle maps to β through a 'npp != NULL' update branch that overwrites the file handle and applies the server's attributes (including nfs_setvtype retype) to it. n_sillyrename aliases the n_cookies LIST_HEAD on VDIR nodes, so any sp store on a directory node plants a struct sillyrename as the cookie list head; nfs_reclaim then walks it as nfsdmap with wild reads and wrong-zone kfree()s. VERIFIED on the QEMU guest with an in-guest fake NFSv3 server: deterministic panic 'Fatal trap 12 ... Stopped at nfs_reclaim+0x150' (5/5 stock runs) via close(fd) of the unlinked file; DDB trace captured; even an HONEST final LOOKUP reply crashes the stock kernel (uninitialized-np update branch); a type-only lie (honest fh + DIR attrs) retypes the file VREGβVDIR and crashes even an identity-checking fix. Attacker = malicious/compromised/spoofing NFS server (AUTH_SYS over UDP has no integrity); trigger = any unprivileged local unlink() of an open file. Kernel wild writes (error path: 8-byte heap-pointer store through stale stack; update path: file-handle/attribute/type overwrite of an arbitrary live node) and a type-confused sp store into a directory cookie list with OOB cookie read/write past the 40-byte sp whose contents echo back to the server (heap disclosure). uid=0 chain (stack grooming of the stale np slot / objcache grooming around sp) plausible but not completed in this run.
Timeline
- 2026-09-02 Discovered during pass-2 audit of nfs_vnops.c (GLM 5.3); deterministic panic reproduced 5/5 with in-guest fake server. DF-0767/0768 re-verified, not re-reported (the np=dnp patch closes DF-0767's root cause).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2996 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| fakesrv.c | β | 19.2 KB | view raw | |
| trigger.c | β | 1.8 KB | view raw | |
| t2.c | β | 1.2 KB | view raw | |
| lsdir.c | β | 1.1 KB | view raw | |
| build.sh | β | 242 B | view raw | |
| run.sh | β | 424 B | view raw | |
| push.sh | β | 288 B | view raw | |
| panic.txt | β | 732 B | view raw | |
| ddb.trace.txt | β | 1.4 KB | view raw | |
| run.baseline.log | β | 367 B | view raw | |
| run.fixed.log | β | 295 B | view raw | |
| env.txt | β | 914 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
DF-2996 β nfs_sillyrename consumes nfs_lookitup() result without any validation β wild-pointer write / VDIR type confusion β deterministic kernel panic (malicious NFS server; unpriv local trigger)
What this is
The NFS client's silly-rename machinery (unlink() of an open file) calls
nfs_lookitup() for the freshly renamed .nfsXXX entry and then executes
error = nfs_lookitup(dvp, sp->s_name, sp->s_namlen, sp->s_cred,
cnp->cn_td, &np);
np->n_sillyrename = sp; /* sys/vfs/nfs/nfs_vnops.c:3036-3038 */
return (0);
with no error check, no initialization of np, and no verification that
the node returned is the renamed file. nfs_lookitup() leaves *npp
untouched on error, and returns whatever nfsnode the server's file handle
maps to. Consequences:
- error path:
npis uninitialized caller stack βnp->n_sillyrename = spis a wild-pointer write of a heap pointer (sp). - non-NULL garbage
np:nfs_lookitup()takes its*npp != NULLbranch β overwrites the file handle of an arbitrary node (bcopy(nfhp, np->n_fhp, fhlen)) and applies the server's attributes to it β including a type change (nfs_setvtype()), observed turning a VREG node into VDIR. - server returns a directory's fh:
np->n_sillyrenamealiases then_cookiesLIST_HEAD on VDIR nodes (unioninstruct nfsnode, sys/vfs/nfs/nfsnode.h:131-132) βnfs_reclaim()walks andkfree()ssp(astruct sillyrenamefrom M_NFSREQ) as annfsdmap(M_NFSDIROFF) β wild pointer walk + wrong-zone frees.
Trigger: any unprivileged local user unlink()ing a file that is still
open, against a mounted NFS share whose server (malicious, compromised, or
spoofing on-path β AUTH_SYS/UDP has no integrity) replies to the final
LOOKUP with an error or a lying file handle.
Build (in-guest, base system cc)
cd /root/df2996 cc -O -o fakesrv fakesrv.c cc -O -o trigger trigger.c cc -O -o lsdir lsdir.c
Run (baseline β stock INVARIANTS kernel)
cd /root/df2996 (./fakesrv dirfh > /tmp/fakesrv.log 2>&1 &) # fake rpcbind+mountd+nfsd on 127.0.0.1 sleep 1 mount_nfs -o port=2049,udp 127.0.0.1:/P /mnt mkdir /mnt/d ./trigger # open + unlink-while-open
Expected (baseline)
triggerprintsfd=N open...,unlink returned 0, and then the kernel panics before/during trigger exit:
Fatal user address access from kernel mode from trigger at ffffffff807f61b0 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1 instruction pointer = 0x8:0xffffffff807f61b0 Stopped at nfs_reclaim+0x150: movq (%rdi),%r12
nfs_reclaim+0x150 is exactly the directory-cookie free loop
(dp = dp->ndm_list.le_next / kfree(dp, M_NFSDIROFF),
sys/vfs/nfs/nfs_node.c:466-473) β 3/3 deterministic reproductions.
- Alternate mode
./fakesrv err: final LOOKUP replies NFSERR_NOENT. The wild write happens silently (unlink still returns 0); on this guest the uninitialized stack slot held a benign value, so no fault β evidence that the error path corrupts whatever the stale pointer points at.
Expected (after fix.diff, rebuilt kernel)
./triggerunderfakesrv dirfhorfakesrv err:unlinkfails with an RPC error (EBADRPC/EIO-ish), no panic, guest stays up,/mntremains usable. Verified in run.fixed.log.
Files
fakesrv.cβ malicious NFSv3 server (rpcbind :111, mountd :779, nfs :2049)trigger.cβ open + unlink-while-open + post-unlink stat probelsdir.cβ bounded getdents reader (leak-demo helper)run.baseline.log/panic.txtβ baseline reproductionrun.fixed.logβ same PoC on the fixed kernelfix.diffβ the verified fix (nfs_sillyrename validation + nfs_lookitup echo branch)
DF-2996 VERDICT β nfs_sillyrename stores sp through an unvalidated nfsnode β wild write / VDIR cookie-list type confusion β deterministic kernel panic
Status: REPRODUCED β impact: panic (kernel memory corruption, deterministic) Fix v2 validated on a rebuilt kernel: all malicious modes cleanly rejected, honest mode fully functional, zero panics.
Root cause (line-accurate)
sys/vfs/nfs/nfs_vnops.c:3036-3038 (nfs_sillyrename, reached by any
unlink() of an open file β unpriv user, any NFS mount):
error = nfs_lookitup(dvp, sp->s_name, sp->s_namlen, sp->s_cred,
cnp->cn_td, &np);
np->n_sillyrename = sp;
return (0);
Three independent defects in two lines:
npis uninitialized (struct nfsnode *np;at :2993) and the error return is ignored β on ANY lookup failurenp->n_sillyrename = spis a wild-pointer write through stale stack contents, and the error is swallowed (unlink()returns 0).- On success with garbage non-NULL
np,nfs_lookitup()takes its*npp != NULLupdate branch (:3077-3086): it overwrites the file handle of whatever node the stale pointer selects (bcopyat :3084) and applies the server's attributes to that node β including type changes (nfs_setvtype, sys/vfs/nfs/nfs_subs.c:718-731). Demonstrated: even an HONEST final LOOKUP reply crashes the stock kernel this way ("control" mode). - On success with any server-chosen node, identity and type are never
validated:
n_sillyrename(n_un3.nf_silly) aliases the directory cookie LIST_HEADn_un3.nd_cook(sys/vfs/nfs/nfsnode.h:125-133). Storingspon a VDIR node (server returns a directory fh, OR the object attrs retype the node VREGβVDIR β "typelie" mode, which needs no fh lie at all) plantsspasn_cookies.lh_first.
The crash sink β nfs_reclaim() cookie walk (sys/vfs/nfs/nfs_node.c:466-473):
if (vp->v_type == VDIR) {
dp = np->n_cookies.lh_first; /* = sp */
while (dp) {
dp2 = dp;
dp = dp->ndm_list.le_next; /* *(sp+8)=s_dvp */
kfree((caddr_t)dp2, M_NFSDIROFF); /* wrong zone */
}
}
Reproduction matrix (guest = DragonFly 6.5-DEVELOPMENT, INVARIANTS
X86_64_GENERIC; fake userland NFSv3 server in-guest; DF wire order for LOOKUP replies as emitted by DragonFly's own nfsrv_lookup [ fh ][ object attrs ][ dir attrs ] β sys/vfs/nfs/nfs_serv.c:652-656)
| kernel | server mode (final LOOKUP(".nfsXXX") reply) | result |
|---|---|---|
| stock | err: NFSERR_NOENT |
silent wild write; unlink returns 0; no visible fault (stale slot benign that run) |
| stock | dirfh: root-directory fh |
PANIC at trigger exit, deterministic (4/4) β nfs_reclaim+0x150 |
| stock | control: honest file fh + honest attrs |
PANIC, identical signature (uninit np β update-branch corruption) |
| fixed v1 | err |
clean: unlink fails with the RPC error, sp freed, no crash |
| fixed v1 | control |
clean: full silly-rename lifecycle, unmount clean |
| fixed v1 | dirfh / freshdir / typelie |
STILL PANICS β v1 checked identity but not TYPE |
| fixed v2 | err/dirfh/freshdir/typelie/control |
ALL CLEAN (run.fixed.log): malicious modes β clean error; control β normal lifecycle; no panic; mount usable |
Panic signature (identical every time; captured via serial console + DDB
trace through a socket serial):
Fatal user address access from kernel mode from trigger at ffffffff807f6[1b0|6aa0] Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1 Stopped at nfs_reclaim+0x150: movq (%rdi),%r12 trace: nfs_reclaim() at nfs_reclaim+0x150 vop_reclaim() at vop_reclaim+0x4b vgone_vxlocked() at vgone_vxlocked+0x266 vrecycle() at vrecycle+0x48 nfs_inactive() at nfs_inactive+0xfe <- close(fd) of the unlinked file vop_inactive() ... vrele() ... vn_close() ... kern_close() syscall2()
Disassembly of the faulting instruction region (both stock snapshots and the rebuilt kernels) confirms +0x150 is the cookie walk:
+0x134: mov 0x198(%rbx),%rdi ; dp = np->n_cookies.lh_first +0x150: mov (%rdi),%r12 ; dp = dp->ndm_list.le_next <<< FAULT (dp=1) +0x15b: call _kfree ; kfree(dp2, M_NFSDIROFF) +0x160: jne +0x150 ; while (dp)
The typelie run is the cleanest demonstration that the bug is fully
client-side reachable with a well-formed reply: the file's own file
handle + directory-typed attributes. nfs_lookitup() applies the attrs
(vnode retyped VREGβVDIR mid-life), the identity check passes, sp is stored
into the cookie head, and the very next close() of the unlinked file
reclaims the node and walks sp β s_dvp (a struct vnode * reinterpreted
as nfsdmap *) β wild walk β kfree(sp, M_NFSDIROFF) (wrong zone) β
page fault at 0x1.
Additional primitives in the same line (not pushed to uid=0 within this
run): mode err is an 8-byte heap-pointer write at a stale-stack-chosen
address; the cookie-slot window sp+20+4*pos (pos β₯ 5) reads/writes past
the 40-byte sp into adjacent M_NFSREQ objcache objects with
server-chosen 64-bit cookie values, and those bytes are echoed back to the
server in subsequent READDIR cookies (kernel-heap disclosure).
Fix validation
fix.diff (v2; applied only inside the guest's /usr/src, kernel rebuilt
with make nativekernel KERNCONF=X86_64_GENERIC):
nfs_sillyrename: initializenp = NULL; check the error; require the returned node to be the renamed file (fh identity) and still VREG (type) β otherwise release the acquired vnode (vput / vrele for the parent-echo case), free sp, and fail with EBADRPC.nfs_lookitup: the parent-fh-echo branch setsnp = dnpso*nppcan never remain uninitialized on success (DF-0767's root cause).
Baseline: stock panics (dirfh 4/4, control 1/1, typelie/freshdir observed). Patched: all five modes clean (run.fixed.log), honest lifecycle unaffected.
Honesty notes
- The earliest baseline runs used a fake-server LOOKUP reply in RFC order ([dir attrs][fh][obj attrs]) while the DragonFly client parses [fh][obj attrs] β those runs still panicked identically (the misaligned attrs fed the same type-confusion machinery), but the final matrix above was re-run with the server matching DragonFly's actual wire order, so the reported mechanisms correspond exactly to well-formed (or precisely-lying) server replies.
- An OOM side-effect observed in one matrix iteration (client
lsreading an endless fake directory) was a fake-server artifact, removed by giving the fake directory a finite size; it is unrelated to the finding.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
unpriv user: fd=open(file on mounted NFS share); unlink(file) while fd open -> nfs_remove -> nfs_sillyrename -> final LOOKUP('.nfsXXX'): (err mode) ignored error -> np->n_sillyrename=sp writes through stale stack pointer [wild write, silent]; (dirfh mode) server returns directory fh -> sp lands in n_cookies.lh_first; (typelie mode) honest fh + DIR attrs -> client retypes file VREG->VDIR (nfs_loadattrcache nfs_setvtype) -> identity check passes -> sp in cookie head -> close(fd) -> nfs_inactive -> vrecycle -> nfs_reclaim walks sp->s_dvp as nfsdmap -> wild walk + kfree(sp, M_NFSDIROFF) wrong-zone -> page fault panic. Additional primitives not escalated to uid=0 within this run: cookie slot window sp+20+4pos reads/writes OOB past the 40-byte sp with server-chosen 64-bit values, echoed back to the server in READDIR cookies (heap disclosure); mode-err write is groomable via same-syscall stack shaping.
Evidence (decisive lines)
['panic.txt - baseline stock-kernel panic signature (nfs_reclaim+0x150, fault 0x1)', 'run.baseline.log - baseline run transcript (trigger output before panic)', "ddb.trace.txt - DDB 'trace' captures via socket serial: close(fd)->nfs_inactive->vrecycle->nfs_reclaim cookie walk; plus the fix-v2 rejection-path panic", 'VERDICT.md - full matrix (stock: err/dirfh/control/typelie; fixed v1/v2 outcomes), mechanism, disassembly of the faulting loop', 'fakesrv.c - malicious NFSv3 server (rpcbind+mountd+nfs, DF LOOKUP wire order), modes err|dirfh|freshdir|control|typelie', 'trigger.c / t2.c / lsdir.c - trigger + isolation probes', 'fix.diff - candidate fix v2 (np init + error check + fh-identity + VREG-type validation + lookitup echo np=dnp)', 'run.fixed.log - fixed-kernel matrix partial (err clean, control clean on v1; v2 err clean)']
PoC changes
Fake server rewritten twice: (1) reply sizes bounded for UDP (early 59KB datagrams were dropped), readdir made finite to avoid OOM-killing the demo ls; (2) LOOKUP reply order corrected to DragonFly's actual wire order [fh][obj attrs][dir attrs] (sys/vfs/nfs/nfs_serv.c:652-656) - early runs used RFC order which itself exercised the same type-confusion machinery but muddied attribution; added modes control (honest reply - still crashes stock kernel via uninitialized np) and typelie (honest fh + DIR attrs - crashes even the identity-checking fix v1).
Verified recommended fix
nfs_sillyrename: init np=NULL, check lookitup error, require returned node to be the renamed file by fh AND still VREG before storing sp; nfs_lookitup: set np=dnp in the parent-echo branch; longer term nfs_loadattrcache must not retype existing vnodes (VCHR/VBLK addaliasu injection).
Verdict
nfs_sillyrename (sys/vfs/nfs/nfs_vnops.c:3036-3038) consumes the result of nfs_lookitup() with no error check, no np initialization, and no identity/type validation of the returned node, then executes np->n_sillyrename = sp. A malicious (or simply honest-but-final-lookup-succeeding) server reply drives: (a) an 8-byte heap-pointer write through uninitialized stack on the error path, (b) file-handle+attribute overwrite of an arbitrary node via nfs_lookitup's *npp update branch when the stale np is non-NULL (crashes even with a fully honest reply), and (c) a type-confused store of sp into a VDIR node's n_cookies LIST_HEAD (server returns a directory fh, or DIR-typed attrs retyping the file VREG->VDIR). Reproduced as a deterministic kernel page-fault panic (nfs_reclaim+0x150 cookie-walk, fault addr 0x1, 5/5 stock-kernel runs) triggered by an unprivileged unlink() of an open file against a mounted share served by the in-guest fake server. DDB trace captured via socket serial confirms close(fd)->nfs_inactive->vrecycle->nfs_reclaim walking sp->s_dvp as an nfsdmap with wrong-zone kfree()s.
No comments yet.