# DF-0586 — Lockless global `hci_pcb` list UAF race

## Verdict

**LATENT BUG, CODE-CONFIRMED — race path unreachable from an unprivileged
user on this guest (no Bluetooth controller); not runtime-demonstrable
here; fix.diff source-validated (applies, compiles, links, boots, adds the
missing lock symbol).**

The lockless-walk-vs-free pattern the finding describes is real and is
exactly where the finding says it is (`sys/netbt/hci_socket.c:87, 576-577,
655-657, 935-1011`). But the *only* walker of `hci_pcb` is `hci_mtap`,
which is reachable solely from the BT-radio input path
(`sys/netbt/hci_unit.c:348,364,381,494,515,528`) and from the
`hci_send`→`hci_output_cmd` path (`sys/netbt/hci_socket.c:533`). Both
require a populated `hci_unit_list`, which is only populated by
`hci_attach` (`sys/netbt/hci_unit.c:83-118`) when a real Bluetooth device
driver probes a controller. The audit guest has no BT controller and no
loadable BT driver module, so the unit list stays empty, `hci_send` returns
`ENETDOWN` at `sys/netbt/hci_socket.c:505` before ever reaching
`hci_output_cmd`, and `hci_mtap` is never invoked.

`PF_BLUETOOTH`/`BTPROTO_HCI` sockets themselves ARE reachable from the
unprivileged `maxx` user once an admin `kldload`s `netbt.ko` (verified:
`socket(33, SOCK_RAW=3, BTPROTO_HCI=1)` succeeds for `maxx` and calls
`hci_sattach`/`hci_sdetach` for `socket()`/`close()` — but those just
insert/remove a pcb into the never-walked list, so no race fires).

A kernel-module harness (`df0586_harness.c`) was written to call
`hci_mtap()` directly from a kernel thread, simulating the missing BT-radio
input path. With a userspace driver (`poc_race.c`) hammering
`socket(PF_BLUETOOTH,SOCK_RAW,BTPROTO_HCI)`/`close()` to drive the real
`hci_sattach`/`hci_sdetach`, the walker performed **>3.3 billion
`hci_mtap()` calls** concurrently with **>4 million socket open/close
cycles** — and the race did not fire. The reason is structural, not
statistical: the INVARIANTS-ON default GENERIC slab allocator
(`sys/kern/kern_slaballoc.c:1559-1571`) only poisons the first
`sizeof(weirdary)` = 64 bytes of a freed chunk with `WEIRD_ADDR`
(`0xdeadc0de`), but `struct hci_pcb`'s `LIST_NEXT` linkage
(`hp_next.le_next`) lives at offset **88** — outside the poison zone. So a
walker reading `pcb->hp_next.le_next` from a concurrently-freed chunk still
sees the pre-free linkage value (a valid pointer or NULL), and silently
walks past it. The race's *read* of the freed `pcb` is real (it is a UAF
read on `hp_flags`, `hp_laddr`, `hp_pfilter`, `hp_efilter`, which ARE in
the first 64 bytes), but it doesn't fault because those reads observe
either the pre-free data, the transient `0xdeadc0de` poison (which the
filter logic happens to treat as "skip this pcb"), or the post-realloc
zeroes — none of which cause a fault. For the walker to fault, the freed
chunk's `hp_next` would need to be overwritten with garbage, which requires
*cross-object slab reuse* (attacker-controlled content) — the heap-grooming
escalation variant the finding itself flags as the worst-case path but
which is out of reach of a pure stress reproducer on this guest.

## Mechanism (the bug is real)

1. The list head is a bare `LIST_HEAD` with no lock initializer —
   `sys/netbt/hci_socket.c:87`.
2. `hci_sattach` (insert path) guards `LIST_INSERT_HEAD` only with
   `crit_enter()`/`crit_exit()` at `sys/netbt/hci_socket.c:655-657`. On
   DragonFly `crit_enter()` blocks local-CPU preemption only — it provides
   NO cross-CPU exclusion.
3. `hci_sdetach` (remove path) has NO protection at all — bare
   `LIST_REMOVE(pcb, hp_next); kfree(pcb, M_PCB);` at
   `sys/netbt/hci_socket.c:576-577`.
