# DF-0724 — VERDICT

## Verdict: NOT REPRODUCED (race path unreachable on this guest; code-level race confirmed by source tracing)

## Mechanism (code-level analysis)

The finding describes a genuine unsynchronized race in `rfcomm_dlc_close`
(`sys/netbt/rfcomm_dlc.c:151-183`). The race is real at the code level,
confirmed by tracing the callout subsystem and the close path:

### The race scenario

1. **T1 (network input path):** A BT peer sends a DISC/DM/UA frame.
   `rfcomm_session.c` handlers (lines 784, 819, 876, 911) call
   `rfcomm_dlc_close(dlc, err)`. T1 enters `rfcomm_dlc_close`:
   - Passes `KKASSERT(dlc->rd_state != RFCOMM_DLC_CLOSED)` at line 156
     (state is non-CLOSED).
   - Begins clearing credit history (lines 160-162).
   - Calls `callout_stop(&dlc->rd_timeout)` at line 164.

2. **T2 (softclock timer):** The `rd_timeout` callout fires on another CPU.
   `rfcomm_dlc_timeout` (`rfcomm_dlc.c:195`) enters:
   - `crit_enter()` (line 199) — blocks interrupts on T2's CPU only;
     does NOT serialize T1 on a different CPU.
   - Reads `dlc->rd_state != RFCOMM_DLC_CLOSED` (line 201) — TRUE
     (T1 hasn't set CLOSED yet).
   - Calls `rfcomm_dlc_close(dlc, ETIMEDOUT)` (line 202).
   - Inside `rfcomm_dlc_close`: passes KKASSERT, clears credits,
     `callout_stop` (self — returns immediately per `kern_timeout.c:915-918`),
     `LIST_REMOVE` (line 166), `rd_session = NULL` (line 167),
     `rd_state = RFCOMM_DLC_CLOSED` (line 168), disconnected callback
     (line 170), session-expiry scheduling (lines 177-183).
   - Returns to `rfcomm_dlc_timeout`: `crit_exit()`, returns. Callout
     handler completes.

3. **T1 resumes:** `callout_stop` (sync=1) was blocking in `ssleep`
   (`kern_timeout.c:910-921`) waiting for the callout handler to complete.
   Now that T2 is done, T1's `callout_stop` returns. T1 continues:
   - `LIST_REMOVE(dlc, rd_next)` (line 166) — **DOUBLE LIST_REMOVE**
     (T2 already removed dlc from the list).
   - `dlc->rd_session = NULL` (line 167) — already NULL (no-op write).
   - `dlc->rd_state = RFCOMM_DLC_CLOSED` (line 168) — already CLOSED.
   - `(*dlc->rd_proto->disconnected)()` (line 170) — **DOUBLE callback**.
   - Session-expiry scheduling (lines 177-183) — **DOUBLE scheduling**.

### Why callout_stop blocks (confirmed)

`callout_stop` (`kern_timeout.c:1091`) calls
`_callout_cancel_or_stop(cc, CALLOUT_STOP, 1)` with `sync=1`. When the
callout is INPROG (handler executing on another CPU) and `sync=1`, the
function blocks in `ssleep(c, &c->spin, 0, "costp", 0)` at line 920,
waiting for the handler to clear the STOP flag. This confirms the
finding's claim that T1's `callout_stop` blocks while T2 completes the
full close.

### Why the KKASSERT doesn't catch it

The `KKASSERT(dlc->rd_state != RFCOMM_DLC_CLOSED)` at line 156 is a
non-atomic read. Both T1 and T2 read `rd_state` before either writes
`CLOSED` (line 168). The assertion passes for both callers. Only after
T2 writes CLOSED (line 168) does the state change, but T1 has already
passed the assertion and is blocked in `callout_stop`.

### Why crit_enter doesn't help

`rfcomm_dlc_timeout` (line 199) uses `crit_enter()`, which blocks
interrupts and preemption on the **current CPU only** (T2's CPU). It
does not prevent T1 (running on a different CPU) from entering
`rfcomm_dlc_close` concurrently. None of the network-input callers
(`rfcomm_session.c`) take `crit_enter()`.

## Why it does NOT reproduce on this guest

The race path is **genuinely unreachable** on the QEMU guest:

1. **`netbt.ko` is not loaded by default.** It is a loadable module
   (not compiled into `X86_64_GENERIC`). Loading requires root
   (`kldload netbt`). An unprivileged user cannot load it.

2. **No Bluetooth adapter / HCI unit exists.** The QEMU guest has no
   BT hardware. There are no `/dev/bt*` or `/dev/ubt*` device nodes,
   no `ng_ubt` module, and `sysctl net.bluetooth.hci.unit_list` returns
   "unknown oid". Without an HCI unit, the L2CAP layer has no link to
   any peer.

3. **`connect()` fails with EHOSTUNREACH.** Without an HCI unit, an
   RFCOMM socket can be created (`socket()` succeeds) and bound
   (`bind()` succeeds), but `connect()` fails with errno 65
   (EHOSTUNREACH). No RFCOMM session is established, no DLC is
   created, and no `rd_timeout` callout is ever armed. The
   `rfcomm_dlc_close` race path is never entered.

4. **Even with BT hardware**, triggering the race would require a
   remote BT peer to send a DISC/DM/UA frame at the exact
   timer-expiry window (20s ± race window). The window is extremely
   narrow.

## Impact assessment

- **Practical impact today: nil.** The current socket consumer
  `rfcomm_disconnected` (`rfcomm_socket.c:166-176`) is idempotent:
  it sets `so->so_error = err` and calls `soisdisconnected(so)`,
  both of which are safe to call twice. The double `LIST_REMOVE`
  writes the same values (no corruption in practice unless list
  mutations occur between T2's remove and T1's resume). The double
  session-expiry scheduling just re-arms the callout (no double-free).
- **Theoretical risk:** For a non-idempotent upper layer consumer,
  the double `disconnected` callback and double `LIST_REMOVE` could
  cause list corruption or use-after-free. The finding correctly rates
  this as Low severity — it is a genuine code-quality/hardening defect
  with no demonstrated security impact on the current socket consumer.

## Escalation assessment (Phase 6)

This is a **race condition** that could theoretically cause memory
corruption (double LIST_REMOVE → list corruption). However:

- The race is **not reachable** on this guest (no BT hardware).
- Even if reachable, the corruption is in a linked-list structure
  (`rs_dlcs`), not a slab object with attacker-controlled content.
- The corrupted field (`rd_next.le_prev` / `le_next`) is not a
  function pointer, refcount, or credential pointer — it's a list
  linkage. Exploitation would require shaping the list to place a
  victim object adjacent, which is not feasible through the BT socket
  interface.
- **No escalation path exists** from this primitive on this guest.
  This is a valid hard stop: the primitive is not reachable, and even
  if it were, the corrupted field is not directly exploitable for
  privilege escalation.

## PoC changes

The PoC folder did not exist (no prior PoC). I created:
- `rfcomm_race_test.c` — trigger attempt: creates an RFCOMM socket,
  binds, and tries to connect to a nonexistent BT peer. Documents
  that the race path is unreachable (connect fails EHOSTUNREACH).
- `fix.diff` — adds `atomic_cmpset_short` CAS loop at the top of
  `rfcomm_dlc_close` to atomically claim the CLOSED transition.
- `build.sh`, `run.sh` — repro scripts.
- `VERDICT.md`, `README.md`, `manifest.json` — evidence pack.

## Fix validation

- **fix.diff applies cleanly:** `patch -p1` succeeded (hunks at 145, 185).
- **fix.diff compiles:** `make` in `sys/netbt/` produced `netbt.ko` with
  `rc=0` and `-Werror` (no warnings, no errors).
- **Fixed module loads:** `kldload /boot/kernel/netbt.ko` succeeded
  after installing the fixed module and rebooting.
- **Disassembly confirms fix:** `objdump -d` shows `lock cmpxchg %cx,0xc(%rdi)`
  (the `atomic_cmpset_short` on `rd_state` at offset 0xc) at the top of
  `rfcomm_dlc_close`.
- **No regression:** The trigger PoC runs identically on the fixed module
  (socket OK, bind OK, connect EHOSTUNREACH — same as unpatched, because
  the race path is unreachable regardless of the fix).
- **fix_status: not_testable** — the PoC cannot exercise the race path on
  this guest (no BT hardware), so a behavioral before/after comparison
  is not possible. The fix is validated to apply + compile + load +
  not regress, and the code path is traced to confirm the CAS closes
  the race.

## Fix description

The fix adds an `atomic_cmpset_short` CAS loop at the top of
`rfcomm_dlc_close` that atomically transitions `rd_state` from any
non-CLOSED value to `RFCOMM_DLC_CLOSED`. If the state is already
CLOSED (another caller won the race), the function returns immediately.
This ensures exactly one caller performs the teardown (LIST_REMOVE,
disconnected callback, session-expiry). The KKASSERT is preserved but
now asserts on the pre-CAS `old_state` value. The explicit
`dlc->rd_state = RFCOMM_DLC_CLOSED` assignment (old line 168) is
removed because the CAS already set it.

This fix **supersedes** any finding-proposal fix (no proposal was present
in the DB for this finding).
