# DF-5757 — l2cap_ctloutput PRCO_GETOPT success-path mbuf leak

## Verdict: REPRODUCED + FIX VALIDATED (`fixed`)

The bug is real and the leak is **exactly 1 mbuf per successful
`getsockopt(BTPROTO_L2CAP, SO_L2CAP_IMTU)`**, deterministic, unprivileged,
no Bluetooth hardware required. The authored `fix.diff` closes it
completely (module-level fix validation: `netbt.ko` rebuilt with only this
fix, leak count goes flat, getsockopt still returns correct data).

## Mechanism (trigger → primitive → effect)

`l2cap_ctloutput()` is the L2CAP socket control-output handler
(`sys/netbt/l2cap_socket.c:95-144`). `PRCO_GETOPT` case, lines 119-132:

```c
case PRCO_GETOPT:
    m = m_get(M_NOWAIT, MT_DATA);              /* :120 allocate 1 mbuf */
    if (m == NULL) { error = ENOMEM; break; }
    m->m_len = l2cap_getopt(pcb, sopt->sopt_name, mtod(m, void *)); /* :125 */
    if (m->m_len == 0) {                       /* :126 error path      */
        m_freem(m);                            /* :127 only free here  */
        m = NULL;
        error = ENOPROTOOPT;
    }
    soopt_from_kbuf(sopt, mtod(m, void *), m->m_len); /* :131 copyout */
    break;                                     /* :132 m NEVER freed   */
```

For `SO_L2CAP_IMTU`, `l2cap_getopt()` (`sys/netbt/l2cap_upper.c:526-528`)
writes `chan->lc_imtu` and returns `sizeof(uint16_t)` = 2 (non-zero), so
the `m->m_len == 0` block is skipped every call. `soopt_from_kbuf()`
copies the 2-byte value out to userspace (it does not take ownership of
the buffer), and `break` exits with the mbuf allocated at :120 still
live and unreferenced by anything — a permanent 1-mbuf leak per call.

`m_get` charges the mbuf to the global `mbuf` objcache
(`sys/kern/uipc_mbuf.c:1009-1044`); nothing ever frees it, so
`netstat -m` "mbufs in use" grows monotonically by 1 per call. With the
guest's pool max of 146632 mbufs, ~146k getsockopt calls exhaust the
pool; from that point every `m_get(M_NOWAIT)` in the kernel fails
(ENOBUFS) until reboot — a permanent, unprivileged, system-wide
networking/memory DoS. (Not driven to exhaustion in this run to keep the
guest usable; the linear leak to pool-max is deterministic arithmetic.
The sco sibling DF-0709 did drive it to exhaustion and killed networking.)

## Reachability / threat model

* netbt is `optional bluetooth` (`sys/conf/files:1613`) — a KLD module,
  not in X86_64_GENERIC. Precondition: an admin `kldload netbt.ko` to
  enable Bluetooth support (normal admin action; DF-0709 established the
  same precondition). Once loaded, AF_BLUETOOTH/BTPROTO_L2CAP sockets are
  creatable by ANY local user (`socket(2)` needs no privilege; verified
  as uid 1001 `maxx`, not in wheel).
* No controller hardware needed: `l2cap_ctloutput` only requires
  `so->so_pcb != NULL` (:108), which `l2cap_sattach` sets at socket
  creation (`sys/netbt/l2cap_socket.c:295-317`).
* Impact ceiling: local kernel-memory-exhaustion DoS. Pure resource leak,
  no corruption primitive → no escalation chain (Phase V rule).

## Reproduction (unpatched netbt.ko, sha of stock module = guest /boot/kernel/netbt.ko)

Two independent runs (first boot, and fresh `vm.sh reset with-src` guest):

```
=== DF-5757 l2cap (fresh guest) ===
BEFORE: 7/146632 mbufs in use (current/max):
l2cap: iterations=10000 ok=10000 fail=0 last_val=672 optlen=2
AFTER 10k l2cap: 10007/146632 mbufs in use (current/max):
```
Delta = +10000 mbufs / 10000 calls, `last_val=672` = L2CAP_MTU_DEFAULT
returned correctly. (run.log, run.2.log; the first-guest capture in
run.2.log shows the same 7 -> 10007.)

## Fix validation (fix.diff applied to /usr/src/sys/netbt, netbt.ko rebuilt)

```
=== PATCHED MODULE: DF-5757 l2cap ===
BEFORE: 7/146632 mbufs in use (current/max):
l2cap: iterations=10000 ok=10000 fail=0 last_val=672 optlen=2
AFTER 10k l2cap: 7/146632 mbufs in use (current/max):
```
Flat (7 → 7), correct value still returned. Build: `cd /usr/src/sys/netbt
&& make` rc=0 under `-Werror` (fix_build.log), patched netbt.ko sha256
f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e.

Note: the same fix hunk also removes the `m = NULL` +
`soopt_from_kbuf(sopt, mtod(m, ...), m->m_len)` NULL-deref panic on the
error path (pass-1 DF-0682); that behavior is known and was not
re-verified separately here.

## Rig notes

`bt_getopt_leak.c` was written fresh for this run (no seed existed); the
only fix-up vs the first compile attempt was none — it built and ran
first try. attempts=2 (two full guest runs).

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