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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0775 Β· 13 files
FileTypeDescriptionSize
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
README.md readme evidence pack overview
↓ download 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:

  1. nfsm_rndup(0x7FFFFFFD) wraps to INT_MIN βœ“
  2. nfsm_adv(info, INT_MIN) corrupts info->dpos βœ“ (confirmed by disassembly of nfsm_adv @ 0xffffffff80814520)
  3. nfsm_dissect(info, 4) computes int n = ptrdiff β€” the 64-bit pointer difference (β‰ˆ 2Β³ΒΉ) truncates to a negative 32-bit int βœ“
  4. bytes <= n β†’ 4 <= negative β†’ FALSE β†’ routes to nfsm_disct β†’ returns EBADRPC βœ“ (confirmed by disassembly cmp %esi,%ecx)
  5. The wild pointer is never returned, never dereferenced βœ“
VERDICT.md verdict full analysis with disassembly proof of why claimed impact doesn't manifest
↓ download raw

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():

  1. 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 checks if (len < 0 || len > RPCAUTH_MAXSIZ) β€” the client-side does not.

  2. 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 */ For i = 0x7FFFFFFD: (0x7FFFFFFD + 3) & ~3 = 0x80000000 = INT_MIN.

  3. 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 of nfsm_adv @ 0xffffffff80814520: the cmp %esi,%ecx / jl at 8081453f-80814541 does NOT jump for ecx=0, esi=0x80000000 (because 0 >= INT_MIN is true in signed comparison), so the in-place dpos += sign_extend(INT_MIN) at 80814543-8081454b executes, 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.


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_testable
baseline no→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 09:23:00 UTC 2026 (sha256 kernel=59a826e35b5d861607af9bfdbdbaaddf78bbe968a24574765504e296832572f6)

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.