# DF-5753 — VERDICT

**Status: reproduced (NULL-pointer kernel write in netisr context, race
window deterministic under load).**
**Impact: panic (write-capable race; UAF extension documented).**
**Confidence: certain.**

## Root cause

`hci_intr()`'s SCO-completion dispatch (sys/netbt/hci_unit.c:387-406):

```c
	IF_DEQUEUE(&unit->hci_scodone, m);
	if (m != NULL) {
		struct hci_link *link;
		lockmgr(&unit->hci_devlock, LK_RELEASE);      /* <-- unlocked */

		TAILQ_FOREACH(link, &unit->hci_links, hl_next) {
			if (link == M_GETCTX(m, struct hci_link *)) {
				hci_sco_complete(link, 1);        /* <-- no ref, no lock */
				break;
			}
		}
```

The mbufs on `hci_scodone` carry the SCO link pointer as context
(`M_SETCTX(m, pcb->sp_link)`, sco_upper.c:310). The membership walk is
the only liveness "check", and it runs with no lock and no reference —
it is a TOCTOU against *syscall-context* teardown:

`sco_sabort()` (socket close/abort, unprivileged owner of the socket,
sco_socket.c:253) → `sco_disconnect(pcb, 0)` (sco_upper.c:213-217):

```c
		sco->hl_sco = NULL;          /* pcb unhooked first */
		pcb->sp_link = NULL;
		hci_link_free(sco, err);     /* ... then ~40 lines before the
		                                TAILQ_REMOVE + kfree */
```

`hci_link_free()` (hci_link.c:931-1024) does a large amount of work
(l2cap_active_list walk, request/PDU teardown, ACL-anchor close, memo
kmalloc) *before* the final `TAILQ_REMOVE` + `kfree` — the link stays on
`unit->hci_links` with `hl_sco == NULL` for that whole window. Any
scodone mbuf the netisr thread dispatches in the window matches the
still-listed link and executes `hci_sco_complete()`:

```c
hci_sco_complete(struct hci_link *link, int num)   /* hci_link.c:884 */
{
	link->hl_sco->sp_pending--;                     /* hl_sco == NULL ! */
	(*link->hl_sco->sp_proto->complete)(link->hl_sco->sp_upper, num);
```

## Reproduction

The rig (production `sco_attach`/`hci_link_alloc`/`sco_send`/
`hci_complete_sco`/`sco_disconnect` verbatim; the fake controller
completes TX instantly the way ubt's USB completion does) runs 1500
rounds of [200 sp_mtu-sized sends → 200 scodone entries →
`sco_disconnect(pcb, 0)`]. The crash landed within the first rounds:

```
Fatal trap 12: page fault while in kernel mode
cpuid = 0
fault virtual address	= 0x1c
fault code		= supervisor write data, page not present
current process		= Idle          <-- netisr thread
Stopped at      hci_sco_complete+0x7:   subl   $0x1,0x1c(%rax)
```

`0x1c` is exactly `offsetof(struct sco_pcb, sp_pending)`; `rax == NULL`
proves `hl_sco` was read as NULL while the link was still on the list —
only possible inside the `sco_disconnect` window above (an in-list link
cannot yet have been freed or reused), so the race attribution is exact.

Note: the baseline run.log also shows one DF-5752 rig marker
("mid-drain re-injection") — the harness of that build had the DF-5752
re-inject enabled by default; it is unrelated to the SCO path and the
faulting instruction is decisively `hci_sco_complete`.

## UAF extension

The same dispatch window extends past `hci_link_free()`'s `kfree`
(hci_link.c:1024): a scodone mbuf dequeued a few microseconds later walks
a list whose entries were freed, and (if the freed chunk is reused as a
new `hci_link`, same M_BLUETOOTH zone) falsely matches a *fresh* link and
credits the wrong connection — or matches a link mid-setup whose
`hl_sco` is still NULL (same NULL write). The write primitive
(`*(hl_sco + 0x1c) -= 1`, then a call through `hl_sco->sp_proto`) is
write-capable memory corruption whenever `hl_sco` can be made to hold
reused memory.

## Threat

Any local user who owns an SCO socket (bluetooth audio path) racing
`close(2)`/abort against in-flight TX completions; no privilege needed
beyond the socket. Requires a controller driver on netbt (as with the
whole netbt family). This is the completion-dispatch counterpart of
DF-5737 (event-context teardown vs syscall TX) — same missing-lock
family, opposite direction, different file and sink.

## Fix validation

`fix.diff`: (a) the scodone walk + `hci_sco_complete()` run under
`hci_devlock` (re-acquired around the dispatch in hci_intr);
(b) `sco_disconnect()` performs the `hl_sco = NULL` + `hci_link_free()`
teardown under the same lock (unit pointer saved **before** the free —
the first fix iteration released the lock through the freed link and
crashed in `lockmgr_release`; that was a rig-visible fix bug, corrected
in the shipped diff).

Post-fix: `hcius_fire=1500` — all 1500 rounds, ~28k completions through
the locked dispatch, **no panic** (fix_run.log), guest up.

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