Skip to content

CoreBluetooth: subscribe() never resolves when the peripheral refuses the CCCD write #471

Description

@yonaka15

Platform: macOS, CoreBluetooth backend. Seen on btleplug 0.12.0; the same logic is on master (f0ac6b7).

What happens: when the peripheral refuses the CCCD write that subscribe() triggers — for example with ATT error 0x0F, Insufficient Encryption — subscribe().await never returns.

Whysrc/corebluetooth/central_delegate.rs, peripheral:didUpdateNotificationStateForCharacteristic:error:. CoreBluetooth reports the refusal through this callback, with error set and isNotifying left unchanged (false). The delegate routes on isNotifying() alone, so the refusal becomes CharacteristicUnsubscribed. on_characteristic_unsubscribed then pops unsubscribe_future_state, which is empty, and the subscribe future waiting in subscribe_future_state is never resolved. On master the error is now carried in the event, but it still takes the isNotifying() == false branch into the unsubscribe queue, so the hang is the same.

Observed: a raw CoreBluetooth replay against the same peripheral gets didUpdateNotificationStateFor … isNotifying=false error=CBATTErrorDomain 15 "Encryption is insufficient." 27–210 ms after setNotifyValue(true), and bluetoothd logs Received error response for write value 15. Through btleplug, subscribe() does not return; we only noticed because we wrap every call in a timeout.

A fix we are running — two commits on top of 0.12.0 in a fork:

  • yonaka15/btleplug@95eccc0 — carry the error on both events. With an error present, the isNotifying state points the opposite way (a refused subscribe leaves it false), so route with is_notifying != failed, and resolve the future with CoreBluetoothReply::Err, which subscribe() already maps to Error::RuntimeError.
  • yonaka15/btleplug@943344a — a refusal for a characteristic already in the requested state lands on an empty queue. In that case, answer the request still pending in the other queue instead of dropping it.

Happy to open a PR against master if this direction looks right.

Same shape, not observed: peripheral:didWriteValueForCharacteristic:error: sends CharacteristicWritten only when error.is_none(), so a refused write-with-response looks like it would leave write() pending too.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions