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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2905 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 aSMBR_MULTIPACKETrequest 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_intthen callssmb_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-initssr_rpwithmd_doneon 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 != NULLwith a backtrace whose innermost frames aremd_done() β m_freem()β the kernel's own mbuf layer flagging the orphaned record list (seemfree_backtrace.txt) RESULT baseline-walk: delta=0β walking viamd_next_record()frees each record correctly (the leak is only at terminalmd_done)
Expected on the fixed copy (subr_mchain_fixed.c, generated from fix.diff):
RESULT fixed-tail: delta=0β leak goneRESULT fixed-walk: delta=0with identical rc/md_top sequence β record walking semantics preserved, no double-free
Files
mchain_leak.cKLD test module (compiles the realsubr_mchain.cin; stock GENERIC has no libmchain symbols, no clash)mbread.cuserland reader ofkern.ipc.mbstat(m_mbufs is first member)MakefileKLD makefile (mode via generatedwalkflag.h)build.sh,run.shexact guest commandsrun.log,run.2.logdecisive runs (incl. 3x stress matrix)mfree_backtrace.txtin-kernel evidence of the orphaned nextpkt listenv.txtguest identityfix.diffgit-apply-able fix (never applied to sys/)VERDICT.md,manifest.json,verdict.jsonverdict record
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:
- TAILTEST β builds an mdchain of 3 records (each a 4-mbuf chain),
attaches records 2 and 3 with
md_append_record(), then callsmd_done()and measuresmbstat.m_mbufs(exact live-mbuf counter: incremented inupdatestats()uipc_mbuf.c:1001, decremented inm_free():1447). - 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 onlym->m_next;m_nextpktchains are never followed bym_freem().sys/netproto/smb/smb_iod.c:353-372β iod receive loop attaches every same-MID message of aSMBR_MULTIPACKETrequest as an mdchain record (md_append_record), typically several within onerecvallpass, 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 pathsmb_rq_new(:115-116) leaks the same way.- Attacker = malicious/compromised SMB server; victim = kernel that mounted
its share (
/dev/nsmbis 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 toSMB_MAXPKTLEN= 128 KiB of attacker-chosen size (smb.h:292, framing bound insmb_trantcp.c:305). - Impact ceiling: unbounded mbuf/cluster consumption β
kern.ipc.nmbclustersexhaustion β 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 wholem_nextpktrecord list and frees each record withm_freem(); also clearsmd_cur(hardening for DF-2907).md_next_record()detaches the retired record (md_top->m_nextpkt = NULL) beforemd_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
fixedfix.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']
Confirmed kernel references
- sys/kern/libmchain/subr_mchain.c:323
- sys/kern/libmchain/subr_mchain.c:336
- sys/kern/libmchain/subr_mchain.c:356
- sys/kern/uipc_mbuf.c:1320
- sys/kern/uipc_mbuf.c:1334
- sys/netproto/smb/smb_iod.c:353
- sys/netproto/smb/smb_rq.c:116
- sys/netproto/smb/smb_rq.c:149
- sys/netproto/smb/smb_rq.c:531
- sys/netproto/smb/smb_rq.c:741
- sys/netproto/smb/smb.h:292
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.
No comments yet.