Signed integer overflow in RPC reply verifier length allows mbuf length corruption
Summary
krpc_subr.c:425 len += fxdr_unsigned(u_int32_t,reply->rp_auth.authlen). authlen attacker-controlled from network RPC reply. len is signed int. authlen=0x7FFFFFE8: 24+0x7FFFFFE8=0x80000000=INT_MIN. :426 (len+3)&~3 stays INT_MIN. :428 m_adj(m,INT_MIN) tail-trim: -INT_MIN UB. m->m_len-=INT_MIN overflows negative. Single-mbuf: EBADRPC boot failure. Chained non-cluster: potential panic in m_pullup. NFS-root boot only attacker is on-path/malicious server. Fix: validate authlen against RPCAUTH_MAXSIZ(400).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0900 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| overflow_demo.c | trigger-source | userspace harness replicating krpc_subr.c:415-428 + m_adj arithmetic to demonstrate the signed overflow for cited authlen values | 6.1 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix: bound reply->rp_auth.authlen against RPCAUTH_MAXSIZ(400) before adding to signed int len; mirrors nfs_socket.c guard | 865 B | view raw |
| build.sh | repro-script | cc -O2 -Wall -o overflow_demo overflow_demo.c | 138 B | view raw |
| run.sh | repro-script | ./overflow_demo | 153 B | view raw |
| build.log | build-log | (in run.log) harness build output, exit 0 | 140 B | view raw |
| run.log | run-log | decisive harness run on unpatched #0: shows BUG FIRES for authlen=0x7FFFFFE8, m_len corrupted 100->2147483748 | 1.7 KB | view raw |
| fix_run.log | run-log | harness run on patched #1 kernel (userspace arithmetic unchanged; validates harness still runs cleanly on fixed kernel) | 1.9 KB | view raw |
| fix_build.log | build-log | tail of make nativekernel + make installkernel output, both rc=0 | 25.1 KB | view raw |
| fix_disasm.txt | fix-evidence | objdump of running patched kernel: cmp $0x190,%eax (RPCAUTH_MAXSIZ) + ja EBADRPC path in krpc_call.part.0 | 534 B | view raw |
| env.txt | environment | uname + cc version | 245 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, impact ceiling, fix validation | 6.9 KB | β raw |
| README.md | readme | human-facing summary + build/run/expected + reachability note | 3.6 KB | β 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-0900 β Signed integer overflow in RPC reply verifier length
The bug (claim)
sys/vfs/nfs/krpc_subr.c:425 in krpc_call()'s gotreply path adds an
attacker-controlled network-supplied authlen (from the RPC reply verifier
header) into a plain signed int len:
415: len = sizeof(*reply); /* = 24 */
424: if (reply->rp_auth.authtype != 0) {
425: len += fxdr_unsigned(u_int32_t, reply->rp_auth.authlen); /* signed int + u32 */
426: len = (len + 3) & ~3; /* stays INT_MIN */
427: }
428: m_adj(m, len); /* m_adj(m, INT_MIN) */
len is declared int at krpc_subr.c:200. With authlen = 0x7FFFFFE8,
24 + 0x7FFFFFE8 = 0x80000000 = INT_MIN β signed integer overflow
(undefined behavior in C); (INT_MIN+3)&~3 stays INT_MIN. m_adj(m, INT_MIN)
takes the len < 0 branch (sys/kern/uipc_mbuf.c:1884), where
len = -len is itself UB and leaves INT_MIN; the subsequent
m->m_len -= len corrupts the mbuf length, and downstream consumers
(m_pullup/copyout) read past the buffer.
Reachability (boot-time only)
krpc_call() is reached only via:
- krpc_portmap() (krpc_subr.c:163) and
- nfs_mountrpc.c:140,146,155,163,202,253,256 (mount-rpc helpers),
which are themselves called only from the diskless NFS-root bootstrap
(bootpc_init() at nfs_vfsops.c:569 β nfs_mountrpc). The
nfs_mountrpc.c file header states verbatim:
"This module is not used by normal operating code because the 'mount' command has a far more sophisticated implementation."
There is no userspace syscall that reaches this code on a normally-booted
kernel. The attacker model is an on-path network attacker or a malicious
NFS server during diskless boot. The normal NFS client/server socket path
in nfs_socket.c already validates the equivalent length field with
if (len < 0 || len > RPCAUTH_MAXSIZ) (nfs_socket.c:2347,2385); the
boot-time krpc_subr.c path omits that guard.
PoC
Because the path is unreachable from userspace on a normally-booted guest,
overflow_demo.c is a userspace harness that reproduces the kernel's exact
arithmetic (krpc_subr.c:415-428) to prove the signed overflow fires for
the cited authlen values. It also reproduces the m_adj single-mbuf
m_len corruption from sys/kern/uipc_mbuf.c:1859-1924.
Build & run (DragonFlyBSD guest, as maxx)
cc -O2 -Wall -o overflow_demo overflow_demo.c ./overflow_demo
Expected output
For authlen = 0x7FFFFFE8 the harness prints:
0x7fffffe8 -2147483648 -2147483648 -2147483648 -2147483548 cited overflow trigger ** BUG FIRES: sizeof_reply(24) + authlen(0x7FFFFFE8) = 0x80000000 = INT_MIN (signed overflow, UB) ** (len+3)&~3 = INT_MIN, passed to m_adj() as req_len. ** m_len corrupted: 100 -> -2147483548 (OOB on next mbuf op)
and concludes that the path performs signed int arithmetic on an
attacker-controlled u_int32_t without any upper bound.
Impact ceiling
Low. Realistic worst case is boot failure / local DoS at diskless-NFS-root
boot time (the finding's summary: "Single-mbuf: EBADRPC boot failure;
chained non-cluster: potential panic in m_pullup"). No memory-corruption
primitive reachable from an unprivileged local user β the path is
network/boot-time only and authlen shapes only a length, not arbitrary
content. No escalation chain applies.
Recommended fix
Bound authlen against RPCAUTH_MAXSIZ (400) β the same guard the normal
NFS socket path already uses β before adding it to len. See fix.diff.
DF-0900 β VERDICT
Verdict: REPRODUCED (signed integer overflow, confirmed by harness + source trace); fix VALIDATED (compiles, boots, bound check confirmed in binary).
The bug (real)
sys/vfs/nfs/krpc_subr.c:425 in krpc_call()'s gotreply path adds an
attacker-controlled network-supplied authlen (from the RPC reply verifier
header reply->rp_auth.authlen, a u_int32_t on the wire) into a plain
signed int len (declared at krpc_subr.c:200) without any upper bound:
415: len = sizeof(*reply); /* = 24 */
424: if (reply->rp_auth.authtype != 0) {
425: len += fxdr_unsigned(u_int32_t, reply->rp_auth.authlen); /* int += u32 */
426: len = (len + 3) & ~3; /* stays INT_MIN */
427: }
428: m_adj(m, len); /* m_adj(m, INT_MIN) */
fxdr_unsigned (sys/vfs/nfs/xdr_subs.h) byte-swaps the big-endian wire
u32 to host order. len is signed int. For authlen = 0x7FFFFFE8:
24 + 0x7FFFFFE8 = 0x80000000 = INT_MINβ signed integer overflow (C UB).(INT_MIN + 3) & ~3 = INT_MIN.m_adj(m, INT_MIN)(sys/kern/uipc_mbuf.c:1859) takes thereq_len < 0branch (m_adj:1884), wherelen = -len(-INT_MIN, itself UB) and the subsequentm->m_len -= lencorruptsm_len(e.g. 100 β 2147483748). Downstreamm_pullup/copyout then read past the buffer.
The harness overflow_demo.c reproduces this exact arithmetic and the
m_adj single-mbuf corruption; for authlen = 0x7FFFFFE8 it prints:
0x7fffffe8 -2147483648 -2147483648 -2147483648 2147483748 cited overflow trigger ** BUG FIRES: sizeof_reply(24) + authlen(0x7FFFFFE8) = 0x80000000 = INT_MIN (signed overflow, UB) ** (len+3)&~3 = INT_MIN, passed to m_adj() as req_len. ** m_len corrupted: 100 -> 2147483748 (OOB on next mbuf op)
Reachability (boot-time only β why no live trigger on this guest)
krpc_call() is reached only via:
- krpc_portmap() (krpc_subr.c:163) and
- the mount-rpc helpers in nfs_mountrpc.c:140,146,155,163,202,253,256,
which are themselves called only from the diskless NFS-root bootstrap
path (bootpc_init() at nfs_vfsops.c:569 β nfs_mountrpc). The
nfs_mountrpc.c file header states verbatim:
"Procedures used by NFS_ROOT and BOOTP to do an NFS mount rpc to obtain the nfs root file handle for a NFS-based root mount point. This module is not used by normal operating code because the 'mount' command has a far more sophisticated implementation."
There is no userspace syscall that reaches this code on a normally-booted
kernel. The attacker model is an on-path network attacker or a malicious
NFS server during diskless boot. The normal NFS client/server socket path
in nfs_socket.c already validates the equivalent length field with
if (len < 0 || len > RPCAUTH_MAXSIZ) (nfs_socket.c:2347,2385); the
boot-time krpc_subr.c path omits that guard β which is exactly the gap the
fix closes.
We therefore reproduce the arithmetic bug via the harness (which
replicates krpc_subr.c:415-428 and m_adj exactly) rather than a live
runtime trigger, which would require staging a diskless NFS boot against a
malicious server β not feasible inside this audit guest.
Impact ceiling
Low (matches the finding's severity). The realistic worst case is
boot failure / local DoS at diskless-NFS-root boot time (the finding's
summary: "Single-mbuf: EBADRPC boot failure; chained non-cluster: potential
panic in m_pullup"). No memory-corruption primitive is reachable from an
unprivileged local user β the path is network/boot-time only and authlen
shapes only a length, not arbitrary content. No escalation chain applies.
Exploit chain
none β this is not a memory-corruption primitive reachable from userspace. It is a boot-time-only signed integer overflow whose ceiling is DoS (boot failure / panic). No uid0 chain is applicable.
Fix (authored in fix.diff, VALIDATED)
Bound authlen against RPCAUTH_MAXSIZ (400, sys/vfs/nfs/rpcv2.h:55) β
the same guard the normal NFS socket path already uses (nfs_socket.c:2347,2385) β
before adding it to len. On overflow-class values the function now returns
EBADRPC instead of performing the corrupting arithmetic:
reply = mtod(m, struct rpc_reply *);
if (reply->rp_auth.authtype != 0) {
u_int32_t authlen = fxdr_unsigned(u_int32_t, reply->rp_auth.authlen);
if (authlen > RPCAUTH_MAXSIZ) { /* NEW */
error = EBADRPC;
goto out;
}
len += authlen;
len = (len + 3) & ~3;
}
m_adj(m, len);
This matches the finding proposal (the summary says: "Fix: validate authlen against RPCAUTH_MAXSIZ(400)").
Phase 8 β fix validation (BUILT + BOOTED + binary-confirmed)
- Baseline (
#0, unpatched): harness fires; source atkrpc_subr.c:425has no bound β overflow path is live. - Applied
fix.diffto in-guest/usr/src(patch -p1β "Hunk #1 succeeded at 422"). - Built
make -j6 nativekernel KERNCONF=X86_64_GENERICβrc=0. Installedmake installkernelβrc=0. (NOTE: DragonFly's loader on this guest requires the full kernel βmake installkernelwrites the 119 MB unstripped ELF to/boot/kernel/kernel; a manual copy ofkernel.strippedwas rejected by the loader with "Unable to load /kernel/kernel".) - Booted patched kernel:
kern.version = DragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 12:01:25 UTC 2026. - Binary-confirmed the bound is compiled in (objdump of the running
kernel's
krpc_call.part.0):
ffffffff808171bf: mov 0x10(%rax),%eax ; load reply->rp_auth.authlen
ffffffff808171c2: bswap %eax ; fxdr_unsigned
ffffffff808171c4: cmp $0x190,%eax ; cmp authlen, RPCAUTH_MAXSIZ(400)
ffffffff808171c9: ja β¦krpc_call.part.0+0x42d ; if above -> EBADRPC goto out
ffffffff808171cf: add $0x1b,%eax ; len = authlen + sizeof(*reply) + 3
With authlen <= 400, the subsequent add $0x1b produces at most 427 β
well within int range, so the overflow is dead. authlen > 400 now
takes the ja to EBADRPC.
fix_status = fixed. The runtime behavioral re-test (live NFS-mount with
a malicious reply) is not_testable on this guest (boot-time-only path,
no diskless-NFS boot can be staged), but the fix is validated to apply
cleanly, compile, boot, and close the vulnerable code path by both source
inspection and binary disassembly of the running kernel.
PoC changes
The findings/poc/DF-0900/ directory did not exist when this runner
spawned (only the DB row + summary were present). I created:
- overflow_demo.c β userspace harness replicating krpc_subr.c:415-428 +
m_adj arithmetic to demonstrate the signed overflow for the cited
authlen values.
- README.md, build.sh, run.sh, fix.diff, env.txt, full logs,
this VERDICT.md, and manifest.json.
Fix verification
fixedVALIDATED: harness confirms INT_MIN overflow; patched objdump shows cmp $0x190 bound check. Compile+boot clean.
BEFORE: len=INT_MIN, m_len corrupted. AFTER: cmp $0x190 ja EBADRPC. No overflow.
Confirmed kernel references
- sys/vfs/nfs/krpc_subr.c:200
- sys/vfs/nfs/krpc_subr.c:415
- sys/vfs/nfs/krpc_subr.c:425
- sys/vfs/nfs/krpc_subr.c:426
- sys/vfs/nfs/krpc_subr.c:428
- sys/kern/uipc_mbuf.c:1859
- sys/kern/uipc_mbuf.c:1884
- sys/vfs/nfs/rpcv2.h:55
- sys/vfs/nfs/nfs_socket.c:2347
- sys/vfs/nfs/nfs_socket.c:2385
- sys/vfs/nfs/nfs_mountrpc.c:140
- sys/vfs/nfs/nfs_vfsops.c:569
Detail
Exploit chain
none -- boot-time-only signed overflow, DoS ceiling. No userspace trigger.
Evidence (decisive lines)
Harness: authlen=0x7FFFFFE8 -> len=INT_MIN, m_len 100->2147483748. Fix disasm: cmp $0x190; ja EBADRPC.
PoC changes
Authored from scratch: overflow_demo.c (replicates krpc arithmetic), fix.diff (bound authlen vs RPCAUTH_MAXSIZ), VERDICT.md, manifest.json.
Verified recommended fix
Extract authlen and reject authlen>RPCAUTH_MAXSIZ(400) with EBADRPC before adding to len at krpc_subr.c:425. Mirrors nfs_socket.c:2347 guard. Matches finding proposal. Full diff in findings/poc/DF-0900/fix.diff.
Verdict
REPRODUCED (harness). krpc_subr.c:425 len += authlen (signed int, no bound). authlen=0x7FFFFFE8 -> INT_MIN -> m_adj(INT_MIN) -> m_len corrupted 100->2147483748. Boot-time-only path (diskless NFS root). Normal NFS socket path already guarded at nfs_socket.c:2347.
No comments yet.