4. `hci_mtap` (the only walker) uses bare `LIST_FOREACH(pcb, &hci_pcb,
   hp_next)` at `sys/netbt/hci_socket.c:935` and dereferences
   `pcb->hp_flags`, `pcb->hp_laddr`, `pcb->hp_efilter`, `pcb->hp_pfilter`,
   `&pcb->hp_socket->so_rcv.sb`, then calls `sbappendaddr`/`sorwakeup` on
   the freed/reused `hp_socket` at `sys/netbt/hci_socket.c:1004-1006`.
5. The only lock in the entire `sys/netbt/` tree is `unit->hci_devlock`
   (`sys/netbt/hci_unit.c:102`), a per-unit device-queue lock. There is
   no pcb-list lock. (`grep -rn 'lockmgr.*hci_pcb\|hci_pcb_lock' sys/netbt`
   returns zero hits pre-fix.)

So on a host that DOES have a Bluetooth controller (or an attacker who can
heap-groom the `kmalloc-128` slab to cause cross-object reuse of a freed
`hci_pcb`), the unlocked walk-vs-free is a genuine UAF race, exactly as
the finding describes. The finding's severity (Medium, CVSS
`AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H`) is appropriate.

## Exploit chain

This is a TOCTOU/UAF primitive, not a deterministic write. To convert to
`uid=0` an attacker would need to:

1. Groom the `kmalloc-128` slab so that the `kfree(pcb, M_PCB)` in
   `hci_sdetach` is immediately followed by an attacker-controlled
   allocation of the same bucket that places forged content at the
   `hp_socket` offset (0) — overwriting the dangling `hp_socket` pointer
   with a pointer to a victim `struct socket`/`struct ucred`-bearing
   object (no SMAP, so userspace addresses work; no SMEP, so a userspace
   ops vector is executable from kernel context).
2. Time the `hci_mtap` walk so it reads the forged `hp_socket` and calls
   `sbappendaddr(&pcb->hp_socket->so_rcv.sb, ...)` / `sorwakeup(pcb->hp_socket)`
   on it.

This requires: (a) a populated `hci_unit_list` (a real or emulated BT
controller — absent on this guest), and (b) a heap-grooming spray of
`sizeof(struct hci_pcb)=104` (→ `kmalloc-128` bucket) with attacker-shaped
content landing on `hp_socket` at offset 0. The slab poison limitation
documented above ALSO blocks the natural panic, so it simultaneously
*helps* an attacker (the bug is silent on INVARIANTS-ON GENERIC, not
noisy) and *hurts* the reproducer (we can't easily demonstrate the fault
without the heap-grooming step).

**Outcome: NOT ACHIEVED on this guest.** The valid hard blocker is "the
vulnerable code path is unreachable at runtime on this guest AND no
harness that respects the realism rules can exercise it end-to-end from an
unprivileged user" — the `hci_unit_list` is empty (no BT controller, no
loadable virtual BT driver), so `hci_mtap` never runs from a real input
path, and the heap-grooming escalation requires that path to be live to
shape the dangling pointer. The kld harness can drive `hci_mtap` but cannot
realistically demonstrate the heap-groomed `uid=0` chain because step (1)
above would require either a real BT controller's packet input timing
(absent) or a circular `kldload`-based re-claimer (forbidden by the
bright-line rule for `uid=0` claims). This is a Medium-severity latent UAF
that needs BT HW (or an emulated BT device) plus patient heap grooming to
land — reported as `corruption` (latent UAF confirmed at source level),
not `uid=0`.

## PoC changes

