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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5830 Β· 15 files
FileTypeDescriptionSize
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 rounds
  • run.2.log β€” fresh-boot (post vm.sh reset with-src) re-confirmation
  • run.3.log β€” full-pool exhaustion wedge, kill -9 immunity, kernel's own "154453 mbufs allocated to ancillary data" attribution
  • wedge_evidence.txt, console.txt β€” raw snapshots
  • fix_validation.log, fix.diff, env.txt, manifest.json, verdict.json
VERDICT.md
↓ download raw

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)

  1. sys/kern/uipc_syscalls.c:934-948 (sys_sendmsg): when msg.msg_control != NULL and sizeof(struct cmsghdr) <= msg_controllen <= MLEN, the kernel allocates control = m_get(M_WAITOK, MT_CONTROL), sets control->m_len = msg.msg_controllen and copies in the user bytes. Ownership of that mbuf is now the caller's until it is handed to kern_sendmsg().
  2. 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 before so_pru_sosend() is ever reached β€” so no consumer takes the mbuf.
  3. Every path that does reach sosend() consumes/frees control on all outcomes β€” generic sosend() at sys/kern/uipc_socket.c:938-942 (out: label frees control), sosend_dgram() at sys/kern/uipc_socket.c:1060-1061, sosend_tcp() at sys/kern/uipc_socket.c:1110-1111. So the only leak window is the holdsock() early return.
  4. sys/kern/uipc_syscalls.c:953-958 (sys_sendmsg cleanup): c cleanup: iovec_free(&iov, aiov); cleanup2: if (sa) kfree(sa, M_SONAME); return (error); frees iov and sa but never control. One MT_CONTROL mbuf (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-src was 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) at uipc_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

fixed
baseline reproduced→ patch + rebuild →patched clean

FIXED. 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)
↓ fix.diffper-fix-DF-5830

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.