# DF-5737 — event-context link teardown races the syscall transmit path (no locking) → UAF write

**File:** `sys/netbt/hci_event.c` (pass 2) triggers; sink across `sys/netbt/hci_link.c` / `l2cap_lower.c`
**Severity:** Medium (speculative/likely — statically argued, not guest-verified)
**CWE-362 / CWE-416**

## Analysis (why this is a real race)

Two threads touch `struct hci_link` state concurrently with **no shared lock**
(netbt uses only `crit_enter()` around list ops, which does not serialize
against other threads on SMP, and the event path drops `hci_devlock` before
dispatch):

1. **Syscall thread (user context):** `send(2)` on an L2CAP socket →
   `l2cap_send()` → `l2cap_lower.c:204` `hci_acl_send(m, chan->lc_link, chan)`
   which walks/inserts on `link->hl_txq`
   (`sys/netbt/hci_link.c:585-586` `TAILQ_INSERT_TAIL`, `link->hl_txqlen += num`)
   and calls `hci_acl_start(link)`.
2. **NETISR thread (softint, mplock held):** event handlers in
   `sys/netbt/hci_event.c` tear links down:
   * `hci_event_command_status()` on a CREATE_CON status 0x12 frees **every**
     link in `HCI_LINK_WAIT_CONNECT` (`sys/netbt/hci_event.c:271-273`);
   * `hci_event_con_compl()` frees the looked-up link on error status
     (`sys/netbt/hci_event.c:559`);
   * `hci_event_discon_compl()` frees by handle (`sys/netbt/hci_event.c:629-631`);
   * `hci_cmd_reset()` (a forged RESET Command_Complete, accepted at any time
     — no correlation with an outstanding command) frees **all** links on the
     unit (`sys/netbt/hci_event.c:1063-1070`).

   `hci_link_free()` (`sys/netbt/hci_link.c:931-1025`) closes L2CAP channels
   (dropping their `hl_refcnt` references) and then `kfree()`s the link.

The reference scheme only protects links that **L2CAP channels hold
references to** (`hl_refcnt`). But the transmit path's `chan->lc_link` raw
pointer is dereferenced in `hci_acl_send()` *before* any refcount is taken
per-call: if the event thread frees the link between the socket thread's
`chan->lc_link` load and its `TAILQ_INSERT_TAIL(&link->hl_txq, ...)`, the
insert writes through the freed `struct hci_link` (~356-byte M_BLUETOOTH
allocation) — a use-after-free **write** with partly attacker-shaped content
(the `l2cap_pdu` list pointers). For links in `HCI_LINK_WAIT_CONNECT`
(channels are still attaching; `hl_refcnt` transitions in
`hci_acl_open()` at `sys/netbt/hci_link.c:143`) the window is widest: a
forged CREATE_CON status-0x12 event frees the link while the connecting
syscall is between `hci_send_cmd()` (`:109`) and `link->hl_refcnt++` (`:143`).

Note the command-completion path also has **no matching** of completions to
outstanding commands (`hci_event_command_compl()` dispatches purely on the
opcode in the event, `sys/netbt/hci_event.c:324-351`), so a forged RESET
completion is accepted in steady state — i.e. the trigger does not require
the controller to actually reset.

## Why untested on the guest

Proving the interleaving requires winning a narrow window between a syscall
thread and the NETISR thread on the 6-CPU guest with no instrumentation hook
in the production transmit path (the harness rig injects events but cannot
freeze a userland syscall mid-`hci_acl_send` without patching the code under
test, which would invalidate the "verbatim" property). A deterministic
demonstration would require a second KLD stubbing `hci_acl_send`'s internals
— that demonstrates the harness, not the race. Filed as speculative/likely
with the static chain above; the recommended fix (refcount the link across
the transmit call) removes the class regardless.

## Recommended fix

Take an explicit reference around the transmit call:

```c
 /* l2cap_lower.c */
 hci_link_ref(chan->lc_link);          /* new: +1 unless being freed */
 err = hci_acl_send(m, chan->lc_link, chan);
 hci_acl_close(chan->lc_link, err);
```

plus converting `hci_link_free()` calls in the event handlers to go through
`hci_acl_close()` (or otherwise respect `hl_refcnt`) so a referenced link is
deferred, and gating `hci_cmd_reset()`'s teardown on an outstanding RESET
command.

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

(No fix.diff for this item: documented recommendation only - see Fix above.)