- `poc_race.c` — original reviewer PoC had multiple errors that prevented
  compilation (`btr_enabled` doesn't exist — the field is `btr_flags &
  BTF_UP`; `SO_HCI_OMIT_XMIT` doesn't exist on DragonFly; `PF_BLUETOOTH`
  was hard-coded to 31 instead of 33; `SOCK_RAW` was hard-coded to 1
  instead of 3). Rewrote it as a *dense* driver: 4 "churner" threads each
  keep 64 HCI sockets open and continuously close+reopen one slot, plus 2
  "spammer" threads that open/close as fast as possible. This densely
  populates the global `hci_pcb` list and drives millions of
  `hci_sattach`/`hci_sdetach` cycles per second.
- `df0586_harness.c` — new kld module that calls `hci_mtap()` directly
  from a kernel thread on cpu1 with a synthesized `struct hci_unit` and
  mbuf. This stands in for the missing BT-radio input path
  (`sys/netbt/hci_unit.c:494`), since the guest has no controller and
  `hci_send` returns `ENETDOWN` before reaching `hci_output_cmd`. Has two
  modes: `mode=0` self-contained (walker + killer threads, killer bypasses
  the lock — for stress), `mode=1` walker-only (default; lets the
  userspace driver exercise the real `hci_sattach`/`hci_sdetach` and is
  compatible with the patched kernel's lock).
- `build.sh`, `run.sh` — exact reproducible build/run commands.
- `fix.diff` — a `git apply`-able minimal fix authored post-verification.
- Full `build.log`, `run.log`, `fix_build.log` saved.

## Fix validation (Phase 8)

`fix_status: not_testable` (with caveat). Concretely:

- **fix.diff applies cleanly** to `/usr/src`: `patch -p1 --forward` ⇒ all
  5 hunks succeeded (`Hunk #1..5 succeeded`).
- **fix.diff compiles cleanly**: `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
  ⇒ `NK_DONE rc=0` (full log in `fix_build.log`).
- **Patched kernel #1 boots**: `kern.version` reports
  `DragonFly 6.5-DEVELOPMENT #1: Wed Jul  8 16:14:16 UTC 2026`.
- **Patched netbt.ko has the new lock symbol**:
  `nm netbt.ko | grep hci_pcb_lock` ⇒
  `00000000000011e0 D hci_pcb_lock` (the unpatched module has no such
  symbol).
- **Runtime panic before/after comparison**: NOT FEASIBLE on this guest.
  The unlocked-walk-vs-free race doesn't reliably fault on
  INVARIANTS-ON GENERIC without a populated `hci_unit_list` (the slab
  poison zone stops at byte 64; `hp_next` lives at byte 88). The harness
  performed 3.3B+ walker cycles against 4M+ socket open/close cycles on
  the unpatched kernel with no panic, so there is no "before panic" marker
  to compare against. The fix is therefore validated at the
  source/compile/boot level (`applies`, `compiles`, `links`, `boots`,
  `adds the lock`) and traced line-by-line to close the cited code path
  (`LK_EXCLUSIVE` around `LIST_INSERT_HEAD` and `LIST_REMOVE`,
  `LK_SHARED` around the `LIST_FOREACH` body), but a runtime
  before/after panic comparison requires BT hardware this guest lacks.

## Recommended fix

The fix.diff in this folder adds a dedicated `struct lock hci_pcb_lock`
initialised at file scope and acquires it:
- `LK_EXCLUSIVE` around `hci_sattach`'s `LIST_INSERT_HEAD` (replacing the
  inadequate `crit_enter()`/`crit_exit()`),
- `LK_EXCLUSIVE` around `hci_sdetach`'s `LIST_REMOVE`,
- `LK_SHARED` around the entire `hci_mtap` `LIST_FOREACH` body.

This matches the finding's proposal (and the rest of netbt, which already
uses a `struct lock` for `unit->hci_devlock`). The finding's proposed diff
additionally wraps `hci_cmdwait_flush` in `hci_pcb_lock`, but that walk is
of `hci_unit_list`, not `hci_pcb` — a different (real) bug that needs its
own `hci_unit_list` lock. My fix.diff is tighter: it only addresses the
`hci_pcb`-list race that is DF-0586's scope. It **supersedes** the
finding's proposal by dropping the misplaced `hci_cmdwait_flush` hunk
while keeping the four essential `hci_pcb` hunks.

## Files in this folder

- `df0586_harness.c` — kld harness driving `hci_mtap` from a kernel
  thread (stands in for the missing BT-radio input path).
- `poc_race.c` — userspace dense driver (churner + spammer threads).
- `build.sh`, `run.sh` — exact build/run commands.
- `fix.diff` — git-apply-able minimal fix.
- `build.log`, `run.log`, `run.2.log`, `run.3.log` — full untrimmed logs.
- `fix_build.log` — full untrimmed `make nativekernel` output.
- `env.txt` — guest environment for the runs.
- `manifest.json` — artifact catalog.
