{
  "finding_id": "DF-2996",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "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.",
  "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+4*pos 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": [
    "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)"
  ],
  "kernel_refs": [
    "sys/vfs/nfs/nfs_vnops.c:2989-3046",
    "sys/vfs/nfs/nfs_vnops.c:3036-3038",
    "sys/vfs/nfs/nfs_vnops.c:3075-3098",
    "sys/vfs/nfs/nfs_vnops.c:3117-3128",
    "sys/vfs/nfs/nfsnode.h:125-133",
    "sys/vfs/nfs/nfs_node.c:466-473",
    "sys/vfs/nfs/nfs_subs.c:718-731"
  ],
  "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).",
  "attempts": 12,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 9600,
  "guest_dirty": 0,
  "build_cmd": "cd /root/df2996 && cc -O -o fakesrv fakesrv.c && cc -O -o t2 t2.c && cc -O -o trigger trigger.c",
  "run_cmd": "cd /root/df2996 && (./fakesrv dirfh > /root/fs.log 2>&1 &) && sleep 1 && mount_nfs -o port=2049,udp,retrycnt=1 127.0.0.1:/P /mnt && mkdir /mnt/d && ./t2 unlink   # or ./trigger",
  "code_hash": "c751e0743ee2d372bc52fd61a6fac274fdb5fd8c0bbbffa794cbb58a01fecbeb",
  "notes": "Fix validation: candidate fix v1 (error check + fh identity) leaves the typelie vector (client-side retype via attrs) exploitable; v2 adds the VREG-type check which eliminates the sp store in every tested mode, but v2's rejection path itself panics with 'lockmgr: LK_RELEASE: no lock held' in dirfh mode (vop_compat_nremove vput) - a lock-accounting flaw in the candidate fix's release of the lookitup-acquired vnode, documented in ddb.trace.txt for maintainers. Recommended maintainer direction: validate identity+type BEFORE applying the reply's attributes (or refuse retypes of non-VNON vnodes in nfs_loadattrcache, which also fixes the DF-2998 family), and release the acquired vnode exactly per nfs_lookitup's (a)/(b) contract.",
  "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)."
}
