DF-3009 / verdict.json
{ "finding_id": "DF-3009", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Malicious NFS-over-TCP server replies to any kernel RPC with a 4-byte zero-length LASTFRAG record mark (0x80000000). nfs_receive() only checks len > NFS_MAXPACKET (nfs_socket.c:653), so len==0 is accepted; the body receive returns success with an empty mbuf chain because sorecvtcp() takes the resid==0 fast path (uipc_socket.c:1723) and the short-receive guard 0!=0 does not fire (nfs_socket.c:672). nfs_reply() then executes info.dpos = mtod(info.mrep == NULL, caddr_t) at nfs_socket.c:862 - a kernel load from VA 0x10 (offsetof m_hdr.mh_data) - and panics the client. Reproduced on the stock guest: Fatal trap 12, fault VA 0x10, Stopped at nfs_reply+0x622: movq 0x10(%rax),%rax, guest down, triggered by stat on the mounted filesystem. Fixed by rejecting len==0 (mirroring the server-side ns_reclen<=0 check at nfs_socket.c:2743) plus an sb_mb==NULL guard; on the patched kernel the same PoC logs 'impossible packet length (0) from nfs server' and reconnects cleanly with no panic.", "exploit_chain": "malicious TCP NFS server -> honest rpcbind/mountd/NULL-ping handshake -> mount_nfs -o tcp succeeds -> stat /mnt issues first kernel GETATTR -> server replies 80 00 00 00 -> nfs_receive returns success with NULL mbuf -> nfs_reply mtod(NULL) load at VA 0x10 -> page fault -> kernel panic. Primitive is a fixed-address kernel read of unmapped memory: reliable DoS/panic, no offset control, no write, no path to uid=0.", "evidence": [ "panic.txt: fault virtual address = 0x10, Stopped at nfs_reply+0x622: movq 0x10(%rax),%rax (from stat)", "run.log: MOUNT_OK then guest death", "run.fixed.log + fixed.console.txt: 30x 'impossible packet length (0) from nfs server' + 'receive error 27', guest stayed up on patched kernel", "fix.diff: len==0 rejection + sb_mb==NULL guard" ], "kernel_refs": [ "sys/vfs/nfs/nfs_socket.c:648", "sys/vfs/nfs/nfs_socket.c:653", "sys/vfs/nfs/nfs_socket.c:665", "sys/vfs/nfs/nfs_socket.c:672", "sys/vfs/nfs/nfs_socket.c:861", "sys/vfs/nfs/nfs_socket.c:862", "sys/kern/uipc_socket.c:1723", "sys/vfs/nfs/nfs_socket.c:2743" ], "poc_changes": "n/a - pack authored fresh in this run (fakesrv.c speaks rpcbind/UDP + mountd/TCP + NFS/TCP, answers userland NULL-pings honestly so mount_nfs -o tcp succeeds, replies 80 00 00 00 to every kernel RPC); run.sh needed rpcbind-free port 111 (no third-party rpcbind running)", "attempts": 2, "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": 95, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o fakesrv fakesrv.c", "run_cmd": "(./fakesrv &) ; mount_nfs -o tcp,port=2049 127.0.0.1:/P /mnt ; stat /mnt", "code_hash": "d2c8d33f324c481545251dd524adebb05d867db900968e50e59c8e6fac7af987", "notes": "Also reachable via the nfsiod reader thread (nfs_iod.c:96) for async bio replies. Server-side twin hypothesis (zero-length record vs nfsd) was killed: nfsrv_getstream rejects ns_reclen<=0 at nfs_socket.c:2743 - verified on guest ('impossible packet length (0) from nfs client'), pack findings/poc/DF-3010 documents the false positive. Fix build: make nativekernel + installkernel in guest; baseline panic gone on patched kernel; guest reset to clean snapshot afterwards.", "recommended_fix": "In nfs_receive() stream path reject zero-length record marks (len == 0 || len > NFS_MAXPACKET -> EFBIG, mirroring nfsrv_getstream's ns_reclen <= 0 check) and convert an empty body chain to EPIPE", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Sat Sep 5 05:04:56 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src; make nativekernel KERNCONF=X86_64_GENERIC + make installkernel + reboot; identical PoC run: mount OK, stat did not panic, console logged the new 'impossible packet length (0)' rejection followed by normal reconnect/retry (hard-mount semantics); guest remained up.", "fix_evidence": [ "run.fixed.log", "fixed.console.txt (30x rejection lines, zero Fatal traps)", "build via /root/build_fixed.log BUILD_RC=0 (in-guest)" ] } |