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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2996 Β· 17 files
FileTypeDescriptionSize
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:

  1. error path: np is uninitialized caller stack β†’ np->n_sillyrename = sp is a wild-pointer write of a heap pointer (sp).
  2. non-NULL garbage np: nfs_lookitup() takes its *npp != NULL branch β†’ 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.
  3. server returns a directory's fh: np->n_sillyrename aliases the n_cookies LIST_HEAD on VDIR nodes (union in struct nfsnode, sys/vfs/nfs/nfsnode.h:131-132) β†’ nfs_reclaim() walks and kfree()s sp (a struct sillyrename from M_NFSREQ) as an nfsdmap (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)

  • trigger prints fd=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)

  • ./trigger under fakesrv dirfh or fakesrv err: unlink fails with an RPC error (EBADRPC/EIO-ish), no panic, guest stays up, /mnt remains 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 probe
  • lsdir.c β€” bounded getdents reader (leak-demo helper)
  • run.baseline.log / panic.txt β€” baseline reproduction
  • run.fixed.log β€” same PoC on the fixed kernel
  • fix.diff β€” the verified fix (nfs_sillyrename validation + nfs_lookitup echo branch)
VERDICT.md
↓ download raw

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:

  1. np is uninitialized (struct nfsnode *np; at :2993) and the error return is ignored β€” on ANY lookup failure np->n_sillyrename = sp is a wild-pointer write through stale stack contents, and the error is swallowed (unlink() returns 0).
  2. On success with garbage non-NULL np, nfs_lookitup() takes its *npp != NULL update branch (:3077-3086): it overwrites the file handle of whatever node the stale pointer selects (bcopy at :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).
  3. On success with any server-chosen node, identity and type are never validated: n_sillyrename (n_un3.nf_silly) aliases the directory cookie LIST_HEAD n_un3.nd_cook (sys/vfs/nfs/nfsnode.h:125-133). Storing sp on 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) plants sp as n_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):

  1. nfs_sillyrename: initialize np = 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.
  2. nfs_lookitup: the parent-fh-echo branch sets np = dnp so *npp can 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 ls reading 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_testable
↓ fix.diffper-fix-DF-2996

Confirmed 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.