Skip to content

fix: make auto-reconnect watcher state main-only to close a data race#38

Merged
MegaManSec merged 1 commit into
mainfrom
fix/auto-reconnect-thread-safety
Jun 2, 2026
Merged

fix: make auto-reconnect watcher state main-only to close a data race#38
MegaManSec merged 1 commit into
mainfrom
fix/auto-reconnect-thread-safety

Conversation

@MegaManSec
Copy link
Copy Markdown
Owner

What & why

The auto-reconnect watcher's state (reconnectWatchlist, reconnectInFlight, intentionalReleases, reconnectTimer) is documented main-only, but the peripheral handoff reaches unregisterFromPC on the outgoing-connection queue: performHandoffToPeer runs inside an OutgoingConnection completion, which fires on com.magicswitch.outgoing, not main. So disarmReconnect / noteIntentionalRelease mutated those non-thread-safe Swift collections (and cancelled the main-queue timer) off-main, racing reconnectTick and handlePeripheralDisconnected.

This is a data race on the primary switch path. Beyond the collection corruption/trap risk, it can drop the intentional-release flag — so the watcher fights a just-completed handoff and tries to reclaim a peripheral that was handed to the peer.

Changes

  • Fix the race (blocking): armReconnect / disarmReconnect / noteIntentionalRelease now hop to main (mirrors the existing setConnectionState pattern). This also guarantees the intentional-release flag is set before handlePeripheralDisconnected reads it.
  • Stale-flag leak: clear the intentional-release flag when closeConnection() fails — no disconnect follows to consume it, so otherwise it would permanently suppress a later genuine drop's auto-reconnect.
  • Self-reconnect observer gap: when a probe finds the device already reconnected on its own, re-register a disconnect observer so its next drop re-arms the watcher (the genuine drop had unregistered it).
  • Sleep-path waste: skip the pre-sleep IOBluetooth scan when neither releaseOnSleep nor autoReconnect needs it, instead of blocking the held sleep transition to build an unused connectedBeforeSleep snapshot.

Testing

  • xcodebuild -configuration Debug build CODE_SIGNING_ALLOWED=NO -> BUILD SUCCEEDED
  • swift format clean
  • No test target exists in this project (per CLAUDE.md); changes verified via static analysis + build.

Not in scope (follow-ups)

Silent give-up notification after the 1h window; bounding bluetoothQueue.sync against an in-flight pair at sleep; distinguishing OP_FAILED from transport errors in HOLDS_ONE; multi-Mac (>2) reclaim using networkDevices.first.

The watcher's state (reconnectWatchlist / reconnectInFlight /
intentionalReleases / reconnectTimer) is documented main-only, but the
peripheral handoff reaches `unregisterFromPC` on the outgoing-connection
queue: `performHandoffToPeer` runs inside an `OutgoingConnection` completion,
which fires on `com.magicswitch.outgoing`, not main. So `disarmReconnect` /
`noteIntentionalRelease` mutated those Swift collections (and cancelled the
main-queue timer) off-main, racing `reconnectTick` and
`handlePeripheralDisconnected` — a data race on non-thread-safe collections
that can lose the intentional-release flag (causing the watcher to fight a
handoff) or corrupt/trap. Hop `armReconnect` / `disarmReconnect` /
`noteIntentionalRelease` to main, matching the existing `setConnectionState`
pattern; this also guarantees the flag is set before the disconnect handler
reads it.

Also fixed alongside it:
- Clear the intentional-release flag when `closeConnection()` fails, so a
  release that never happened can't permanently suppress a later genuine
  drop's auto-reconnect.
- Register a disconnect observer when a probe finds the device already
  reconnected on its own, so its next drop re-arms the watcher (the genuine
  drop had unregistered the old observer).
- Skip the pre-sleep IOBluetooth scan when neither `releaseOnSleep` nor
  `autoReconnect` needs it, rather than block the held sleep transition to
  build a `connectedBeforeSleep` snapshot nothing will read.
@MegaManSec MegaManSec merged commit 26aee49 into main Jun 2, 2026
2 checks passed
@MegaManSec MegaManSec deleted the fix/auto-reconnect-thread-safety branch June 2, 2026 22:44
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

🎉 This PR is included in version 2.11.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant