RPC reply verifier-length integer overflow corrupts XDR cursor β wild pointer deref client kernel panic or OOB read
Summary
nfs_socket.c:1498 i=fxdr_unsigned(int32_t,*tl) verifier body length from untrusted server reply full int32 range no bound. :1504-1505 else if(i>0) ERROROUT(nfsm_adv(info,nfsm_rndup(i))). nfsm_rndup(a)=((a)+3)&~3 signed overflow for i in {0x7FFFFFFD,0xE,0xF} produces INT_MIN(0x80000000). nfsm_adv (nfsm_subs.c:911) n>=len TRUE for negative len executes info->dpos += INT_MIN advances ~2GiB backwards = wild pointer. :1507 nfsm_dissect(info,4) with corrupted dpos returns wild pointer. :1509 *tl==0 dereferences wild address = kernel page fault panic or OOB kernel memory read if mapped. No bound on verifier length anywhere between wire and nfsm_adv contrast server-side nfs_getreq caps at RPCAUTH_MAXSIZ. Trigger: malicious/MITM NFSv3 server crafts reply verifier_len=0x7FFFFFFF any NFS RPC. Client kernel panics on first RPC. Same pattern as DF-0768. Fix: if(i>RPCAUTH_MAXSIZ) error=EBADRPC + nfsm_adv if(len<0) return EBADRPC.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0775 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| malicious_server.c | trigger-source | malicious NFS/RPC server: portmap + NFS + MOUNT, replies with verifier_len=0x7FFFFFFD on kernel RPCs | 10.4 KB | view raw |
| trigger.sh | trigger-source | mount trigger script | 1.4 KB | view raw |
| build.sh | build-script | cc -O2 -o malicious_server malicious_server.c | 159 B | view raw |
| run.sh | run-script | start server + mount_nfs + ls | 1.0 KB | view raw |
| VERDICT.md | verdict | full analysis with disassembly proof of why claimed impact doesn't manifest | 6.9 KB | β raw |
| README.md | readme | evidence pack overview | 2.6 KB | β raw |
| run.log | run-log | unpatched #0 kernel run β EBADRPC, no panic | 2.6 KB | view raw |
| fix_run.log | run-log | patched #1 kernel run β identical EBADRPC behavior | 1.3 KB | view raw |
| fix_build.log | build-log | single-fix kernel build (35401 lines, rc=0) | 5.6 MB | β download |
| fix.diff | suggested-fix | bounds check: if (i < 0 || i > RPCAUTH_MAXSIZ) EBADRPC | 658 B | view raw |
| env.txt | environment | uname, cc version, vfs.usermount | 190 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0775 β PoC Evidence Pack
Finding: RPC reply verifier-length integer overflow corrupts XDR cursor β wild pointer deref client kernel panic or OOB read
Verdict: NOT REPRODUCED (false positive for claimed impact)
TL;DR
The signed overflow in nfsm_rndup(i) IS real, and info->dpos IS
corrupted (dpos += INT_MIN, jumping ~2 GiB backwards). However, the
subsequent nfsm_dissect() bounds check truncates its pointer-difference
to int, which makes the comparison fail and routes to nfsm_disct(),
returning EBADRPC. The claimed wild-pointer dereference (*tl == 0
at line 1509) is never reached. No panic, no OOB read β only
EBADRPC, which is the same error any malformed RPC reply produces.
Files
| File | Description |
|---|---|
malicious_server.c |
Malicious NFS/RPC server (portmap + NFS + MOUNT) |
trigger.sh |
Mount trigger script |
build.sh |
Build the server |
run.sh |
Run the full test (server + mount + ls) |
VERDICT.md |
Full analysis with disassembly proof |
fix.diff |
Defense-in-depth fix (bounds check on verifier length) |
run.log |
Unpatched-kernel run output |
fix_run.log |
Patched-kernel run output |
fix_build.log |
Single-fix kernel build log |
env.txt |
Guest environment |
How to reproduce
./build.sh # build the malicious server
# as root inside the guest:
./run.sh # start server, mount, test
Expected: ls: /mnt_df0775: RPC struct is bad (EBADRPC). Guest stays
up. No panic.
Why the claimed impact doesn't manifest
See VERDICT.md for the full disassembly proof. Short version:
nfsm_rndup(0x7FFFFFFD)wraps toINT_MINβnfsm_adv(info, INT_MIN)corruptsinfo->dposβ (confirmed by disassembly ofnfsm_adv@0xffffffff80814520)nfsm_dissect(info, 4)computesint n = ptrdiffβ the 64-bit pointer difference (β 2Β³ΒΉ) truncates to a negative 32-bit int βbytes <= nβ4 <= negativeβ FALSE β routes tonfsm_disctβ returnsEBADRPCβ (confirmed by disassemblycmp %esi,%ecx)- The wild pointer is never returned, never dereferenced β
DF-0775 β VERDICT
Verdict: NOT REPRODUCED (false positive for claimed impact)
Finding title: RPC reply verifier-length integer overflow corrupts XDR cursor β wild pointer deref client kernel panic or OOB read
Claimed impact: kernel panic (wild pointer dereference) or OOB kernel memory read when a malicious NFS server replies with verifier_len β {0x7FFFFFFD, 0x7FFFFFFE, 0x7FFFFFFF}.
Actual observed impact: EBADRPC β the NFS operation fails
gracefully. No panic, no OOB read, no kernel memory disclosure.
The guest remains fully stable after 21+ malicious replies.
What IS real
The finding correctly identifies three things in
sys/vfs/nfs/nfs_socket.c:nfs_request_processreply():
-
No bounds check on verifier length (line 1498):
c i = fxdr_unsigned(int32_t, *tl); /* full int32 range, no bound */The server-side path at line 2347 checksif (len < 0 || len > RPCAUTH_MAXSIZ)β the client-side does not. -
Signed overflow in nfsm_rndup (line 1505):
c ERROROUT(nfsm_adv(info, nfsm_rndup(i))); /* nfsm_rndup(a) = (((a)+3) & (~0x3)) β overflows for a near INT_MAX */Fori = 0x7FFFFFFD:(0x7FFFFFFD + 3) & ~3 = 0x80000000 = INT_MIN. -
dpos corruption in nfsm_adv (nfsm_subs.c:916-918):
c n = mtod(info->md, caddr_t) + info->md->m_len - info->dpos; if (n >= len) { /* 0 >= INT_MIN β TRUE (signed) */ info->dpos += len; /* dpos += INT_MIN β wild pointer, ~2 GiB back */Confirmed by disassembly ofnfsm_adv@0xffffffff80814520: thecmp %esi,%ecx/jlat8081453f-80814541does NOT jump forecx=0, esi=0x80000000(because0 >= INT_MINis true in signed comparison), so the in-placedpos += sign_extend(INT_MIN)at80814543-8081454bexecutes, corrupting dpos.
What does NOT happen (the claimed impact)
The finding claims that after dpos is corrupted, nfsm_dissect(info, 4)
at line 1507 returns the wild pointer, which is then dereferenced at
line 1509 (if (*tl == 0)).
This is false. nfsm_dissect() never returns the wild pointer.
The reason is a int n truncation in the bounds check:
void *nfsm_dissect(nfsm_info_t info, int bytes) {
int n; /* β 32-bit! */
n = mtod(info->md, caddr_t) + info->md->m_len - info->dpos;
/* β 64-bit pointer arithmetic, result is ptrdiff_t (64-bit) */
if (bytes <= n) { /* n is truncated to int before compare */
ptr = info->dpos; /* β wild pointer β ONLY reached if n >= 4 */
...
} else {
error = nfsm_disct(...); /* β returns EBADRPC for our case */
}
}
After the dpos corruption, the pointer difference
(m_data + m_len - dpos_corrupted) is a huge 64-bit value
(β 2Β³ΒΉ + original_remaining). Truncated to int, this is negative
(0x80000000 + small β still bit-31 set β negative as signed int).
The signed comparison 4 <= (negative) is FALSE, so the code routes
to nfsm_disct(), which also detects the bad state and returns
EBADRPC. The wild pointer is never returned, never dereferenced.
Disassembly proof β nfsm_dissect @ 0xffffffff80815170:
80815191: sub %rax,%rcx ; rcx = (m_data+m_len) - dpos [64-bit]
80815194: cmp %esi,%ecx ; 32-BIT: compares ecx (lower 32) with bytes
80815196: jl 808151b0 ; ecx(0x80000000) < esi(4) signed β JUMP TAKEN
; β falls into nfsm_disct β EBADRPC
The cmp %esi,%ecx instruction explicitly compares the lower 32 bits
(%ecx) of the 64-bit pointer difference against bytes (%esi). The
compiler did NOT optimize away the int n intermediate β it honors the C
type and truncates. For any practical mbuf size (β€ 4 KiB), the truncated
value is always negative, so the wild-pointer path is unreachable.
Mathematical proof the wild pointer is unreachable
Let R = original remaining bytes in the current mbuf (0 β€ R β€ ~4096).
After corruption: ptrdiff = R + 2Β³ΒΉ. Truncated to int:
n = (int)(R + 0x80000000). For 0 β€ R < 2Β³ΒΉ (always true for mbufs):
n = R + 0x80000000 which has bit 31 set β n < 0. Since bytes = 4 > 0,
bytes <= n is 4 <= negative β always FALSE. The wild pointer is
returned only if n >= 4, which requires R >= 2Β³ΒΉ β impossible for an
mbuf.
Reproduction evidence
A malicious NFS/RPC server (malicious_server.c) was built and run on
the guest. It implements:
- portmap v2 (UDP/TCP 111) β returns port 2049 for all GETPORT
- rpcbind v3/v4 β PROG_MISMATCH (forces fallback to portmap v2)
- NFS NULL proc β valid reply (satisfies mount_nfs userspace probe)
- NFSv2 MOUNT proc β valid reply with a 32-byte dummy file handle
- All other NFS procs (kernel RPCs) β reply with verifier_len=0x7FFFFFFD
mount_nfs -2 -o tcp,port=2049,ro 127.0.0.1:/x /mnt succeeded. The
kernel NFS client then sent 21+ GETATTR RPCs, each receiving the
malicious verifier. Every operation returned EBADRPC:
$ ls /mnt_df0775 ls: /mnt_df0775: RPC struct is bad
The guest never panicked. No trap, no page fault, no kernel message. The kernel was completely stable throughout.
Server log excerpt:
[ok] prog=100003 vers=2 proc=0 xid=0x... (NULL β mount_nfs userspace) [ok] prog=100000 vers=2 proc=3 xid=0x... (portmap GETPORT β 2049) [ok] prog=100005 vers=1 proc=1 xid=0x... (MOUNT β dummy FH) [EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DD -> verifier_len=0x7FFFFFFD [EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DE -> verifier_len=0x7FFFFFFD ... (21 total kernel RPCs with malicious verifier)
Exploit chain
Not applicable β this is not a memory-corruption primitive. The dpos
corruption is real but the corrupted pointer is never dereferenced due
to the int n truncation defense in nfsm_dissect(). There is no
write primitive, no read primitive, no control-flow hijack. The only
observable effect is EBADRPC, which is the same error any malformed
RPC reply would produce. This is not exploitable for privilege
escalation.
Recommended fix (defense-in-depth)
Although the claimed impact does not manifest, the underlying code has a
legitimate hardening gap: the client-side verifier length is unbounded
while the server-side caps at RPCAUTH_MAXSIZ (400). A future compiler
change, code refactor, or different architecture could remove the int
truncation defense and expose the dpos corruption. The fix matches the
existing server-side check:
i = fxdr_unsigned(int32_t, *tl);
if (i < 0 || i > RPCAUTH_MAXSIZ) { /* β add this */
error = EBADRPC;
goto nfsmout;
}
See fix.diff. This supersedes the finding markdown's proposal
(which suggested the same bound check plus a redundant nfsm_adv length
check β the latter is unnecessary since the bound check eliminates the
overflow entirely).
Fix verification
not_testablenot_applicable -- the claimed impact (panic/OOB read) is a false positive that does not manifest on either the unpatched #0 or the single-fix #1 kernel. The defense-in-depth fix.diff was nonetheless authored, built (rc=0), installed, and tested: behavior is identical (EBADRPC, no panic) on both kernels. The fix closes the verifier-length hardening gap (now bounded at RPCAUTH_MAXSIZ=400, matching the server-side) but does not change observable behavior since the int-truncation defense in nfsm_dissect already prevented the claimed wild-pointer dereference.
Baseline #0 (unpatched): 21x malicious verifier -> EBADRPC, no panic, guest up. Patched #1 (with fix): 3x malicious verifier -> EBADRPC, no panic, guest up. Identical behavior -- the claimed impact never manifests on either kernel.
Confirmed kernel references
Detail
Exploit chain
none -- not a memory-corruption primitive. The dpos corruption is real but the corrupted pointer is never dereferenced due to the int-truncation defense in nfsm_dissect(). There is no write primitive, no read primitive, no control-flow hijack. The only observable effect is EBADRPC, identical to any malformed RPC reply. Not exploitable for privilege escalation.
Evidence (decisive lines)
Baseline #0 kernel, malicious verifier delivered 21+ times: [EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DD -> verifier_len=0x7FFFFFFD [EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DE -> verifier_len=0x7FFFFFFD ... $ ls /mnt_df0775 -> ls: /mnt_df0775: RPC struct is bad (EBADRPC) Guest STAYED UP. No panic, no page fault, no dmesg. nfsm_dissect disassembly proof: 80815191: sub %rax,%rcx ; rcx = 64-bit ptrdiff = R + 2^31 80815194: cmp %esi,%ecx ; 32-BIT truncation! ecx = 0x80000000 80815196: jl <nfsm_disct> ; INT_MIN < 4 signed -> JUMP -> EBADRPC Wild pointer never returned, never dereferenced.
PoC changes
Wrote malicious_server.c (malicious NFS/RPC server implementing portmap v2 on UDP+TCP/111 and NFS+MOUNT on TCP/2049; returns verifier_len=0x7FFFFFFD for kernel RPCs, valid replies for userspace mount_nfs probes). Wrote trigger.sh, build.sh, run.sh, VERDICT.md with disassembly analysis, fix.diff (defense-in-depth bounds check), manifest.json. The finding markdown's PoC directory did not exist; created from scratch.
Verified recommended fix
Add bounds check after reading verifier length at sys/vfs/nfs/nfs_socket.c:1498: if (i < 0 || i > RPCAUTH_MAXSIZ) { error = EBADRPC; goto nfsmout; } -- matching the existing server-side check at line 2347. This eliminates the signed overflow path entirely. Full git-apply-able diff in findings/poc/DF-0775/fix.diff. Supersedes the finding proposal (which also suggested a redundant nfsm_adv length check -- unnecessary since the bounds check prevents the overflow). Note: this is defense-in-depth, not a fix for a reproducing vulnerability.
Verdict
NOT REPRODUCED -- false positive for claimed impact (panic / OOB read). The signed overflow in nfsm_rndup(i) IS real (kernel built with -fno-strict-overflow, confirmed wrapping to INT_MIN) and info->dpos IS corrupted (nfsm_adv takes the in-place dpos += INT_MIN path, confirmed by disassembly). BUT the wild pointer is NEVER dereferenced: the subsequent nfsm_dissect() computes its bounds as int n = (m_data+m_len-dpos) which TRUNCATES the huge 64-bit pointer difference (~2^31) to a negative 32-bit int. The signed comparison bytes(4) <= n(INT_MIN) is FALSE, routing to nfsm_disct() which returns EBADRPC/NULL. The claimed *tl == 0 dereference at line 1509 is never reached. Confirmed by disassembling nfsm_dissect @ 0xffffffff80815170: the cmp %esi,%ecx at 0x80815194 explicitly compares the lower 32 bits (ecx) -- the compiler honors the C int n type and truncates. For any practical mbuf size (R <= 4096), n = (int)(R + 2^31) always has bit 31 set (negative), so 4 <= n is always FALSE. A malicious NFS server (malicious_server.c implementing portmap+NFS+MOUNT) was built and run; mount_nfs -2 succeeded; the kernel NFS client sent 21+ GETATTR RPCs each receiving verifier_len=0x7FFFFFFD. Every operation returned EBADRPC ('RPC struct is bad'); the guest never panicked, no OOB read, no kernel messages. The server-side (line 2347) already caps verifier length at RPCAUTH_MAXSIZ(400) -- the client-side at line 1498 does not, which is a legitimate hardening gap but does NOT produce the claimed impact.
No comments yet.