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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3204 Β· 20 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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_testable
↓ fix.diffper-fix-DF-3204

Confirmed 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.