sys_sendmsg() leaks the MT_CONTROL mbuf when kern_sendmsg() early-returns at holdsock() failure β unprivileged, unbounded, permanent kernel mbuf-pool exhaustion (reboot-only local DoS)
Summary
sendmsg with ancillary data on a non-socket fd allocates an MT_CONTROL mbuf in sys_sendmsg, then kern_sendmsg returns at its very first statement group β the holdsock() failure for ENOTSOCK/EBADF β before sosend() ever takes ownership; sys_sendmsg's cleanup frees iov and sa but never control. Every path that reaches sosend consumes control on all outcomes, so the holdsock early return is the sole leak window: one 256-byte mbuf leaked permanently per failing call. Verified as uid 1000: exactly +1 mbuf/call linear and permanent (7->20007->40007->60007 of 146632; never reclaimed after exit; reproduces from fresh reset); a single detached 3M-iteration run exhausted the entire mbuf pool (154453 attributed by the kernel to 'ancillary data', 55% of mb_map, 133 kernel allocation requests denied system-wide) with objcache-exhausted console warnings and the leaking process D-state immune to kill -9 β reboot the only recovery. Network-stack-wide DoS from one unprivileged process in under a minute. consume-control-on-early-return fix proposed; not a memory-corruption class.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5830 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leaksendmsg.c | β | 3.0 KB | view raw | |
| build.sh | β | 132 B | view raw | |
| run.sh | β | 240 B | view raw | |
| build.log | β | 262 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.2.log | β | 783 B | view raw | |
| run.3.log | β | 2.7 KB | view raw | |
| wedge_evidence.txt | β | 851 B | view raw | |
| console.txt | β | 1.4 KB | view raw | |
| fix_validation.log | β | 2.3 KB | view raw | |
| fix.diff | β | 529 B | view raw | |
| env.txt | β | 679 B | view raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| README.md | β | 2.3 KB | β raw | |
| verdict.json | β | 5.1 KB | view raw |
DF-5830 β sys_sendmsg() leaks the control mbuf when kern_sendmsg() bails out early
Severity: Medium (unprivileged, permanent kernel-memory exhaustion; local DoS)
Class: CWE-401 missing release of kernel resource; bucket base:kern / DoS
File: sys/kern/uipc_syscalls.c
Status: reproduced on guest (stock kernel), fix validated on patched kernel.
What
sendmsg(2) with ancillary (control) data on a non-socket fd (or any fd for
which holdsock() fails: EBADF, ENOTSOCK) leaks one MT_CONTROL mbuf β
permanently β inside the kernel. Any local user can loop this without limit.
Build
cc -O2 -Wall -o leaksendmsg leaksendmsg.c # any unprivileged user
Run
netstat -m | sed -n 1p # note mbufs-in-use ./leaksendmsg 20000 # 20000 failing sendmsg() on a regular-file fd netstat -m | sed -n 1p # +20000 mbufs, never reclaimed ./leaksendmsg 20000 ok # control experiment: real socketpair, NO growth
Expected output (vulnerable kernel)
OK: 20000 sendmsg calls on regular-file fd (all ENOTSOCK) 7/146632 -> 20007/146632 mbufs in use (current/max):
(linear: each round of N calls adds exactly N mbufs; count never drops, even after the leaking process exits).
Full-wedge demonstration (throwaway guest only!)
./leaksendmsg 3000000 &
exhausts the whole mbuf pool (~146k mbufs / ~37-77 MB) in under a minute:
serial console spews Warning: objcache(mbuf) exhausted on cpuN!, kernel
denies network-memory requests system-wide, and the leaking process parks in
state D (wchan objcache) immune to kill -9 β only a reboot recovers.
See run.3.log for the full transcript.
Patched kernel (fix.diff)
7/146632 -> 7/146632 -> 7/146632 across the identical protocol; happy-path
(control consumed on a real socket) and EFAULT-copyin path both regression-clean.
See fix_validation.log.
Evidence files
run.logβ baseline: control experiment + 3 linear leak roundsrun.2.logβ fresh-boot (postvm.sh reset with-src) re-confirmationrun.3.logβ full-pool exhaustion wedge,kill -9immunity, kernel's own "154453 mbufs allocated to ancillary data" attributionwedge_evidence.txt,console.txtβ raw snapshotsfix_validation.log,fix.diff,env.txt,manifest.json,verdict.json
DF-5830 β VERDICT
Finding: sys_sendmsg() leaks the MT_CONTROL mbuf when kern_sendmsg()
returns early at its holdsock() failure β one permanently-leaked mbuf per
failing sendmsg(2) with ancillary data, from any unprivileged user, with no
bound and no reclamation. Result: permanent kernel network-memory exhaustion
(local DoS; the wedged leaking process is even immune to SIGKILL once the
pool is dry).
Verdict: REPRODUCED (impact = dos, confidence = certain).
Root cause (path:line, this tree)
sys/kern/uipc_syscalls.c:934-948(sys_sendmsg): whenmsg.msg_control != NULLandsizeof(struct cmsghdr) <= msg_controllen <= MLEN, the kernel allocatescontrol = m_get(M_WAITOK, MT_CONTROL), setscontrol->m_len = msg.msg_controllenand copies in the user bytes. Ownership of that mbuf is now the caller's until it is handed tokern_sendmsg().sys/kern/uipc_syscalls.c:799-801(kern_sendmsg):c error = holdsock(td, s, &fp); if (error) return (error); /* <-- control mbuf never freed */For a non-socket fd (ENOTSOCK) or bad fd (EBADF) the function returns beforeso_pru_sosend()is ever reached β so no consumer takes the mbuf.- Every path that does reach
sosend()consumes/freescontrolon all outcomes β genericsosend()atsys/kern/uipc_socket.c:938-942(out:label freescontrol),sosend_dgram()atsys/kern/uipc_socket.c:1060-1061,sosend_tcp()atsys/kern/uipc_socket.c:1110-1111. So the only leak window is theholdsock()early return. sys/kern/uipc_syscalls.c:953-958(sys_sendmsgcleanup):c cleanup: iovec_free(&iov, aiov); cleanup2: if (sa) kfree(sa, M_SONAME); return (error);freesiovandsabut nevercontrol. OneMT_CONTROLmbuf (256 bytes of objcache memory) is leaked per call.
The only other in-tree callers of kern_sendmsg() pass control == NULL
(sys_sendto at uipc_syscalls.c:876, sendfile trailers at
uipc_syscalls.c:1666), so the bug is specific to sendmsg(2) β the classic
ancillary-data path (every SCM_RIGHTS/SCM_CREDS fd-passing program shape,
just aimed at a non-socket fd).
Threat model
Unprivileged local user. Requirements: one open fd that is not a socket (a
regular file is enough β DTYPE_VNODE β ENOTSOCK), a 16..224-byte user
buffer for msg_control, and a loop. No jail, no vfs.usermount, no special
sysctls. Tested as uid 1000.
Reproduction (stock kernel, DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026)
Control experiment β 20000 successful sendmsg with control on a real
AF_UNIX socketpair: mbuf count unchanged (7/146632). The success path
does not leak.
Leak experiment β 3 rounds of 20000 failing sendmsg on a regular-file fd,
run as unprivileged maxx:
7/146632 -> 20007/146632 -> 40007/146632 -> 60007/146632 mbufs in use
Exactly +1 mbuf per call; count never drops after process exit (permanent).
Re-confirmed from a fresh boot after vm.sh reset with-src:
7/146632 -> 20007/146632 for one round (run.2.log).
Exhaustion experiment β leaksendmsg 3000000 (detached, unprivileged):
the counter blew through the pool (154460/146632) and froze; the serial
console emitted Warning: objcache(mbuf) exhausted on cpu0/1/4/5!;
netstat -m reported 154453 mbufs "allocated to ancillary data", 77742
Kbytes pinned (55% of mb_map), and 133 kernel memory requests denied
system-wide. The leaking process sat in state D (wchan objcache) and
survived kill -9 (run.3.log). Only a guest reset recovered.
Elapsed cost: 20000 leaked mbufs in ~2-3 s; full pool (~146k mbufs / ~37 MB) exhausted in well under a minute by a single process.
Why not worse (impact ceiling)
This is resource exhaustion, not memory corruption: the leaked mbufs are correctly allocated, initialized and simply abandoned β no OOB, no UAF, no info leak. The ceiling is denial of service (permanent network-stack memory starvation + unkillable D-state processes). Categorized per the project rubric as Medium (local DoS), consistent with sibling finding DF-2714.
Fix (validated)
fix.diff makes kern_sendmsg() take ownership of control on the
holdsock() early return β matching sosend()'s all-paths consumption
contract (so kern_sendmsg() now consumes control on every exit).
Two-line semantic change; all other callers pass control == NULL.
Patched-kernel validation (guest /usr/src + fix.diff,
make nativekernel, reboot into #1 Oct 13 09:45:32):
- identical 3x20000 leak protocol: 7/146632 -> 7/146632 -> 7/146632
(baseline was 7 -> 20007 -> 40007 -> 60007) β leak gone;
- 20000 happy-path sendmsg-with-control on a real socketpair: no panic, no
double-free (sosend still consumes), no growth;
- 50000 control-copyin-EFAULT iterations (the m_free(control) path at
uipc_syscalls.c:944-947): no double-free, no panic.
fix_status: fixed; fix_baseline_reproduced: 1; fix_patched_reproduced: 0
(the leak no longer reproduces on the patched kernel).
Honesty notes
- Guest was dirtied by the wedge run and the patched-kernel install;
vm.sh reset with-srcwas executed afterwards (fresh clean-source boot). - The +1 mbuf seen in the EFAULT regression run (
8/146632) is the ssh exec's own transient socket, not cumulative: 50000 iterations produced no growth. sys_sendfile()was re-checked for the same shape: its header mbuf chain is explicitly freed on the early-error path (m_freem(mheader)atuipc_syscalls.c:1970-1971) β no sibling leak there.
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
fixedFIXED. fix.diff (kern_sendmsg frees control on the holdsock() early return) applied inside the guest /usr/src copy, kernel rebuilt (make -j4 nativekernel KERNCONF=X86_64_GENERIC, 367 s) and booted (#1 Oct 13 09:45:32). Baseline protocol 3x20000 leak rounds: stock kernel 7->20007->40007->60007; patched kernel 7->7->7 (leak gone). Regression: 20000 happy-path sendmsg-with-control on a real socketpair (no double-free, no panic) and 50000 control-copyin-EFAULT iterations (m_free path at uipc_syscalls.c:944-947, no double-free, no panic). fix_patched_reproduced=0 means the leak no longer reproduces on the patched kernel.
fix_validation.log (build+install transcript, patched-kernel run outputs); fix.diff (git-apply-able, sha256 352a552bd9119a2b753b4d6e5a54441634c49bff25a40689d08d49c7db6140c6)
Confirmed kernel references
Detail
Exploit chain
unprivileged user opens any regular file (DTYPE_VNODE fd) and loops sendmsg(fd, msghdr with 16..MLEN bytes of msg_control, 0); each call leaks one MT_CONTROL mbuf permanently; ~146k iterations (~37 MB, <1 minute) exhaust the mbuf objcache system-wide: network-memory allocations are denied, console spews objcache(mbuf) exhausted, and the loop parks in m_get(M_WAITOK) in D-state surviving kill -9. No privilege or special configuration required.
Evidence (decisive lines)
run.log (control experiment: real socketpair 20000x => no growth; then 3x20000 leak rounds => 7->20007->40007->60007 exactly +1/call); run.2.log (fresh-boot re-confirmation 7->20007); run.3.log + wedge_evidence.txt (full-pool exhaustion: 154460/146632 frozen, '154453 mbufs allocated to ancillary data', 133 requests denied, kill -9 immunity, objcache-exhausted console warnings); fix_validation.log + fix.diff (patched kernel #1 Oct 13: same protocol => 7/146632 flat across 3 rounds; happy-path and EFAULT-copyin regression clean).
PoC changes
PoC authored fresh for this finding (no seed). Uses only open()+sendmsg()+socketpair(); dual mode: leak mode (regular-file fd, expects ENOTSOCK per call) and 'ok' control mode (real AF_UNIX socketpair with SCM_RIGHTS control actually consumed by the peer).
Verified recommended fix
kern_sendmsg(): on the holdsock() early-return path, m_freem(control) before returning, making kern_sendmsg() consume the control mbuf on all paths (matching sosend()'s contract) - see fix.diff.
Verdict
REPRODUCED on the stock guest kernel (DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026), as unprivileged uid 1000, from a fresh vm.sh reset. sys_sendmsg() (sys/kern/uipc_syscalls.c:934-948) allocates the MT_CONTROL mbuf and copies in user ancillary data, then calls kern_sendmsg(), which returns early at its holdsock() failure (uipc_syscalls.c:799-801) for a non-socket/bad fd BEFORE any consumer takes the mbuf; sys_sendmsg's cleanup (uipc_syscalls.c:953-958) frees iov and sa but never control. Exactly one 256-byte mbuf is leaked per failing sendmsg(2): measured 7->20007->40007->60007/146632 mbufs for 3x20000 calls, permanent (never reclaimed after process exit), linear, and reproducible from a fresh boot. A detached 3M-iteration run exhausted the pool (154460 mbufs, 154453 attributed by the kernel to 'ancillary data', 77742 Kbytes, 55% of mb_map, 133 kernel allocation requests denied), spewed 'Warning: objcache(mbuf) exhausted on cpu0/1/4/5!' on the serial console, and left the leaking process in state D (wchan objcache) immune to kill -9 - a reboot-only recovery. Impact is local DoS (resource exhaustion, no memory corruption); ceiling consistent with sibling finding DF-2714, hence Medium.
No comments yet.