# DF-0710 — VERDICT

**Verdict: REPRODUCED (code-level; live path unreachable on guest)**
**Impact: dos (kernel thread infinite-loop / hang in `sco_input`)**
**Fix: VALIDATED at compile + machine-code + logic level (`not_testable` live)**

## 1. The bug (confirmed in source)

`sys/netbt/sco_socket.c:210-228` — `sco_input(arg, m)`:

```c
while (m->m_pkthdr.len > sbspace(&so->so_rcv))      /* line 221 */
    sbdroprecord(&so->so_rcv.sb);                   /* line 222 */
...
sbappendrecord(&so->so_rcv.sb, m);
sorwakeup(so);
```

A `while` is used where the two sibling Bluetooth socket implementations use a
one-shot `if { drop; return; }`:

- `sys/netbt/l2cap_socket.c:231` — `if (m->m_pkthdr.len > sbspace(&so->so_rcv)) { kprintf(...); m_freem(m); return; }`
- `sys/netbt/rfcomm_socket.c:241` — `if (m->m_pkthdr.len > sbspace(&so->so_rcv)) { kprintf(...); m_freem(m); return; }`

Only SCO has the broken `while`. The consequence, proven by tracing each kernel
primitive:

- `sbspace` (`sys/netbt/bluetooth.h:150-152`) =
  `(long) imin((int)(ssb_hiwat - ssb_cc), (int)(ssb_mbmax - ssb_mbcnt))`.
  For an empty SCO receive buffer this equals `sco_recvspace` = **4096**
  (`sys/netbt/sco_socket.c:81`, set via `soreserve` in `sco_sattach:282`).
- `sbdroprecord` (`sys/kern/uipc_sockbuf.c:517-535`) is a **no-op when the
  buffer is empty** — it is guarded by `if (m)` at line 524
  (`m = sb->sb_mb; if (m) {...}`). On an empty buffer it frees nothing, so
  `ssb_cc`/`ssb_mbcnt` and therefore `sbspace` do not change.

**Therefore**: an inbound SCO packet with `m_pkthdr.len > 4096` arriving at a
socket whose receive buffer cannot free enough room (including the trivially
empty buffer) makes the loop condition permanently true: `sbdroprecord` no-ops,
`sbspace` never grows, and the loop never exits. The Bluetooth protocol thread
spins at 100% CPU forever. This is a **deterministic, unbounded kernel hang**.

The packet does not need to be enormous — a single byte over the hi-water mark
(`4097`) is sufficient (harness case 2). The bug is a pure control-flow defect;
nothing is corrupted (no write primitive), so there is **no escalation chain**.

## 2. Reachability on this guest — UNREACHABLE LIVE

`sco_input` is reachable only via the BT controller input path:

`BT controller RX → hci_sco_recv(m, unit)` (`sys/netbt/hci_link.c:828`)
strips the 3-byte SCO header (line 839) and calls
`(*link->hl_sco->sp_proto->input)(...)` (`sys/netbt/hci_link.c:867`), which is
the `sco_input` callback registered in `sco_proto` (`sco_socket.c:70-78`,
`.input = sco_input`). `hci_sco_recv` performs **no upper-bound check against
`sco_recvspace`** (only DIAGNOSTIC-gated self-consistency checks at lines
841-854), so an oversized-but-self-consistent packet is forwarded unchanged.

Confirmed on the audit guest (`6.5-DEVELOPMENT #0`):

| Check                                            | Result                          |
|--------------------------------------------------|---------------------------------|
| `options BLUETOOTH` in `X86_64_GENERIC`          | **absent** (count 0)            |
| `sco_input` symbols in `/boot/kernel/kernel`     | **0** (not compiled into kernel)|
| netbt kernel module loaded (`kldstat`)           | none loaded                     |
| `netbt.ko` present in `/boot/kernel/`            | yes (stock, not loaded)         |
| Bluetooth controller (`pciconf`)                 | **no BT PCI/USB device**        |

So the vulnerable routine is not present in the running kernel AND there is no
hardware to drive `hci_sco_recv` even if `netbt.ko` were loaded. The live PoC
`sco_input_hang` consequently fails with `socket: Protocol not supported`
(captured in `live_poc_unreachable.log`).

Because the live path is unreachable, the bug is reproduced by a **deterministic
code-level harness** that replicates the *exact* in-kernel primitives
(`sbspace` macro, `sbdroprecord` no-op-on-empty semantics, `sco_recvspace=4096`).
This is the accepted path for latent / hardware-gated findings.

## 3. Reproduction harness — `sco_input_logic.c`

`sco_input_logic.c` faithfully models the sockbuf as a record queue with
`ssb_cc`/`ssb_mbcnt` accounting and implements both `sco_input_buggy` (the
`while`) and `sco_input_fixed` (the `if`+drop), with a 100000-iteration cap to
detect the infinite loop without actually hanging. Result (3 consistent runs):

