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

krpc_call() early-error exits (socreate/sosetopt/sobind) leak the caller's request mbuf chain β€” md_mount/md_lookup_swap's 'message already freed' ownership contract is false

Summary

krpc_call chains the caller's request onto mhead only at krpc_subr.c:275-276; error exits before that point (:220,:231,:263) jump to out: which frees only mhead (NULL) - the request chain is leaked. md_mount's v3->v2 fallback reassigns m at :160 without freeing, and both md_mount:166 and md_lookup_swap:259 return under the comment 'message already freed', which is false for these exits. Bounded leak (<=~6 mbufs per boot); the trigger failures are practically unreachable in single-threaded early boot. No UAF/double-free variant (post-chaining exits free exactly once). Distinct from DF-3204 (reply-chain leak). Fix: free *data in out: when mhead==NULL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3210 Β· 5 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
verdict.json β€” 2.3 KB view raw
manifest.json β€” 827 B view raw
fix.diff β€” 483 B view raw

DF-3210 β€” krpc_call() early-error exits leak the caller's request mbuf chain; nfs_mountrpc.c's "message already freed" ownership contract is false

Files: sys/vfs/nfs/krpc_subr.c:213-265,275-276,437-441 (callee defect) manifesting at sys/vfs/nfs/nfs_mountrpc.c:146-166, 252-259 (callers).

What

krpc_call() only chains the caller's request mbuf (*data) onto mhead at krpc_subr.c:275-276. Every error exit before that point (socreate :220-221, sosetopt(SO_RCVTIMEO) :231-232, sobind :263-265) jumps to out: where only mhead (NULL) and from (NULL) are freed β€” the request chain is leaked. After chaining, all exits free it via m_freem(mhead).

The callers encode the opposite assumption: * md_mount v3 attempt (nfs_mountrpc.c:143-147): on error it falls into the v2 branch and reassigns m at :160 without freeing β€” leaks the v3 request chain when krpc_call exited early. * md_mount v2 (nfs_mountrpc.c:163-166): return error; /* message already freed */ β€” comment false on early exits. * md_lookup_swap (nfs_mountrpc.c:252-259): same return error; /* message already freed */.

Severity honesty

Bounded leak: at most one chain (1-2 mbufs) per failed call, ≀3 calls per boot β†’ ≀ ~6 mbufs. The triggering failures (socreate/sosetopt/sobind with all 512 swept reserved ports busy) are practically unreachable in the single-threaded early-boot context. No UAF, no double-free (verified: after a post-chaining error exit the request is freed exactly once as part of m_freem(mhead), and callers never dereference m after a failed krpc_call before reassignment β€” :146β†’:160 and :258-259). Filed as Info: real defect, no demonstrated impact.

Ceiling

Boot-path only (#if BOOTP||NFS_ROOT); audit guest boots hammer2 from disk, nfs_mountroot never runs β€” same not-runtime-testable ceiling as DF-3203/3204/3205.

VERDICT.md
↓ download raw

VERDICT β€” DF-3210

status: untested (boot-path-only; not runtime-testable on the audit guest) impact: none (bounded mbuf leak; no corruption, no reachable exhaustion) confidence: certain (ownership contract violation provable by inspection; trigger likelihood marginal)

Proof chain (path:line)

  1. sys/vfs/nfs/krpc_subr.c:275-276 β€” mhead = m_gethdr(M_WAITOK, MT_DATA); mhead->m_next = *data; β€” the caller's request becomes part of mhead's chain only from this point on.
  2. Error exits before :275 β€” socreate (:220-221), sosetopt(SO_RCVTIMEO) (:231-232), and sobind (:263-265, the reserved-port sweep loop) β€” all goto out.
  3. sys/vfs/nfs/krpc_subr.c:437-441 β€” out: frees only mhead (NULL on those paths) and from; *data is never freed and never reassigned on error β‡’ request chain leaked.
  4. Caller-side manifestation: * sys/vfs/nfs/nfs_mountrpc.c:146-147 v3 krpc_call error β†’ else branch at :151 β†’ m reassigned at :160 without m_freem(m) β‡’ v3 request leaked on early-exit errors. * sys/vfs/nfs/nfs_mountrpc.c:165-166 β€” return error; /* message already freed */ β€” false for early exits. * sys/vfs/nfs/nfs_mountrpc.c:258-259 β€” same false comment in md_lookup_swap; request (m_get head + xdr_string_encode chain, 2 mbufs) leaked.
  5. Not a UAF/double-free: for error exits after chaining (sosend :324-327, PROGMISMATCH :393-395 β€” that reply-chain variant is DF-3204, not re-reported), *data was freed exactly once inside m_freem(mhead); callers never touch m after a failed call before reassignment (:146β†’:160) or return (:259).

Trigger reality

socreate(AF_INET, SOCK_DGRAM) and sosetopt(SO_RCVTIMEO) do not fail in practice; sobind fails only when all ~512 swept reserved UDP ports (:256-262) are taken during single-threaded early boot. Bounded: ≀1 chain per failed call, ≀3 calls per boot.

Why untested

Only reachable inside the boot-path mount RPCs (#if BOOTP||NFS_ROOT); audit guest (DragonFly 6.5-DEVELOPMENT

0, X86_64_GENERIC, hammer2 disk root) never executes nfs_mountroot β†’ md_mount/md_lookup_swap.

Free the unconsumed request at out: when it was never chained:

--- a/sys/vfs/nfs/krpc_subr.c
+++ b/sys/vfs/nfs/krpc_subr.c
@@ -437,7 +437,13 @@
  out:
-   if (mhead) m_freem(mhead);
+   if (mhead) {
+       m_freem(mhead);
+   } else if (*data) {
+       /* Early error exit before the request was chained onto mhead.
+          Callers (nfs_mountrpc.c:166,259) expect krpc_call to consume it. */
+       m_freem(*data);
+       *data = NULL;
+   }
    if (from) kfree(from, M_SONAME);
    soclose(so, FNONBLOCK);
    return error;

(After gotreply, mhead != NULL and *data is the reply β€” path unaffected.)

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-3210

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3210/VERDICT.md steps 1-4; decisive lines: krpc_subr.c:275-276 (chaining point), :220-221/:231-232/:263-265 (pre-chain error exits), :437-441 (out: frees only mhead/from), nfs_mountrpc.c:146->160 (fallback reassign), :166/:259 (false 'message already freed' comments)

PoC changes

none β€” no runnable seed exists for a boot-path mbuf leak; verdict is an ownership/lifetime derivation over both files

Verified recommended fix

in krpc_call out: free *data when mhead==NULL (request never chained), matching the callers' 'krpc_call consumes the request' contract

Verdict

krpc_call() chains the caller's request mbuf onto mhead only at krpc_subr.c:275-276; its early error exits (socreate :220, sosetopt :231, sobind :263) jump to out: with mhead==NULL so *data is never freed, while both md_mount (nfs_mountrpc.c:163-166) and md_lookup_swap (:258-259) return on krpc_call error under the comment 'message already freed' β€” the ownership contract is false and the request chain leaks (md_mount's v3->v2 fallback additionally reassigns m at :160 without freeing). Inspection-certain defect; impact bounded to <=~6 mbufs per boot and the trigger failures are practically unreachable in single-threaded early boot, so filed Info. Not runtime-testable: boot-path-only code, guest boots hammer2 from disk (same ceiling as DF-3203/3204/3205).