# DF-3476 — RFCOMM DLC teardown deadlock: `ng_btsocket_rfcomm_untimeout()` under `pcb_lock` vs `process_timeout()` blocking on `pcb_lock` (synchronous `callout_stop`)

## Where

`sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c`

Lock-order inversion between the pcb lockmgr lock and the callout
stop-interlock:

- **Path A (timeout callback).** `ng_btsocket_rfcomm_process_timeout()`
  (:3550-3583) is the callout callback; its FIRST action is
  `lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE)` (:3554) — a *blocking*
  acquisition. While it waits, the callout is marked `CALLOUT_INPROG`
  (set by softclock before invoking the callback,
  `sys/kern/kern_timeout.c:567`, cleared only at :644 *after* the callback
  returns).

- **Path B (timeout teardown under pcb_lock).** Five call sites invoke
  `ng_btsocket_rfcomm_untimeout(pcb)` → `callout_stop(&pcb->timo)`
  (:3538) **while holding pcb->pcb_lock**:
  - `ng_btsocket_rfcomm_pcb_kill()`  :3403-3404 (RFCOMM task thread; also holds session_lock)
  - `ng_btsocket_rfcomm_disconnect()`:834-835 (user thread, close/shutdown)
  - `ng_btsocket_rfcomm_detach()`    :768-769 (user thread)
  - `ng_btsocket_rfcomm_receive_sabm()`:2112 (task thread, remote-driven)
  - `ng_btsocket_rfcomm_receive_ua()`  :2273 (task thread, remote-driven)

- **The interlock.** DragonFly's `callout_stop()` is *synchronous*
  (`_callout_cancel_or_stop(cc, CALLOUT_STOP, 1)`,
  `sys/kern/kern_timeout.c:1091-1094`): when the callback is INPROG it
  `ssleep("costp")`s (:910-921) until softclock clears the STOP request —
  which happens only after the callback returns (:643-647).

## Deadlock

Timer expiry racing any pcb_lock critical section that reaches an
untimeout call:

```
CPU X (softclock):                        CPU Y (task/user thread):
  INPROG = 1                                 lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE)  [held]
  process_timeout():                          ... pcb_kill()/disconnect()/... decides
  lockmgr(&pcb->pcb_lock) -> BLOCKS           TIMO flag still set (callback hasn't
  (waits for CPU Y)                           run yet, so it can't have cleared it)
                                            ng_btsocket_rfcomm_untimeout(pcb)
                                              callout_stop() -> sees INPROG
                                              ssleep("costp") -> BLOCKS
                                              (waits for callback to return)
```

Circular wait; both sleeps are non-interruptible (no PCATCH). Permanent
wedge of: the triggering thread (user close() hangs in D-state forever —
`lksleep(&pcb->state, ..., 0, "rf_det", 0)` at :785 is not catchable
either), the RFCOMM task thread (blocked on session_lock/pcb_lock — whole
bluetooth RFCOMM processing stops), and one CPU's softclock thread (its
callout wheel stops being serviced → timers on that CPU stop firing
system-wide symptoms).

## Trigger

Requires an RFCOMM DLC with an armed timer (default
`net.bluetooth.rfcomm.sockets.stream.timeout` = 60 s) and the timer firing
inside the µs-scale window in which a thread holds pcb_lock and reaches an
untimeout call. Two attacker-facing levers:

- **Remote** (Bluetooth peer in radio range): the peer controls frame
  timing; e.g. answer our SABM with UA exactly at timeout expiry
  (`receive_ua` :2273 takes pcb_lock then untimeouts), or send DISC to
  kill the DLC at expiry. Repeatable at will — each connect attempt arms a
  fresh 60 s timer.
- **Local**: close()/shutdown() of an RFCOMM socket timed against its
  DLC timer (`disconnect` :834-835).

The interleaving needs the timer to fire while the other side already holds
pcb_lock (a window of the order of the critical-section length), so single
attempts are low-probability; sustained attempts (many parallel DLCs, or a
remote peer replaying UA bursts around the 60 s mark) make it a realistic
persistent-wedge attack on any Bluetooth-equipped system running RFCOMM.

## PoC

Not executable on the audit guest: reproducing needs an established L2CAP
session (real HCI hardware / `ubt`-class device); the guest has none, and
`ng_btsocket_rfcomm_connect()` fails at L2CAP connect before any DLC timer
is armed. status=untested for that reason; the mechanism above is proven
from source (path:line) with the exact sleep/wake conditions quoted from
`kern_timeout.c`.

## Fix

Drop the pcb_lock across the untimeout, or take the callout interlock
BEFORE pcb_lock consistently. Minimal safe change at each site: move
`ng_btsocket_rfcomm_untimeout()` outside the pcb_lock critical section is
racy against process_timeout's flag updates; the robust fix is to make
`ng_btsocket_rfcomm_process_timeout()` NOT block on pcb_lock — e.g. have it
delegate to the RFCOMM task (which already serializes DLC processing)
instead of taking pcb_lock itself:

```diff
 static void
 ng_btsocket_rfcomm_process_timeout(void *xpcb)
 {
 	ng_btsocket_rfcomm_pcb_p pcb = xpcb;
 
-	lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE);
+	/*
+	 * Do NOT block on pcb_lock here: untimeout() callers hold it and
+	 * callout_stop() waits for this callback to return -> AB-BA wedge.
+ 	 * Setting the flag without the lock is safe: flags are u_int16_t,
+ 	 * the TIMO/TIMEDOUT pair is only mutated while the callout is
+ 	 * stopped or firing, and the task re-checks under pcb_lock.
+ 	 */
+	if (!(pcb->flags & NG_BTSOCKET_RFCOMM_DLC_TIMO))
+		return;			/* untimeout won the race */
 	pcb->flags &= ~NG_BTSOCKET_RFCOMM_DLC_TIMO;
 	pcb->flags |= NG_BTSOCKET_RFCOMM_DLC_TIMEDOUT;
 	... (state switch moved to the task under pcb_lock)
```

(full diff in fix.diff; state-machine part of the callback is relocated
into `ng_btsocket_rfcomm_session_process_pcb()` which already holds
pcb_lock when it checks TIMEDOUT at :1805/:1817/:1854).
