# DF-0709 — sco_ctloutput PRCO_GETOPT mbuf leak

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

The bug is real and the leak is exactly **1 mbuf per `getsockopt(BTPROTO_SCO, SO_SCO_MTU)` call**, deterministic and reproducible. The authored `fix.diff` closes it completely (verified by building a single-fix `netbt.ko` module, loading it, and re-running the same workload — mbuf count goes flat).

## Mechanism (trigger → primitive → effect)

`sco_ctloutput()` is the Bluetooth SCO socket control-output (`getsockopt`/`setsockopt`) handler. In the `PRCO_GETOPT` (getsockopt) case, `sys/netbt/sco_socket.c:111-121`:

```c
case PRCO_GETOPT:
    m = m_get(M_WAITOK, MT_DATA);          /* :111 allocate 1 mbuf         */
    m->m_len = sco_getopt(pcb, sopt->sopt_name, mtod(m, uint8_t *));   /* :112 */
    if (m->m_len == 0) {                   /* :113 error path              */
        m_freem(m);
        m = NULL;
        err = ENOPROTOOPT;
    }
    /* *opt = m; */
    /* XXX There are possible memory leaks (Griffin) */   /* :119 the author flagged it */
    soopt_from_kbuf(sopt, mtod(m, void *), m->m_len);    /* :120 copy result to user */
    break;                                  /* :121 *** m is NEVER freed *** */
```

For `SO_SCO_MTU`, `sco_getopt()` (`sys/netbt/sco_upper.c:345-347`) returns `sizeof(uint16_t)` = **2** (non-zero), so the `if (m->m_len == 0)` block at :113-117 is **skipped** every call. Execution falls straight through :120 (`soopt_from_kbuf` copies the 2-byte MTU out to userspace) and `break` at :121. **The mbuf allocated at :111 is never freed on this success path.** Compare the sibling `PRCO_SETOPT` case at :123-135, which correctly calls `m_freem(m)` at :134 — GETOPT is missing the equivalent free.

The author left a self-incriminating comment at :119: `/* XXX There are possible memory leaks (Griffin) */`.

**Effect**: every successful `getsockopt(SO_SCO_MTU)` permanently leaks one mbuf. The mbuf pool (`146632` mbufs max on this guest) is exhausted after ~146k calls, after which all kernel networking fails (observed: the guest's network stack died mid-test during a runaway loop, requiring a reset — concrete DoS).

## Reachability / threat model

- The netbt Bluetooth stack is **not** compiled into `X86_64_GENERIC`; it is a loadable module `netbt.ko` (`sys/conf/files`: `netbt/*` are `optional bluetooth`). It is not auto-loaded.
- **Precondition (realistic):** an administrator runs `kldload netbt.ko` to enable Bluetooth support. This is a normal admin action (you load the module because you want Bluetooth). Once loaded, the Bluetooth socket domain (`AF_BLUETOOTH`/`BTPROTO_SCO`) is open to **any local user** — no privilege or special device is required to create an SCO socket and call `getsockopt`.
- Verified: the unprivileged user `maxx` (uid 1001, not in wheel) can `socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)` and trigger the leak with no further setup. No Bluetooth controller hardware is needed — the socket and its PCB are created without any adapter attached.
- Impact ceiling: **local memory-exhaustion DoS**. A pure resource leak — no memory-corruption primitive, so per Phase 6 there is **no escalation chain** to develop.

## Reproduction (unpatched `#0` baseline, netbt.ko loaded)

```
BEFORE(baseline): 7/146632 mbufs in use (current/max):
iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0
AFTER 10k(baseline): 10007/146632 mbufs in use (current/max):
```
Delta = **+10000 mbufs per 10000 calls** = exactly 1 mbuf leaked per `getsockopt`. Confirmed across multiple runs (7→10007→20007 in an earlier run; deterministic).

## Fix

`fix.diff` restructures the `PRCO_GETOPT` case so `soopt_from_kbuf()` runs only on success (m_len != 0) and the mbuf is freed **unconditionally** on the way out:

```c
case PRCO_GETOPT:
    m = m_get(M_WAITOK, MT_DATA);
    m->m_len = sco_getopt(pcb, sopt->sopt_name, mtod(m, uint8_t *));
    if (m->m_len == 0) {
        err = ENOPROTOOPT;
    } else {
        soopt_from_kbuf(sopt, mtod(m, void *), m->m_len);
    }
    m_freem(m);          /* <-- the fix: always free the GETOPT mbuf */
    break;
```

This is a single logical change (free the GETOPT mbuf). It additionally closes a latent NULL-deref on the original error path (the old code set `m = NULL` then dereferenced `mtod(m,...)` at :120 when `m_len == 0` — e.g. `SO_SCO_HANDLE` with no link), but that is incidental to fixing the leak correctly.

## Fix validation (single-fix module build + reload)

Because netbt is a module, the fix was validated by rebuilding **only `netbt.ko`** (`cd /usr/src/sys/netbt && make`, rc=0, `-Werror`) and loading the patched module (sha256 `b63c5280…`) — the GENERIC kernel proper is unchanged (`#0`). Same workload on the patched module:

```
=== PATCHED MODULE LEAK TEST ===
BEFORE(patched): 7/146632 mbufs in use (current/max):
iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0
AFTER 10k(patched): 7/146632 mbufs in use (current/max):
iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0
AFTER 20k(patched): 7/146632 mbufs in use (current/max):
=== PATCHED CONFIRMATION (50k calls) ===
BEFORE: 8/146632 mbufs in use (current/max):
iterations=50000 getsockopt_ok=50000 getsockopt_fail=0 last_mtu=0
AFTER 50k: 7/146632 mbufs in use (current/max):
=== functionality: getsockopt still returns valid data ===
getsockopt OK, len=2, mtu=0
```

**Before/after contrast:** baseline leaks **+10000 mbufs / 10k calls**; patched leaks **0** mbufs over 70k calls. `getsockopt` still functions (returns len=2, valid MTU). `fix_status = fixed`.

## PoC changes

- Added `#include <netbt/sco.h>` (defines `SO_SCO_MTU`) — the original PoC used `SO_SCO_MTU` without including the header that defines it.
- Added `stdlib.h` (for `atol`) and made the iteration count a CLI argument (default 50000) so the leak can be measured precisely against `netstat -m` rather than requiring a separate `watch` terminal.
- The PoC prints `iterations/getsockopt_ok/getsockopt_fail/last_mtu` so the leak rate is unambiguous.
