# DF-5764 — VERDICT

**Status: reproduced** (KLD rig, production code compiled verbatim; fix validated)
**Impact: dos (connection-level TX blackhole + graceful-disconnect disabled)**
**Confidence: certain** (defect proven deterministically through shipped code paths; fix discriminates baseline vs patched)

## Root cause (path:line)

`sys/netbt/l2cap_lower.c:195-204` — `l2cap_start()`:

```c
IF_DEQUEUE(&chan->lc_txq, m);           /* :195  m leaves the channel queue */
KKASSERT(chan->lc_link != NULL);        /* :197 */
KKASSERT(m != NULL);                    /* :198 */
chan->lc_pending++;                     /* :203  raised BEFORE the fallible call */
return hci_acl_send(m, chan->lc_link, chan);   /* :204  ENETDOWN / ENOMEM */
```

`hci_acl_send()` failure returns free `m` and create no `l2cap_pdu`:

- ENETDOWN — `link->hl_state == HCI_LINK_CLOSED` → `m_freem(m); return ENETDOWN;`
  (sys/netbt/hci_link.c:546-549)
- ENOMEM — `zalloc(l2cap_pdu_pool)` NULL (hci_link.c:551-553) or `m_split()`
  NULL (hci_link.c:566-570) → `nomem:` frees everything (hci_link.c:592-599)

`lc_pending` is only decremented in `hci_acl_complete()` for a PDU found on
`link->hl_txq` with `lp_chan == chan` (hci_link.c:730-742). After a failed
send no such PDU exists, so the counter is permanently inflated — and every
subsequent `l2cap_start()` invocation (from `l2cap_send()` when
`lc_pending == 0` never fires; but also from `hci_acl_complete()`/`hci_acl_start()`
streaming callers) that dequeues another SDU increments it again: **compounding**.

All three consumers of `lc_pending` gate on `== 0`:

| consumer | gate | consequence when permanently > 0 |
|---|---|---|
| `l2cap_send()` l2cap_upper.c:398 | `if (chan->lc_pending == 0) return l2cap_start(chan);` | every further `send(2)` silently succeeds into `lc_txq`; nothing ever transmitted (W2: 64 SDUs, ret 0 each) |
| `l2cap_disconnect(chan, linger)` l2cap_upper.c:244 | `(IF_QEMPTY(&chan->lc_txq) && chan->lc_pending == 0) \|\| linger == 0` | graceful disconnect handshake never starts; channel state stays L2CAP_OPEN (W3) |
| `l2cap_start()` shutdown flush l2cap_lower.c:173-174 | `L2CAP_SHUTDOWN && chan->lc_pending == 0` | flush path unreachable (W4) |

## Reproduction (baseline)

Rig: `l2pw_harness.ko` — VERBATIM production `l2cap_upper.c`,
`l2cap_lower.c`, `l2cap_misc.c`, `l2cap_signal.c`, **`hci_link.c`** (the
failure branch under test is production code), minimal stubs for the
transport/socket layers (md5s recorded in `setup.log`). Channel state built
with production calls: `l2cap_attach()` + `l2cap_cid_alloc()` + one
`hci_acl_open()`-style reference + `L2CAP_OPEN` (post-config state,
l2cap_signal.c:605) on a link in `HCI_LINK_CLOSED` (what `hci_link_alloc()`
returns, hci_link.c:909 — and the state the DF-5737 teardown race window
leaves behind; ENOMEM under pressure hits the same imbalance).

Observed (run.log, decisive lines):

```
l2pw: W1 send#1 ret=50 (ENETDOWN=50) lc_pending=1 link_txq_empty=1 lc_txq_len=0
l2pw: W1 PASS: pending==1 but NO pdu exists ...
l2pw: W2 64 more sends: ret==0 all, lc_txq_len=64 (blackholed) lc_pending=1
l2pw: W3 l2cap_disconnect(linger) ret=0 state=5 (OPEN=5) reqs_empty=1 SHUTDOWN=1
l2pw: W4 l2cap_start ret=50 lc_txq_len=63 lc_pending=2 state=5   <- compounding
l2pw: WEDGE ROUND PROVEN (assertion failures: 0)
l2pw: CONTROL ROUND OK (assertion failures: 0)                    <- healthy accounting works
```

`hw.l2pw_wedge_proven = 1`, `hw.l2pw_ctl_ok = 1`, no panic. The CONTROL
round (link OPEN + production `hci_acl_complete(link,1)`) shows
`lc_pending` 1→0 round-trip works and the SHUTDOWN disconnect fires at 0 —
proving the wedge is caused precisely by the failed-send increment, not by
rig artifacts.

