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

Zero-length SunRPC record mark from an NFS/TCP server returns a NULL reply mbuf β€” nfs_reply() dereferences mtod(NULL), deterministic client kernel panic

Field Value
ID DF-3009
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 / CWE-20
File sys/vfs/nfs/nfs_socket.c
Lines 648-672, 861-862
Area vfs/nfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

The SOCK_STREAM path of nfs_receive() validates the RPC record-mark length only against 'len > NFS_MAXPACKET'. A malicious server replying to any kernel RPC with the 4-byte mark 0x80000000 (LASTFRAG, length 0) yields len==0; sbinit(&sio, 0) makes sorecvtcp() return immediately via its resid==0 fast path with an empty mbuf chain, and the short-receive guard 'sio.sb_cc != len' (0 != 0) never converts this to EPIPE β€” nfs_receive() returns success with *mp == NULL. nfs_reply() then executes info.dpos = mtod(info.mrep == NULL, caddr_t): a kernel-mode load from VA 0x10 (offsetof m_hdr.mh_data) β€” unconditional page fault, client panic, before xid matching. The server-side twin nfsrv_getstream() rejects ns_reclen <= 0 at :2743; the client lacks the equivalent check β€” that asymmetry is the whole bug. Malicious or compromised NFS server (or MITM of an unencrypted NFS/TCP stream) panics any client that has mounted it over TCP; after the mount exists, an unprivileged local user triggers it with a single stat(); also reachable through the nfsiod reader thread for async bio replies. Impact ceiling is a reliable client DoS β€” fixed-address read of unmapped memory with no offset control and no write, no uid=0 route. VERIFIED on the stock INVARIANTS guest: mount handshake honest, then 'Fatal trap 12: page fault, fault virtual address = 0x10, Stopped at nfs_reply+0x622: movq 0x10(%rax),%rax', guest down. Fix validated in-guest (reject len==0 + empty-chain guard): identical PoC produces zero panics β€” new 'impossible packet length (0) from nfs server' rejection followed by the normal reconnect/retry loop.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of nfs_socket.c (GLM 5.3); deterministic client panic reproduced + fix validated. DF-0775 re-verified, not re-reported.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3009 Β· 12 files
FileTypeDescriptionSize
fakesrv.c β€” 10.1 KB view raw
build.sh β€” 171 B view raw
run.sh β€” 644 B view raw
fix.diff β€” 899 B view raw
build.log β€” 213 B view raw
run.log β€” 778 B view raw
run.fixed.log β€” 868 B view raw
fixed.console.txt β€” 182 B view raw
panic.txt β€” 821 B view raw
env.txt β€” 654 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
verdict.json β€” 4.3 KB view raw
VERDICT.md
↓ download raw

DF-3009 β€” VERDICT

REPRODUCED β€” malicious-server client kernel panic (NULL-pointer dereference at a fixed virtual address), verified on the stock guest kernel, then fix validated on a patched kernel build.

Root cause (path:line)

  • sys/vfs/nfs/nfs_socket.c:648 β€” the SOCK_STREAM receive path parses the SunRPC record mark: len = ntohl(len) & ~0x80000000;
  • sys/vfs/nfs/nfs_socket.c:653 β€” the only sanity check is if (len > NFS_MAXPACKET). A record mark of exactly 0x80000000 (LASTFRAG, length 0) is accepted. (The server-side twin, nfsrv_getstream() at sys/vfs/nfs/nfs_socket.c:2743, does reject ns_reclen <= 0 β€” the client lacks the equivalent check.)
  • sys/vfs/nfs/nfs_socket.c:665 β€” sbinit(&sio, len) with len == 0 gives sb_climit == 0; sorecvtcp() (sys/kern/uipc_socket.c:1723) immediately takes the resid == 0 β†’ release path and returns 0 with an empty mbuf chain (sio.sb_mb == NULL).
  • sys/vfs/nfs/nfs_socket.c:672 β€” the short-receive guard if (error == 0 && sio.sb_cc != len) compares 0 != 0 and does not fire; *mp = sio.sb_mb stays NULL and nfs_receive() returns success.
  • sys/vfs/nfs/nfs_socket.c:861-862 β€” nfs_reply(): info.md = info.mrep; (NULL) then info.dpos = mtod(info.md, caddr_t); which is a load of ((struct mbuf *)NULL)->m_data β€” virtual address 0x10 (offsetof(struct m_hdr, mh_data) = 8+8). Unconditional page fault in kernel mode.

Trigger chain

  1. Malicious (or compromised) NFS server, NFS program spoken over TCP.
  2. mount_nfs -o tcp ... handshake answered honestly (rpcbind/mountd + NULL-proc ping), so the mount succeeds.
  3. Any subsequent kernel RPC over the TCP connection (e.g. the GETATTR issued by stat /mnt, or the async reader thread for bio requests) is answered with exactly 4 bytes: 80 00 00 00.
  4. Client kernel panics before xid matching β€” the fault is in the reply pre-parse, so the xid/proc of the pending request is irrelevant.

Baseline run (stock kernel #0 Thu Jul 2 06:02:54 UTC 2026)

sh run.sh:

MOUNT_OK
... ssh session dies with the guest ...

serial console (see panic.txt):

Fatal user address access from kernel mode from stat at ffffffff808021a2
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x10
fault code      = supervisor read data, page not present
instruction pointer = 0x8:0xffffffff808021a2
current process = 1087 (stat)
Stopped at      nfs_reply+0x622:        movq    0x10(%rax),%rax

vm.sh status β†’ down. Faulting instruction is the mtod(NULL) load β€” exactly the predicted site (nfs_socket.c:862).

Exploitability beyond DoS

The primitive is a kernel-mode read of a fixed unmapped address (0x10). There is no offset control and no write; userland cannot map page 0 into the kernel on this platform. Verdict: reliable client DoS/panic only, no escalation path. (Classification: impact=panic, not uid0_privesc.)

Fix validation (kernel #0 Sat Sep 5 05:04:56 UTC 2026)

fix.diff applied to the guest's /usr/src, make nativekernel KERNCONF=X86_64_GENERIC + make installkernel, reboot, exact same PoC:

  • mount still succeeds (MOUNT_OK),
  • stat /mnt does not panic; the console shows the new rejection firing 30Γ— followed by the normal reconnect/retry loop:

    kernel: impossible packet length (0) from nfs server 127.0.0.1:/P kernel: receive error 27 from nfs server 127.0.0.1:/P

  • vm.sh status β†’ up throughout; hard-mount retry (stat blocking while a dead server never answers) is pre-existing designed behavior, not a regression. Guest then reset to the clean snapshot.

fix_status: fixed (baseline reproduced β†’ patched not reproduced, same PoC, same options).

Honesty notes

  • One earlier mount attempt in the same session failed with "Program not registered" β€” that was self-inflicted (my DF-3010 rpcbind occupied UDP 111); not a PoC defect. After cleanup the baseline reproduced on the first clean attempt.
  • The nfs_reply() NULL-mrep case is also reachable through the nfsiod reader thread (sys/vfs/nfs/nfs_iod.c:96 calls the same nfs_reply(nmp, NULL)), so async READ/WRITE replies hit the same line.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

['run.fixed.log', 'fixed.console.txt (30x rejection lines, zero Fatal traps)', 'build via /root/build_fixed.log BUILD_RC=0 (in-guest)']
↓ fix.diffDragonFly 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

Confirmed kernel references

Detail

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 (decisive lines)

['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']

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)

Verified 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

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.