# DF-0659 — VERDICT

## Verdict: REPRODUCED at source level; live trigger requires Bluetooth HW + race

The flag-clearing bug at
`sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:223-238` is **real and
confirmed by source trace.** It cannot be live-triggered on this guest
because (a) the Bluetooth code is `optional netgraph7_bluetooth_l2cap`
and is not built into the default kernel or any module on the running
guest, and (b) even when built, it requires Bluetooth hardware to drive
the L2CAP state machine and a tight race window on the auto-disconnect
timer.

## Mechanism (cited line-by-line)

1. **`sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:201-217`
   `ng_l2cap_discon_timeout()`.** Sets the
   `NG_L2CAP_CON_AUTO_DISCON_TIMO` flag at line 210, then schedules the
   callout. KASSERTs at line 204 that the flag was clear before; i.e.
   **this function must never be called with the flag already set**.
2. **`:223-238` `ng_l2cap_discon_untimeout()`.**
   ```c
   226: if (!(con->flags & NG_L2CAP_CON_AUTO_DISCON_TIMO))
   227:     panic(...);                        /* correct: flag must be set */
   232: if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0)
   233:     return (ETIMEDOUT);                /* BUG: flag still set */
   235: con->flags &= ~NG_L2CAP_CON_AUTO_DISCON_TIMO;
   236: return (0);
   ```
   If `ng_uncallout` returns 0 (callout already fired/running), the flag
   is **NOT cleared**.
3. **`sys/netgraph7/netgraph/ng_base.c:3273-3296` `ng_uncallout()`.**
   Returns `callout_stop(c)` value. `callout_stop` returns 0 when the
   callout has already fired or is currently running — a real, common
   condition (the timer fired just as we tried to cancel it).
4. **`:149-164` `ng_l2cap_con_ref()`.** Calls `discon_untimeout(con)`
   **without checking the return value** — so even on `ETIMEDOUT`, the
   caller proceeds with the flag still set. `refcnt` is now > 0.
5. **`:170-194` `ng_l2cap_con_unref()`.** When `refcnt` drops to 0, the
   conditions at line 188-192 are met (OPEN state, OUTGOING, discon_timo
   > 0, not DYING) and it calls `discon_timeout(con)`.
6. **`:202-208` `discon_timeout()` panics.** Line 204 KASSERTs the flag
   is NOT set: `if (con->flags & (LP_TIMO|AUTO_DISCON_TIMO)) panic(...)`.
   But the flag IS set (step 2 didn't clear it). **Panic.**

The race window: `con_ref` is called within a netgraph item processing
between (a) the auto-disconnect timer firing (which sets up an async
callout dispatch) and (b) the dispatched `ng_l2cap_process_discon_timeout`
running. An attacker sending an L2CAP signaling command (e.g. Echo
Request) timed to arrive in this window triggers the panic.

## Why we cannot trigger it on this guest

```
$ ls /boot/kernel/ | grep -iE 'ng_l2cap|bluetooth|hci|ubt'
(empty — no Bluetooth modules built)

$ grep ng_l2cap_misc /home/maxx/dfbsd/dfbsd/sys/conf/files
netgraph7/bluetooth/l2cap/ng_l2cap_misc.c   optional netgraph7_bluetooth_l2cap
```

The Bluetooth L2CAP code is **not built** on the default kernel or any
loadable module. To exercise it, an admin would need to enable
`options netgraph7_bluetooth` + `options netgraph7_bluetooth_l2cap` in
the kernel config and rebuild. Even then, the panic requires actual
Bluetooth hardware to drive an outgoing L2CAP connection through the
auto-disconnect-timer window — there is no Bluetooth HW on the audit
guest.

## Privilege / threat model

- **Attacker position:** unauthenticated Bluetooth peer on an existing
  outgoing L2CAP connection from the victim (adjacent network, `AV:A`).
- **Preconditions:** admin has built a Bluetooth-capable kernel; victim
  has an outgoing OPEN L2CAP connection to the attacker; all
  channels/commands released (refcnt=0); auto-disconnect timer armed
  (default 5 sec).
- **Trigger:** race-timed L2CAP signaling command from the attacker.
- **Impact:** kernel panic = full system DoS.

This is **not** unpriv→root. It is a remote-adjacent DoS of systems
that have actually built and are using Bluetooth L2CAP — a tiny set.

## Recommended fix

Clear the flag **unconditionally** before checking `ng_uncallout`'s
return value, so the panic in `discon_timeout` cannot fire even when
the callout has already fired. The proposed diff in the finding markdown
is correct; `findings/poc/DF-0659/fix.diff` carries a verified,
git-apply-able version.
