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

md_done() leaks every pending m_nextpkt record of an mdchain β€” remote mbuf-pool exhaustion DoS by a malicious SMB server

Field Value
ID DF-2905
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-772 Missing Release of Resource after Effective Lifetime
File sys/kern/libmchain/subr_mchain.c
Lines 323-330, 336-351, 369
Area libmchain (legacy smb consumer)
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket legacy
Reported pending
Known CVE none
CVE match novel

Summary

md_done() tears an mdchain down with m_freem(md_top), but DFly m_free() follows only m->m_next (uipc_mbuf.c:1320) and explicitly warns 'mfree: m->m_nextpkt != NULL' (uipc_mbuf.c:1334-1339) without freeing the record list. Any record attached via md_append_record() and not yet consumed by md_next_record() when md_done() runs is permanently leaked. A malicious SMB server attaches attacker-sized (≀128KiB) same-MID response records to TRANS2 request mdchains (smb_iod.c:353-372, SMBR_MULTIPACKET at smb_rq.c:587); smb_t2_reply exits on success or first parse error with records pending (smb_rq.c:531-545) and smb_rq_done/smb_rq_new then call md_done β€” orphaning each remaining record. Line-rate flood β†’ mbuf/cluster pool exhaustion β†’ kernel-wide networking DoS. Distinct defect and trigger from pass-1 DF-0627 (duplicate-drop leak in smb_iod.c): this is the library-level record-tail leak; DF-0627's fix does not cover it. Attack surface: malicious/compromised SMB server vs a root-mounted smbfs share (/dev/nsmb 0600 β€” no unpriv local surface; legacy bucket).

Proof of contest

VERIFIED library-level (findings/poc/DF-2905/): KLD compiling the audited file builds a 3-record Γ— 4-mbuf mdchain, md_append_record Γ—2, md_done() β†’ only record 1 freed: mbstat.m_mbufs delta +8 per load (6/6), in-kernel 'mfree: m->m_nextpkt != NULL' backtrace md_done()←m_freem(). Fix validated: patched build delta 0 (3/3), identical md_next_record walking, no double-free. Remote end-to-end (fake SMB1 server + mounted share) not executed; reachability by code trace. No uid=0 route for a pure leak.

Walk the whole record list in md_done + detach the promoted record in md_next_record (also clears md_cur β€” closes the DF-2907 hazard). Validated fix.diff in findings/poc/DF-2905/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_mchain.c (GLM 5.3); leak reproduced deterministically + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2905 Β· 15 files
FileTypeDescriptionSize
mchain_leak.c β€” 3.2 KB view raw
mbread.c β€” 725 B view raw
Makefile β€” 425 B ↓ download
build.sh β€” 1.1 KB view raw
run.sh β€” 845 B view raw
build_base_tail.log β€” 7.1 KB view raw
build_fix_tail.log β€” 7.0 KB view raw
run.log β€” 352 B view raw
run.2.log β€” 681 B view raw
mfree_backtrace.txt β€” 1.1 KB view raw
env.txt β€” 518 B view raw
fix.diff β€” 581 B view raw
subr_mchain_fixed.c β€” 11.1 KB view raw
VERDICT.md β€” 4.8 KB ↓ raw
README.md β€” 3.8 KB ↓ raw

DF-2905 β€” md_done() leaks all pending m_nextpkt records of an mdchain

File: sys/kern/libmchain/subr_mchain.c (md_done, :323-330; md_append_record/md_next_record, :336-369) Severity: High Β· Class: resource leak β†’ remote memory-exhaustion DoS Β· Bucket: legacy (only consumer: sys/netproto/smb/, netsmb/smbfs)

What the bug is

