krpc_call leaks the reply mbuf chain on the RPC_PROGMISMATCH exit β reachable by spoofed replies, by DF-3203's stale read, and by legitimate v2-only servers during mountd version fallback
Summary
At the accept-status check, error==RPC_PROGMISMATCH does 'goto out' with m still owning the soreceive'd reply chain; the out: path (:437-441) frees only mhead and from and closes the socket - m is never freed. Every other exit either NULLs m or transfers ownership. Triggerable by a spoofed PROGMISMATCH reply, incidentally by DF-3203's uninitialized rp_status read yielding 2, and by real-world v2-only mountd servers (md_mount tries MOUNT v3 first and falls back to v1 on EBADRPC - the normal version-negotiation path leaks one mbuf chain per boot RPC). Bounded impact (handful of mbufs per boot), no UAF. Boot-only path - not_testable on standard guest; control flow unambiguous from source. Fix: m_freem(m) before the goto out. [VERIFIED pass2: reproduced with exact accounting - 50 PROGMISMATCH replies leak exactly 50 persistent mbufs vs flat honest control, DF-3203 stale-value coupling 3 of 3 with plus 3 mbufs, real-world v2-only mountd fallback trigger confirmed; free-before-exit fix-validated delta 0]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3204 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| dfrig13_krpc.c | β | 10.8 KB | view raw | |
| attacker.c | β | 7.6 KB | view raw | |
| build.sh | β | 167 B | view raw | |
| run.sh | β | 331 B | view raw | |
| run.log | β | 990 B | view raw | |
| pmis.log | β | 5.5 KB | view raw | |
| good.log | β | 5.5 KB | view raw | |
| run.couple.log | β | 1.7 KB | view raw | |
| run.fixed.log | β | 3.1 KB | view raw | |
| fx_pmis.log | β | 5.5 KB | view raw | |
| fx_good.log | β | 5.5 KB | view raw | |
| build_summary.log | β | 1.1 KB | view raw | |
| fix.diff | β | 316 B | view raw | |
| fix_all.diff | β | 2.3 KB | view raw | |
| env.txt | β | 4.3 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw | |
| run_fixed.sh | β | 2.8 KB | view raw |
DF-3204 β krpc_call leaks the reply mbuf chain on the RPC_PROGMISMATCH exit
File: sys/vfs/nfs/krpc_subr.c:391-396 (leaking exit), :437-441
(the out: path that never frees m)
Severity: Low Β· CWE-401 Β· verified in VERIFY mode with the
verbatim-interposition kernel-compiled rig (dfrig13_krpc.c, sha256-gated
#include of the production krpc_subr.c; real UDP stack, real mbufs,
userland attacker attacker.c mode pmis).
What was reproduced (baseline kernel #1)
The attacker answers each request with an in-bounds 24-byte accepted reply
whose rp_status = 2 (RPC_PROGMISMATCH, bytes 20..23 on the wire β no OOB
involved). The verbatim code at :393-395 executes
error = EBADRPC; goto out; with m still owning the soreceive-d reply
chain, and the out: path frees only mhead and from and closes the
socket β m is never freed.
mbufs before: 7/146632 mbufs in use mbufs after: 57/146632 mbufs in use # 50 calls -> exactly +50 leaked
(run.log T3: 50/50 calls returned EBADRPC.)
Control (T33, honest replies transferred to the caller and freed):
mbufs before control: 57 -> after: 57 # +0
DF-3203 coupling (the same leak taken via the uninitialized read):
planting 0x00000002 at mbuf offset 20..23 with the userland primer and
sending a 16-byte short reply β 3/3 calls returned EBADRPC with exactly
+3 leaked mbufs (run.couple.log, detailed in the DF-3203 pack) β the leak
exit taken on a stale value, and incidentally the same path a v2-only
mountd drives during the normal md_mount v3βv1 fallback (nfs_mountrpc.c
:140-163: v3 EBADRPC β v1 retry, one leaked chain per boot RPC).
Fix validation (kernel #2)
With fix.diff (m_freem(m); m = NULL; before the goto out): 50/50
calls still return EBADRPC (PROGMISMATCH semantics unchanged) and
mbufs before: 8 -> after: 8 # +0 β leak gone
honest control stays flat (run.fixed.log).
Reproduce (on the DragonFly guest)
sh build.sh # gate + build + install; reboot sh run.sh # T3 leak (+50) + T33 control (+0) # fix validation: sh build.sh af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886 ; reboot sh run_fixed.sh # (in DF-3205 pack) T3 +0
Files in this folder
| file | purpose |
|---|---|
dfrig13_krpc.c / attacker.c |
harness + userland attacker (mode pmis) |
build.sh / run.sh |
exact commands |
run.log / pmis.log / good.log |
baseline decisive run (netstat brackets, attacker view) |
run.couple.log |
DF-3203-stale-value coupling (+3) |
run.fixed.log / fx_pmis.log / fx_good.log |
fixed-kernel A/B |
build_summary.log |
build gate excerpts (full logs in DF-3205 pack) |
fix.diff / fix_all.diff |
validated fix (single / combined) |
env.txt, VERDICT.md, manifest.json, verdict.json |
context + verdicts |
DF-3204 β VERDICT (verified 2026-10-20, VERIFY mode)
REPRODUCED (resource leak: one reply mbuf chain per RPC_PROGMISMATCH exit; bounded, no UAF) β demonstrated through the verbatim production code on the guest (kernel #1; sha256-gated include of krpc_subr.c; the rig calls the real krpc_call over the real UDP stack; the attacker is the userland mode-pmis server).
Mechanism, line-accurate
:391-395 β when the (in-bounds) reply status equals RPC_PROGMISMATCH
the code does error = EBADRPC; goto out; while m still owns the
chain soreceive stored into sio.sb_mb at :367. The out: path
(:437-441) frees mhead (:438) and from (:439) and closes the socket
(:440) β m is never freed. Every neighboring exit either NULLs m
(loop top :352-355), transfers it (gotreply :431) or cannot have one.
Baseline numbers (netstat -m first line, "N/M mbufs in use") - T3, 50 in-bounds PROGMISMATCH replies: 50/50 calls returned EBADRPC; mbufs 7 -> 57, exactly +50, still allocated afterwards (re-checked after a 2 s settle). One leaked mbuf per call, linear, persistent. - T33 control, 50 honest replies (caller frees the returned chain): 57 -> 57, +0. The differential isolates the PROGMISMATCH exit. - Coupling with DF-3203 (stale value, not wire status): userland primer planting 0x00000002 at mbuf offset 20..23 followed by a 16-byte short reply -> 3/3 calls EBADRPC, mbufs 7 -> 10 (+3); the leak exit taken purely on the uninitialized rp_status read. - Real-world trigger unchanged from the finding: md_mount tries MOUNT v3 first and falls back to v1 on EBADRPC (nfs_mountrpc.c:140-163), so a v2-only mountd leaks one chain per boot RPC; a spoofed PROGMISMATCH (DF-3205 delivery) leaks one per reply.
Impact / privesc assessment Low as filed: bounded memory leak (handful of mbufs per boot; even a persistent spoofer is rate-limited by the boot RPC pattern), no UAF, no double free β the chain is simply orphaned while still marked allocated. Not a local unpriv->root vector; krpc is boot-path only (no runtime callers on a disk-root guest). Recorded impact is the availability effect (kernel mbuf pool consumption), which is honest but small; the fix is one line.
Fix validation (kernel #2 = baseline + fix_all.diff) fix.diff adds m_freem(m) + m = NULL before the goto out. A/B: 50/50 calls still EBADRPC (semantics preserved); mbufs 8 -> 8 (+0); honest control flat; no regression in the other suites (DF-3205 T5 6/6, DF-3203 short replies gated).
Artifacts: run.log (T3 +50 / T33 +0 netstat brackets), pmis.log, good.log, run.couple.log (stale-value coupling), run.fixed.log (fixed +0), fx_pmis.log, fx_good.log, build_summary.log.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
boot-network attacker (DF-3205 delivery) replies PROGMISMATCH, or a v2-only mountd forces the md_mount v3->v1 fallback, or the DF-3203 stale rp_status read happens to hold 2; each occurrence orphans one soreceive-d reply mbuf chain (kernel mbuf pool consumption; bounded availability impact)
Evidence (decisive lines)
run.log: T3 marker 50/50 EBADRPC with mbufs before 7/146632 and after 57/146632, control T33 57 -> 57; run.couple.log: 3/3 EBADRPC with mbufs 7 -> 10 from primer-planted stale status 2; run.fixed.log: T3 50/50 EBADRPC with mbufs 8 -> 8 on the fixed kernel; pmis.log/good.log/fx_pmis.log/fx_good.log attacker-side views
PoC changes
seed sketch assumed an untestable boot lab; replaced with the established verbatim-interposition kernel-compiled rig driving the real UDP stack with a userland pmis-mode attacker; leak accounting via netstat -m first-line deltas bracketed around each sysctl-driven batch, with an honest-reply control batch as the differential
Verified recommended fix
m_freem(m) and NULL it before the EBADRPC goto out at krpc_subr.c:393-395 (validated in fix.diff)
Verdict
The RPC_PROGMISMATCH exit of krpc_call leaks the reply mbuf chain, demonstrated on the guest through the verbatim production code: with an in-bounds 24-byte attacker reply carrying accept-status 2, 50/50 calls returned EBADRPC and netstat -m showed mbufs in use 7 -> 57, exactly one leaked mbuf per call, still allocated afterwards, while the honest-reply control loop stayed flat (57 -> 57); the DF-3203 coupling was also reproduced by planting 0x00000002 at mbuf offset 20..23 with a userland primer and sending a 16-byte short reply, which took the same leak exit on the uninitialized value 3/3 times with exactly +3 leaked mbufs. Root cause is exact: at krpc_subr.c:393-395 the PROGMISMATCH branch does goto out while m owns the chain taken from sio.sb_mb at :367, and the out: path at :437-441 frees only mhead and from and closes the socket. The real-world v2-only mountd fallback (md_mount tries MOUNT v3 then retries v1 on EBADRPC, nfs_mountrpc.c:140-163) drives the same exit once per boot RPC. Bounded impact: orphaned-but-allocated mbufs, no UAF or double free, rate-limited by the boot RPC pattern; not a local unpriv->root vector since krpc is boot-path only. The validated one-line fix (m_freem(m) before the goto out) removed the leak (+0 mbufs across 50 calls) while preserving EBADRPC semantics 50/50.
No comments yet.