## Fix validation

`fix.diff` applied to the guest's `/usr/src/sys/netbt/l2cap_lower.c`
(increment only on success), module rebuilt from the re-copied verbatim
source, exact same PoC re-run (fix_run.log):

| assertion | baseline (unfixed) | patched |
|---|---|---|
| W1 lc_pending after failed send | 1 (phantom) | **0** |
| W2 lc_txq after 64 more sends | 64 blackholed, ret 0 | **0 queued, honest ENETDOWN per send** |
| W3 disconnect(linger) | state OPEN, no request | **state CLOSED (0) — handshake path reachable** |
| W4 l2cap_start retry | pending 1→2 | **no-op** |
| `hw.l2pw_wedge_proven` | 1 | **0** |
| `hw.l2pw_ctl_ok` (regression) | 1 | **1** (C1 pending==1 backed by real PDU; C3 1→0 round-trip intact) |

fix_status: **fixed**.

## Why not higher severity

- No memory corruption: the counter is a uint8_t bookkeeping field; nothing
  dereferences through it.
- Stuck mbufs are bounded: `l2cap_ssend()` (l2cap_socket.c:450-451) appends
  the original mbuf to `so_snd` (flow-controlled by `sbspace`) and only the
  copy goes down; the wedged copies sit in `lc_txq` (freed at detach,
  l2cap_upper.c:275 IF_DRAIN).
- Trigger requires an established (OPEN) channel plus one `hci_acl_send()`
  failure: ENOMEM under memory pressure, or ENETDOWN in the link-teardown
  window (co-fires with DF-5737's race — on a racing-teardown box the wedge
  is one of the outcomes of the same window).
- Recovery exists: `close(2)` → detach → force-close reclaims everything.

Scope note: the ENETDOWN-via-teardown-race *window* itself is DF-5737's
(owned there). This finding is the error-path accounting defect in
l2cap_lower.c that makes any failure permanent for the channel — filed
narrowly on the ENOMEM/all-failures root cause at l2cap_lower.c:203-204.

## What was checked and found clean in this file (pass-2 negatives)

- `l2cap_recv_frame` 4-byte `m_copydata` (l2cap_lower.c:115): the sole caller
  `hci_acl_recv` guarantees ≥ 4 payload bytes (START short-frame drop,
  hci_link.c:476-481; fragments only append). DF `m_copydata`
  (sys/kern/uipc_mbuf.c:1671-1696) is bounds-checked with always-on KASSERTs
  anyway; no OOB on this path.
- `l2cap_recv_frame` length check (:124): dead but harmless — `want == 0`
  exactness (hci_link.c:509-517) means only exact frames are delivered;
  the overshoot case stalls in reassembly (owned by DF-0566).
- Dispatch ordering (:127-142): SIGNAL_CID handoff guarded by
  l2cap_recv_signal's own loop guards (l2cap_signal.c:73-83) and internal
  `left` arithmetic in the CONFIG handlers (l2cap_signal.c:424-431, 632-641);
  data-CID dispatch requires `lc_link == link && lc_state == L2CAP_OPEN`
  with globally-unique lcids (l2cap_cid_alloc, l2cap_misc.c:207-236). Sound.
- `l2cap_close` ordering (:96-98): setting `lc_state = L2CAP_CLOSED` BEFORE
  `hci_acl_close()` makes any re-entrant `hci_link_free()` channel walk
  (hci_link.c:944-951) a no-op for this channel — single-threaded safe; no
  non-race path reaches :78/:86 with `lc_link == NULL` (all `lc_state != CLOSED`
  transitions assign `lc_link` first: l2cap_upper.c:171/178/190,
  l2cap_signal.c:289/302).
- No single-threaded re-entrant `hci_link_free` via `l2cap_close`: every
  `hci_link_free` entry with `hl_refcnt > 0` (discon_compl hci_event.c:631,
  con_compl failure hci_event.c:559, RESET completion hci_event.c:1068) runs
  with link state OPEN/WAIT_*, so the inner `hci_acl_close` arms the expiry
  callout instead of recursing. The concurrent-callout variant is DF-5739's.
- `lc_pending` uint8_t overflow: in-flight PDUs per channel ≤ 2-3 by
  construction (streaming prefetch only); unreachable.
- The racy `:69` state read and the unlocked `lc_link` lifetime are owned by
  DF-5749 / DF-5737 respectively (not re-reported).

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