# DF-0688 — sco_ctloutput PRCO_SETOPT unconditional NULL-deref

## Verdict: REPRODUCED (panic, unprivileged→kernel DoS)

## Mechanism (sys/netbt/sco_socket.c:123-135)

In `sco_ctloutput()` for the `PRCO_SETOPT` case:

```c
case PRCO_SETOPT:
    m = m_get(M_WAITOK, MT_DATA);                        /* line 124: m->m_len == 0 */
    err = soopt_to_kbuf(sopt, mtod(m,void*), m->m_len, m->m_len);  /* copies 0 bytes */
    if (m->m_len == 0) {                                  /* line 127: ALWAYS TRUE  */
        m_freem(m);
        m = NULL;
        err = EIO;
        /* no `break` here — falls through */
    }
    err = sco_setopt(pcb, sopt->sopt_name, mtod(m, uint8_t *));  /* line 133: mtod(NULL) */
    m_freem(m);
    break;
```

`m_get(M_WAITOK, MT_DATA)` always yields `m->m_len == 0` (uipc_mbuf.c:1040).
`sopt_to_kbuf(... , m->m_len, m->m_len)` therefore copies 0 bytes. The
`if (m->m_len == 0)` branch always fires, sets `m = NULL; err = EIO`, but —
because the reviewer omitted `break` — execution falls through to line 133
where `mtod(m, uint8_t *)` expands to `((uint8_t *)((m)->m_data))`, i.e.
`((uint8_t *)((NULL)->m_data))`. That reads `offsetof(struct mbuf, m_data)`
== `0x10` from NULL, which on this guest traps as a page fault.

The result is **unconditional**: ANY `setsockopt` on a SCO socket panics,
regardless of optname or optval.

## Trigger (unprivileged, after admin Bluetooth setup)

```c
int s = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
int v = 1;
setsockopt(s, BTPROTO_SCO, 0, &v, sizeof(v));   /* unconditional panic */
```

**Precondition**: `netbt.ko` must be loaded. This is the realistic
admin-setup case (an admin deploying Bluetooth loads the module, e.g. via
`kldload netbt.ko` or `/boot/loader.conf`). Once loaded, the AF_BLUETOOTH
domain is registered and any unprivileged user can create the SCO socket
that triggers the panic. This is analogous to the ACCEPTABLE precondition
of an admin mounting/making-mountable a filesystem image.

## Evidence

`panic.txt` (excerpt from `dfbsd-qemu/boot.log`):

```
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x10            <- offsetof(struct mbuf, m_data)
Stopped at      sco_ctloutput.cold.2+0x19:      movq    0x10,%rax
db>
```

The `.cold.2` symbol is the GCC-outlined cold path of sco_ctloutput —
exactly the `m_len==0` branch that the missing `break` falls out of.

## Fix

`fix.diff` adds the missing `break;` in the `if (m->m_len == 0)` block:

```diff
@@ -128,6 +128,7 @@
 			m_freem(m);
 			m = NULL;
 			err = EIO;
+			break;			/* DF-0688: missing bail-out */
 		}
```

After the fix, `setsockopt` on a SCO socket returns `EIO` (matching the
apparent intent of the bail-out block — the mbuf-dance was already broken
in a way that could never deliver a valid option to `sco_setopt`).
Restoring actual option delivery would require a further rewrite (replace
the mbuf dance with a stack buffer as `l2cap_setopt2` does), but that is
a feature, not a security fix.

## Fix-validation

Built the patched `netbt.ko` against the read-only audit source
(`make -f Makefile` in `sys/netbt/`); installed to `/boot/kernel/netbt.ko`
(sha256 `0f2c251b...`); kept the kernel itself on the unpatched baseline
(`#0`); re-ran the PoC as `maxx`:

```
BEFORE: Fatal trap 12, fault va=0x10, Stopped at sco_ctloutput.cold.2+0x19
AFTER:  setsockopt returns EIO ("Input/output error"); guest stays up.
```

Three repeat runs are byte-identical (deterministic fix). See `fix_run.log`.

## Threat-model note

This is a local DoS that an unprivileged user can trigger after a
legitimate admin setup (`kldload netbt.ko` for Bluetooth). No primitive
beyond panic — the `m` pointer is NULL (not stale), so this is not a UAF
or arbitrary-write primitive. There is no escalation chain to develop.
