# 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.