md_done() tears down an mdchain with m_freem(md_top). On DragonFly, m_free() follows only m->m_next (sys/kern/uipc_mbuf.c:1320) and even prints mfree: m->m_nextpkt != NULL when the mbuf being freed still has a record link (:1334-1339) β€” but it does not free the m_nextpkt list. Whenever an mdchain has records appended via md_append_record() and md_done() runs before every record was consumed through md_next_record(), every remaining record chain is orphaned: a permanent, attacker-sized mbuf leak.

In-tree reachability (malicious SMB server β†’ root-mounted smbfs client):

  • smb_iod_recvall (sys/netproto/smb/smb_iod.c:353-372) attaches every same-MID NetBIOS message of a SMBR_MULTIPACKET request as a new record (md_initm/md_append_record), draining the socket in one loop before the requester thread wakes.
  • smb_t2_reply (sys/netproto/smb/smb_rq.c:474-545) breaks out of its record loop on success (:531-534) or on any parse error, without consuming remaining records; smb_t2_request_int then calls smb_rq_done β†’ md_done(&rqp->sr_rp) (smb_rq.c:741-743, 149) β€” all still-attached records leak.
  • smb_rq_new (smb_rq.c:115-116) re-inits sr_rp with md_done on request reuse β€” same leak on the retry path.

Each leaked record is a full server-controlled message up to SMB_MAXPKTLEN (0x1FFFF = 128 KiB, sys/netproto/smb/smb.h:292). A sustained flood of extra same-MID responses exhausts the mbuf/cluster pools (kern.ipc.nmbclusters) β†’ system-wide networking DoS/panic. Same threat model as pass-1 finding DF-0627 (duplicate-response leak in smb_iod.c), but a different defect and trigger: unconsumed record tails at md_done(), in the mchain library itself.

How to reproduce (single-tenant QEMU guest, root)

# host: pack transferred to guest /root/df2905
ssh dfbsd   # (vm.sh's ssh config; see dfbsd-qemu/)
cd /root/df2905
sh build.sh          # builds mbread + 4 KLD variants (base/fix x tail/walk)
sh run.sh            # measures live mbufs (mbstat.m_mbufs) around each kldload

Expected on stock (vulnerable) subr_mchain.c:

  • RESULT baseline-tail: delta=+8 (3 records x 4 mbufs; md_done frees only record 1; Β±1 mbuf noise from the ssh session itself)
  • kernel console shows mfree: m->m_nextpkt != NULL with a backtrace whose innermost frames are md_done() β†’ m_freem() β€” the kernel's own mbuf layer flagging the orphaned record list (see mfree_backtrace.txt)
  • RESULT baseline-walk: delta=0 β€” walking via md_next_record() frees each record correctly (the leak is only at terminal md_done)

Expected on the fixed copy (subr_mchain_fixed.c, generated from fix.diff):

  • RESULT fixed-tail: delta=0 β€” leak gone
  • RESULT fixed-walk: delta=0 with identical rc/md_top sequence β€” record walking semantics preserved, no double-free

Files

  • mchain_leak.c KLD test module (compiles the real subr_mchain.c in; stock GENERIC has no libmchain symbols, no clash)
  • mbread.c userland reader of kern.ipc.mbstat (m_mbufs is first member)
  • Makefile KLD makefile (mode via generated walkflag.h)
  • build.sh, run.sh exact guest commands
  • run.log, run.2.log decisive runs (incl. 3x stress matrix)
  • mfree_backtrace.txt in-kernel evidence of the orphaned nextpkt list
  • env.txt guest identity
  • fix.diff git-apply-able fix (never applied to sys/)
  • VERDICT.md, manifest.json, verdict.json verdict record
VERDICT.md
↓ download raw

VERDICT β€” DF-2905

Status: reproduced Β· Impact: dos (remote mbuf-pool exhaustion; memory-safety class: permanent kernel heap leak, not corruption) Β· Confidence: certain

