# DF-5754 — VERDICT

**Status: reproduced (use-after-free through the freed unit's hci_devlock,
netisr/detach race).**
**Impact: panic (UAF read/write on the unit object).**
**Confidence: certain** (race window widened by flood; the faulting
instruction and object are exact).

## Root cause

`hci_detach()` (sys/netbt/hci_unit.c:120-130):

```c
	crit_enter();
	hci_disable(unit);
	TAILQ_REMOVE(&hci_unit_list, unit, hci_next);
	crit_exit();
	kfree(unit, M_BLUETOOTH);
```

There is **no reference counting on `struct hci_unit`** anywhere in
netbt, and the `crit_enter()` only defers interrupts on the detaching
CPU — it does not stop the netisr thread (possibly another CPU) that is

* walking `hci_unit_list` in `btintr()` (bt_input.c:37,
  `TAILQ_FOREACH(unit, &hci_unit_list, hci_next) hci_intr(unit);`), and
* inside `hci_intr(unit)` (hci_unit.c:329-411), whose `goto another`
  loop re-acquires `lockmgr(&unit->hci_devlock, LK_EXCLUSIVE)` on every
  packet — i.e. writes to the unit object it may be executing past its
  lifetime.

When `hci_detach()` runs under rx load (controller detaching while
traffic is in flight — USB hot-unplug or driver module unload), the unit
is freed underneath the walker: the next `lockmgr()` operates on the
freed (and reusable) chunk. The same lifetime hole covers driver rx in
flight (`hci_input_event(unit, m)` taking the same devlock) and every
`hci_unit_lookup()` consumer (hci_unit.c:265-279 returns a raw pointer
that sockets/upper layers retain for the socket lifetime:
hci_socket.c:503, l2cap_upper.c:161, sco_upper.c:139, hci_ioctl.c:151).

## Reproduction

Rig: 300 rounds of [`hci_attach()` (production) → flood-inject
hci_input_event() from a kthread → `hci_detach()` (production)]. The
baseline (continuous flood, inquiry-16 payload) died in round 0/1:

```
Fatal trap 12: page fault while in kernel mode
cpuid = 0
fault virtual address	= 0x90
fault code		= supervisor read data, page not present
current process		= Idle            (kernel/netisr thread)
Stopped at      lockmgr_exclusive+0x5d:   orl   (%r14),%esi
```

`lockmgr_exclusive` reading through `r14` (the lock pointer loaded from
the freed unit) faulting at 0x90 = a garbage lock address out of freed
memory — the `lockmgr(&unit->hci_devlock, ...)` in `hci_intr`/
`hci_input_event` on a unit freed by `hci_detach`. This is distinct from
DF-0683 (hci_disable mutating the queues/link list without devlock):
the fault here is on the *unit object's lock itself*, which only
`hci_detach`'s `kfree` can have freed.

## Fix validation

`fix.diff`:

* `struct hci_unit` gains `hci_refcnt`; a global `hci_unit_lock`
  spinlock guards the list and the counts.
* `btintr()` snapshots the (referenced) unit list under the spinlock and
  drops the references after `hci_intr()`.
* `hci_detach()` removes the unit under the spinlock and waits the
  reference count out before `kfree()`. (The controller driver remains
  responsible for quiescing its own rx callbacks before detach — same
  contract as before, now documented; the rig's fake driver honours it
  by stopping and draining its injector in `disable`.)

Post-fix: `hciud_fire=300` (filler-packet flood keeps the netisr walker
busy during every detach) — **all 300 rounds survived, guest up**
(fix_run.log). One earlier fix-check iteration also ran 100+ rounds
clean of the unit-lifetime signature before tripping the *separate,
already-known* DF-0683 (hci_disable's unlocked drain racing hci_intr's
queue processing — different function, different mechanism, queue/memo
corruption rather than the unit UAF); the shipped rig mode 1 avoids
allocating memos during the race to keep the two findings separable,
and the final 300-round run completed with no incident at all.

## Rig-evolution note (honesty)

The rig's driver-quiesce emulation initially had a TOCTOU (enable-flag
check before the inflight increment) that let one injector call slip
past `fake_disable()`'s drain — a *rig* bug producing the same
lockmgr_exclusive signature during fix-check; fixed by incrementing
`g_d_inflight` before checking the flag. The baseline panic (above) was
captured before that change and reflects the genuinely unprotected
kernel (flood continuous through detach).

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