DF-2996 / verdict.json
{ "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)." } |