What was run

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS stock), uname -a in env.txt. The real sys/kern/libmchain/subr_mchain.c was compiled into a KLD (mchain_leak.c) β€” verified byte-identical to the audited tree (sha256 prefix aa1bf6c02603854c on host and in the guest build dir). The module:

  1. TAILTEST β€” builds an mdchain of 3 records (each a 4-mbuf chain), attaches records 2 and 3 with md_append_record(), then calls md_done() and measures mbstat.m_mbufs (exact live-mbuf counter: incremented in updatestats() uipc_mbuf.c:1001, decremented in m_free() :1447).
  2. WALKTEST β€” same topology, then md_next_record() Γ—3 (expect rc 0,0,ENOENT and md_top == rec2,rec3,NULL) + md_done(); must free everything on both vulnerable and fixed code (guards the fix against breaking walk semantics or double-freeing).

Result

run vulnerable fixed
tail (main, run.log) +8 mbufs leaked (7-8, Β±1 ssh-session noise) 0
tail (stress Γ—3, run.2.log) +8, +8, +7 0, 0, 0
walk (main) 0 (rc=0,0,ENOENT; tops correct) 0 (identical)

Cumulative: netstat -m went from 7 β†’ 55 mbufs in use after 6 vulnerable module loads (6 Γ— 8 = 48 leaked) while fixed loads leaked exactly 0.

Independent in-kernel evidence: every vulnerable md_done() emits

mfree: m->m_nextpkt != NULL
Trace ... md_done() at md_done+0x1c <- m_freem() <- md_done() ...

(mfree_backtrace.txt) β€” the mbuf layer's own warning that a record list is being orphaned, with the backtrace pointing at md_done().

