Skip to content

fix(ble): bound the flow-control credit wait so a wedged peer can't hang Write#161

Merged
xunholy merged 1 commit into
mainfrom
fix/ble-credit-stall-timeout
Jun 29, 2026
Merged

fix(ble): bound the flow-control credit wait so a wedged peer can't hang Write#161
xunholy merged 1 commit into
mainfrom
fix/ble-credit-stall-timeout

Conversation

@xunholy

@xunholy xunholy commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

A BLE Write can hang forever on flow-control credit (category C, robustness). Writes split into chunks, each waiting until the firmware advertises enough credit via FlowCtrl notifications. consumeCredit parked in creditCond.Wait() with no deadline — so if the firmware stops publishing credit (a wedged board or a silently-dropped link), the wait never returns and Write blocks forever. Only Close could unblock it; the caller's intent (and any turn-level cancellation) was ignored, because Write has no ctx (io.Writer).

Fix

consumeCredit now bounds the wait with bleCreditStallTimeout (15s). It still wakes immediately on a credit refresh or Close via creditCond, but a deadline caps the total wait — exceeding it returns a stall error instead of hanging. 15s is conservative: a healthy board refreshes credit at its sub-second BT scheduling tick, so it never false-positives on a busy-but-live board; it fires only when the peer is genuinely gone. Overridable via the creditStallTimeout field so tests exercise the stall path without a 15s wait.

Found via the internal audit sweep (final backlog item).

Verification

  • TestConsumeCredit_StallTimesOut: no credit ever arrives → stall error within the timeout. Proven to HANG (2s test deadline) on the old unbounded code.
  • TestConsumeCredit_CloseWakesWaiter: Close while parked → os.ErrClosed promptly.
  • TestConsumeCredit_CreditArrivesSucceeds: a fresh credit notification lets the waiter proceed and deduct.
  • task ci green (lint 0 / vet / build / test:full 0 fail / govulncheck clean); task eval 17/17.

Robustness fix — anchors v0.779.0; closes the audit-sweep backlog.

…ang Write

BLE Writes split into chunks, each waiting until the firmware has advertised
enough flow-control credit (FlowCtrl notifications). consumeCredit parked in
creditCond.Wait() with NO deadline: if the firmware stops publishing credit —
a wedged board or a silently-dropped link — the wait never returns and Write
blocks forever. Only Close could unblock it; the caller's intent (and any
turn-level cancellation) was ignored because Write has no ctx (io.Writer).

consumeCredit now bounds the wait with bleCreditStallTimeout (15s). It still
wakes immediately on a credit refresh or Close via creditCond, but a deadline
caps the total wait: exceeding it returns a stall error instead of hanging. 15s
is conservative — a healthy board refreshes credit at its sub-second BT
scheduling tick, so this never false-positives on a busy-but-live board; it
only fires when the peer is genuinely gone. The timeout is overridable via the
creditStallTimeout field so tests exercise the stall path without a 15s wait.

Found via the internal audit sweep (final backlog item).

Tests:
- TestConsumeCredit_StallTimesOut: no credit ever arrives -> returns a stall
  error within the (test-shortened) timeout. Proven to HANG (2s test deadline)
  on the old unbounded code.
- TestConsumeCredit_CloseWakesWaiter: Close while parked -> os.ErrClosed
  promptly (not after the stall timeout).
- TestConsumeCredit_CreditArrivesSucceeds: a fresh credit notification lets the
  waiter proceed and deduct.

Verified: task ci green (lint 0 / vet / build / test:full 0 fail / govulncheck
clean); task eval 17/17.
@xunholy xunholy merged commit 1786fb4 into main Jun 29, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant