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)
PoC verification
Evidence pack
findings/poc/DF-3009 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 isif (len > NFS_MAXPACKET). A record mark of exactly0x80000000(LASTFRAG, length 0) is accepted. (The server-side twin,nfsrv_getstream()atsys/vfs/nfs/nfs_socket.c:2743, does rejectns_reclen <= 0β the client lacks the equivalent check.)sys/vfs/nfs/nfs_socket.c:665βsbinit(&sio, len)withlen == 0givessb_climit == 0;sorecvtcp()(sys/kern/uipc_socket.c:1723) immediately takes theresid == 0 β releasepath and returns 0 with an empty mbuf chain (sio.sb_mb == NULL).sys/vfs/nfs/nfs_socket.c:672β the short-receive guardif (error == 0 && sio.sb_cc != len)compares0 != 0and does not fire;*mp = sio.sb_mbstays NULL andnfs_receive()returns success.sys/vfs/nfs/nfs_socket.c:861-862βnfs_reply():info.md = info.mrep;(NULL) theninfo.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
- Malicious (or compromised) NFS server, NFS program spoken over TCP.
mount_nfs -o tcp ...handshake answered honestly (rpcbind/mountd + NULL-proc ping), so the mount succeeds.- 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. - 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 /mntdoes 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:96calls the samenfs_reply(nmp, NULL)), so async READ/WRITE replies hit the same line.
Fix verification
fixedfix.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)']
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.
No comments yet.