Why this is exploitable (reachability trace, code-only but complete)

  • sys/kern/uipc_mbuf.c:1320 β€” m_free() returns/frees only m->m_next; m_nextpkt chains are never followed by m_freem().
  • sys/netproto/smb/smb_iod.c:353-372 β€” iod receive loop attaches every same-MID message of a SMBR_MULTIPACKET request as an mdchain record (md_append_record), typically several within one recvall pass, before the requesting thread resumes.
  • sys/netproto/smb/smb_rq.c:474-545 (smb_t2_reply) exits its record loop on success (:531-534) or first parse error without draining remaining records; :741-743 β†’ smb_rq_done (:146-153) β†’ md_done(&rqp->sr_rp) frees only the current record; the rest leak. Retry path smb_rq_new (:115-116) leaks the same way.
  • Attacker = malicious/compromised SMB server; victim = kernel that mounted its share (/dev/nsmb is 0600, so the local user must be root to set up, but the remote server needs no authentication beyond the session β€” it simply appends extra same-MID responses to a TRANS2 exchange, each up to SMB_MAXPKTLEN = 128 KiB of attacker-chosen size (smb.h:292, framing bound in smb_trantcp.c:305).
  • Impact ceiling: unbounded mbuf/cluster consumption β†’ kern.ipc.nmbclusters exhaustion β†’ network stack starvation / kernel OOM conditions. Deterministic per-response leak of up to ~128 KiB mbufs; repeatable at line rate.

Distinct from pass-1 DF-0627 (leak of a dropped duplicate chain in smb_iod.c:363-366): DF-2905 is the library-level record-tail leak at md_done(); DF-0627's one-line fix in smb_iod.c does not cover it.

Fix validation

fix.diff (authored post-verification, never applied to sys/):

  • md_done() walks the whole m_nextpkt record list and frees each record with m_freem(); also clears md_cur (hardening for DF-2907).
  • md_next_record() detaches the retired record (md_top->m_nextpkt = NULL) before md_done() so the promoted record is not double-freed.

Built as build_fix_*/mchainleak.ko against the same guest: tail delta 0, walk semantics identical (rc 0,0,ENOENT; md_top sequence unchanged), no double-free, module loads/unloads cleanly. All md_done() call sites were audited (smb_trantcp.c:272, smb_rq.c:116,149,417-418,747-748) β€” none rely on partial-free semantics, and record chains only ever carry nextpkt on sr_rp.

Module-level A/B was chosen over a full nativekernel rebuild because the defect is confined to this file and the harness links the identical source into both variants β€” a controlled experiment with the same allocator, counters and call path. Baseline and patched behaviors differ exactly on the defect.

Not reproducible / out of scope here

  • Turning the leak into memory corruption: none β€” this is a pure leak.
  • The full malicious-SMB-server end-to-end (needs a fake SMB1 server and a mounted share; reachability established by code trace above, guest test proves the library defect deterministically at the exact md_done() call the smb paths invoke).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to a copy of the audited file and built into the same KLD harness: baseline md_done leaks +8 mbufs per call (6/6 loads), patched leaks 0 (3/3 loads) and preserves md_next_record walking exactly (rc 0,0,ENOENT; md_top=rec2,rec3,NULL; no double-free; clean kldload/kldunload).

['run.2.log stress matrix (3x baseline +8 each vs 3x fixed 0 each)', 'run.log RESULT lines all four variants', 'fix.diff', 'subr_mchain_fixed.c']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

malicious SMB server -> extra same-MID NetBIOS messages for a MULTIPACKET (TRANS2) request -> smb_iod_recvall md_append_record() attaches each as mdchain record -> t2 reply loop exits on success/first error with records pending -> smb_rq_done -> md_done() frees only current record (m_freem follows m_next only, uipc_mbuf.c:1320) -> remaining records orphaned -> repeat flood -> mbuf/cluster pool exhaustion -> kernel-wide networking DoS (no memory-corruption avenue; pure leak).

Evidence (decisive lines)

['run.log: RESULT baseline-tail delta=+8 vs RESULT fixed-tail delta=0', 'run.2.log: 3x stress matrix (+8/+8/+7 vs 0/0/0), netstat -m 7 -> 55 mbufs in use after 6 vulnerable loads', "mfree_backtrace.txt: 'mfree: m->m_nextpkt != NULL' backtrace md_done() <- m_freem() from kldload", 'dmesg WALKTEST lines in run.log session: rc=0,0,ENOENT, md_top=rec2,rec3,NULL on both variants', 'build_base_tail/subr_mchain.c sha256 prefix aa1bf6c02603854c == audited sys/ tree']

PoC changes

No seed PoC existed (pass-2 new finding). Wrote KLD harness compiling the real subr_mchain.c into the module (stock GENERIC lacks libmchain symbols); userland mbstat.m_mbufs reader as the leak oracle; walk-mode regression guard for the fix. Two compile iterations: added sys/malloc.h (M_WAITOK), guarded unused-function under -Werror, replaced fragile make-mode CFLAGS with a generated walkflag.h after the first walk builds silently compiled the tail test.

Verified recommended fix

md_done(): free the entire m_nextpkt record list (save next=m->m_nextpkt before m_freem, loop), clear md_cur; md_next_record(): detach the retired record (md_top->m_nextpkt=NULL) before md_done so the promoted record is not double-freed.

Verdict

md_done() tears down an mdchain with m_freem(), which follows only m_next and never frees the m_nextpkt record list; any record still attached when md_done() runs is permanently leaked. Proven on the guest by compiling the audited subr_mchain.c into a KLD: md_done() on a 3-record chain frees 4 of 12 mbufs (delta +8 live mbufs, repeated 6x, with the kernel's own 'mfree: m->m_nextpkt != NULL' backtrace pointing at md_done), while a build patched by fix.diff frees all 12 (delta 0) with identical md_next_record walking semantics. In-tree reachability: a malicious SMB server attaches attacker-sized (<=128KiB) same-MID response records to TRANS2 request mdchains (smb_iod.c:353-372); smb_t2_reply/smb_rq_done call md_done without draining them (smb_rq.c:531-545, 741-743, 115-116), so each extra response leaks a full mbuf chain at line rate -> remote mbuf-pool exhaustion DoS against any kernel that smbfs-mounted the attacker's share.