# DF-5740 — VERDICT

**Finding**: `sys/netbt/hci_socket.c` mutates socket buffers from the
NETISR thread with no synchronization, and hands the TX-completion
callback (`hci_drop`) a raw socket pointer with no reference.

Three coupled defects, one root cause family — *hci_socket.c touches
sockets from whatever CPU/netisr port processes controller traffic,
while every socket op (send/close) is serialized on a different port
(netisr_cpuport(0), uipc_socket.c:259)*:

1. **RX side (demonstrated panic)** — `hci_mtap()`
   (hci_socket.c:1003-1010) calls `sbappendaddr(&so->so_rcv.sb, ...)`
   + `sorwakeup()` from `btintr` on the queueing CPU's netisr port,
   holding neither `ssb_lock` nor `ssb_token`.  A user thread in
   `soreceive()` (uipc_socket.c:1320) holds `ssb_token`+`ssb_lock`,
   which do not exclude the netisr appender.  Torn interleavings
   desynchronize `ssb_mb` from `ssb_cc`.  **Reproduced**: INVARIANTS
   kernel panicked in 24 s with `panic: receive 1`
   (uipc_socket.c:1348: `KASSERT(m != NULL || !ssb_cc)`) —
   `soreceive()+0xd1f` via `sys_recvfrom`, cpu4, while cpu3 ran the
   completion flood (`panic.txt`).  On production kernels the same
   interleave silently corrupts (see 3).

2. **TX side, live socket** — `hci_send()` appends the "precious"
   record with `sbappendrecord` on cpu0's port (hci_socket.c:522) while
   `hci_drop()` drops it with `sbdroprecord`/`sowwakeup`
   (hci_socket.c:437-438) from the completion cpu's port
   (hci_unit.c:505-507).  Neither takes the sockbuf token;
   `sbdroprecord`'s own contract ("must be called while holding a
   critical section", uipc_sockbuf.c:510-512) is violated cross-CPU.
   `race5.log` shows the damage accumulating as non-EWOULDBLOCK send
   failures (96 by iter 875) on the raced `so_snd`.

3. **TX side, dying socket (UAF)** — the completion context is stored
   as a raw pointer in the mbuf (`M_SETCTX(m, so)`, hci_socket.c:523)
   with **no reference**.  `hci_sdetach()` tries to defuse it with
   `hci_cmdwait_flush()` (hci_socket.c:446-465), but that sweep only
   sees mbufs still *in* `unit->hci_cmdwait`: any command mbuf already
   dequeued by the concurrent completion path (hci_event.c:282/354 →
   `hci_output_cmd` hci_unit.c:490-509, which reads the ctx at :505
   *after* a long `hci_mtap` walk over every listening socket) is
   invisible to it.  After the sweep, detach sets `so_pcb = NULL`,
   `sofree()` (hci_socket.c:573-574) drops the last reference →
   `ssb_release`→`sbflush` frees the pending so_snd records and
   `sodealloc` frees the socket (uipc_socket.c:380-437).  The
   completion cpu then executes `hci_drop(freed_so)` →
   `sbdroprecord(&freed->so_snd.sb)` + `m_freem()` of the already
   freed record chain (double free) + `sowwakeup(freed)`, or — when
   the objcache slot was already recycled into the next socket the
   trigger opens — steals and frees an *unrelated live socket's*
   records.  I could not capture this exact interleaving in a
   stand-alone panic: on non-reused slots `sbflush` has already NULLed
   `sb_mb`, so the stale drop is a silent no-op; the observable forms
   (record theft desyncs, double frees, the cpu0 wedge of race2/4/5)
   are all present in the logs.  Mechanism verified line-by-line; the
   code holds no reference and the flush sweep is TOCTOU by
   construction.

**Honest confound**: race1/2's page-fault panic (`hci_input_event+0x42`,
NULL m) was enabled by DF-5742's unbounded tap exhausting the mbuf pool
under my non-draining taps; it is a rig artifact (m_gethdr NULL +
no-op KKASSERT in the kmod + `_m_copyback2(NULL)` returning success)
and is NOT claimed as this finding's proof.  The proof is the
`panic: receive 1` run (race6) whose taps drained.

**Reproduced**: yes (panic + corruption + wedge under race load).
**Impact**: kernel memory corruption (sockbuf list/accounting
desync, mbuf double-free, cross-socket record theft, UAF write into
freed/reallocated `struct socket`); panic on INVARIANTS kernels, hard
hangs (cpu wedge) otherwise.  Not driven to uid=0: the corrupted
objects are mbuf chains and socket slots; a full chain would need
objcache grooming of the recycled socket slot and control over the
stolen record's reuse — plausible but not completed in this run.
**Threat model**: unprivileged local user on any machine with a
Bluetooth controller (raw HCI sockets are unprivileged; the attacker
supplies both the tap/`recvfrom` side and the command traffic; with
DF-5741 every socket is "privileged" anyway).

**Fix validated**: `fix.diff` (sockbuf tokens around all four mutation
sites, ctx-owned socket reference via `soreference`/`sofree`, and a
`hci_devlock`-guarded single-consumer consume of the ctx shared by
`hci_output_cmd` and `hci_cmdwait_flush`).  Applied to the guest's
`/usr/src`, rig rebuilt (build.sh copies the patched files verbatim),
identical race re-run: 425+ iterations / 85k+ commands queued / 0 send
failures / no panic / no wedge vs baseline panic at ~100-150 iterations
(`fix_run.log`), then stopped manually.  Baseline vs patched recorded.

**Key kernel refs**
- sys/netbt/hci_socket.c:1003-1010 (mtap append, no token)
- sys/netbt/hci_socket.c:433-439→fix (drop, no token/no ref)
- sys/netbt/hci_socket.c:446-465 (flush TOCTOU)
- sys/netbt/hci_socket.c:522-523 (append + M_SETCTX, no ref)
- sys/netbt/hci_socket.c:567-579 (detach: flush, so_pcb=NULL, sofree)
- sys/netbt/hci_unit.c:494-509 (output_cmd: mtap window, ctx read)
- sys/netbt/hci_event.c:281-284,353-356 (cmdwait drain, netisr port)
- sys/kern/uipc_socket.c:259 (so_port = netisr_cpuport(0))
- sys/kern/uipc_socket.c:1348 (the KASSERT that fired)
- sys/kern/uipc_sockbuf.c:510-512 (sbdroprecord critical-section contract)

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