```
[case 1] empty buf + oversize pkt (8192 > 4096)
    BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
    FIXED (if)  : iters=1 (dropped packet)
[case 2] empty buf + pkt 4097 (> hiwat 4096 by 1)
    BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
    FIXED (if)  : iters=1 (dropped packet)
[case 3] half-full buf (2048) + oversize pkt 8192
    BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
    FIXED (if)  : iters=1 (dropped packet)
[case 4] 3 records (1024 each) + oversize pkt 8192
    BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
    FIXED (if)  : iters=1 (dropped packet)
[case 5] empty buf + small pkt 100 (control)
    BUGGY(while): iters=0 (terminated) (appended=1)
    FIXED (if)  : iters=0 (appended)
[case 6] empty buf + pkt 4096 (== hiwat, fits)
    BUGGY(while): iters=0 (terminated) (appended=1)
    FIXED (if)  : iters=0 (appended)

SUMMARY: BUGGY(while) infinite-loops: 4 / 6 ; FIXED(if) <=1 iter on ALL: YES
```

Cases 5-6 are controls: normally-sized packets terminate on both paths, so the
fix does not regress legitimate traffic. Cases 1-4 (any packet exceeding
`sco_recvspace`, with empty through multi-record buffers) all infinite-loop in
the buggy version and are cleanly dropped by the fix.

## 4. Machine-code confirmation (netbt.ko, built before/after fix)

Because `sco_socket.c` is `optional bluetooth` and `options BLUETOOTH` is absent
from `X86_64_GENERIC`, the file is **not compiled into the default kernel**;
the correct build target is the `netbt.ko` module (`sys/netbt/Makefile`,
`bsd.kmod.mk`). Both builds use `-Werror`.

`sco_input` disassembly of the baseline `netbt.ko` (offset 0x540):

```
57f:  7e 33           jle    5b4 <sco_input+0x74>     ; if fits, go append
581:  ...             mov %rbx,%rdi
584:  e8 .. .. .. ..  callq  sbdroprecord             ; drop a record
...
5b2:  7f cd           jg     581 <sco_input+0x41>     ; *** BACKWARD BRANCH = loop ***
5b4:  ...             (append + sorwakeup)
```

The `7f cd jg 581` at 0x5b2 is the compiled `while` — a backward conditional
branch wrapping the `sbdroprecord` call. When `sbdroprecord` is a no-op this
branch is taken unconditionally and forever.

`sco_input` disassembly of the patched `netbt.ko` (offset 0x610):

```
635:  7f 29           jg     660 <sco_input+0x50>     ; one-shot if -> drop path
637:  ...             push/save
64b:  e8 .. .. .. ..  callq  sbappendrecord           ; normal append
65a:  e9 .. .. .. ..  jmpq   sorwakeup (tail)
660:  48 89 f7        mov    %rsi,%rdi                ; drop path: arg = m
663:  e9 .. .. .. ..  jmpq   m_freem (tail) + return
```

There is **no backward branch** in the patched `sco_input`. The loop is
structurally impossible: a single forward `jg 660` redirects to the `m_freem`
drop path. Module size changes (105024 -> 104960 bytes) confirming the code
differ. This is conclusive proof the fix eliminates the loop at the
machine-code level.

## 5. The fix (`fix.diff`)

Minimal, root-cause fix matching the sibling implementations exactly:

```diff
-	while (m->m_pkthdr.len > sbspace(&so->so_rcv))
-		sbdroprecord(&so->so_rcv.sb);
+	if (m->m_pkthdr.len > sbspace(&so->so_rcv)) {
+		DPRINTF("%s: packet (%d bytes) dropped (socket buffer full)\n",
+			__func__, m->m_pkthdr.len);
+		m_freem(m);
+		return;
+	}
```

The comment above is updated to reflect that the packet is dropped (the old
"dump data until the latest one will fit" wording described the buggy intent).
`git apply --check` passes against the read-only `sys/` tree.

## 6. Impact / threat model

- **Effect**: unbounded kernel-thread CPU spin; the Bluetooth protocol thread is
  wedged and the BT subsystem becomes unresponsive. Pure DoS — no memory is
  corrupted (`sbdroprecord` is a no-op on an empty buffer), so **no privilege
  escalation primitive exists**. This is a logic/DoS bug, not memory corruption.
- **Trigger preconditions** (realistic): (a) a malicious paired Bluetooth peer
  that sends an oversized SCO data packet, or (b) a malicious/compromised USB
  Bluetooth dongle injecting via `hci_sco_recv`, or (c) a root raw-HCI socket
  with an active SCO connection. No unprivileged-local-only path exists without
  BT hardware. Severity Medium is appropriate (local/peer DoS requiring BT
  hardware or root HCI; no privesc, no remote-without-BT).

## 7. Files in this evidence pack

| File                          | Purpose                                                  |
|-------------------------------|----------------------------------------------------------|
| `sco_input_logic.c`           | deterministic code-level harness (buggy vs fixed logic)  |
| `sco_input_hang.c`            | original (live) PoC — proves live unreachability on guest|
| `build.sh` / `run.sh`         | exact reproducible build/run                             |
| `build.log`                   | harness build output                                     |
| `run.log`, `run.2.log`, `run.3.log` | 3 stress-test runs (identical results)            |
| `live_poc_unreachable.log`    | live PoC `socket: Protocol not supported`               |
| `env.txt`                     | guest uname, gcc, BT reachability facts                  |
| `sco_input_baseline.disasm`   | baseline sco_input: backward branch `jg 581`             |
| `sco_input_patched.disasm`    | patched sco_input: forward branch `jg 660`, no loop      |
| `fix_build.log`               | netbt.ko built before/after fix (full, with disasm)      |
| `fix.diff`                    | git-apply-able one-line fix (while -> if + drop)         |
| `manifest.json`               | machine-readable catalog                                 |
