CAT via Bluetooyh is broken again - #786
Open
patrickrb wants to merge 2 commits into
Open
Conversation
… race) A background INTENT_ACTION_DISCONNECT (rig powered off / RFCOMM link dropped) runs disconnect() on another thread, which nulls the socket field. A CAT/TX worker already past the connected check in BluetoothSerialService.write() / BluetoothSerialSocket.write() then dereferenced the now-null socket (socket.write / socket.getOutputStream) -> NullPointerException. That NPE escaped BluetoothRigConnector.sendCommand's IOException-only catch and crashed the CAT worker on a background thread, leaving the link dead with no recovery — which surfaces as "connects, drops after ~10 s, no recovery" on Android 8 and 13/14. This is the check-then-act (TOCTOU) race the USB-serial path was already hardened against in CableSerialPort.writeIfOpen; the Bluetooth twin never received the analogous guard. Both write() layers now: - mark `socket` and `connected` volatile so connect / disconnect / read-loop / CAT-TX threads see consistent values under the Java memory model, and - snapshot the socket field ONCE into a local and route it through the new pure BluetoothSerialSocket.writeIfConnected(connected, sink, data) helper, which reports a torn-down link as the "not connected" IOException the caller already handles instead of NPEing. Added BluetoothSerialWriteTest (pure JVM, 4 cases) covering the guard, including the race case (connected==true but the snapshotted sink is null). Closes #781 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The functional fix is localized and covered by new unit tests; remaining feedback is limited to minor documentation/test determinism nits.
Pull request overview
This PR fixes a TOCTOU (check-then-act) race in the CAT-over-Bluetooth write path where a concurrent disconnect could null the socket between a connection check and the actual write, leading to an uncaught NullPointerException and a dead CAT worker thread.
Changes:
- Make the Bluetooth socket and connection state
volatileto improve cross-thread visibility. - Snapshot the socket once in
write()and route the write through a newBluetoothSerialSocket.writeIfConnected(...)helper to convert the race into a recoverableIOException("not connected"). - Add a pure-JVM unit test suite covering the race case, disconnected case, success case, and IOException propagation.
File summaries
| File | Description |
|---|---|
| ft8af/app/src/main/java/com/k1af/ft8af/bluetooth/BluetoothSerialSocket.java | Adds volatile state + socket snapshotting and a shared writeIfConnected guard to prevent NPEs during concurrent disconnects. |
| ft8af/app/src/main/java/com/k1af/ft8af/bluetooth/BluetoothSerialService.java | Snapshots the service socket once and uses the shared guard to avoid check-then-act races in the service layer. |
| ft8af/app/src/test/java/com/k1af/ft8af/bluetooth/BluetoothSerialWriteTest.java | Adds pure-JVM tests that validate the guard behavior and the exact race condition. |
Review details
Suppressed comments (1)
ft8af/app/src/main/java/com/k1af/ft8af/bluetooth/BluetoothSerialSocket.java:129
writeIfConnectedJavadoc says disconnect runs beforeconnectedwas cleared, butconnectedisn't cleared indisconnect(); it is cleared later in the read-loop exception path. Updating the wording avoids implying an ordering guarantee that doesn't exist.
* call. When the link is down ({@code !connected}) or the snapshot was
* already null (disconnect() ran first, before {@code connected} was cleared),
* throw the {@code "not connected"} {@link IOException} the CAT connector
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #781
What changed
Fixes a check-then-act (TOCTOU) race in the Bluetooth CAT write path that
crashed the CAT worker mid-QSO and left the link dead with no recovery.
Symptom (issue #781): on Android 8 and 13/14 the rig either fails to
connect at all, or connects and drops after ~10 s with no reconnect. Last
known good version is dev.1026. The USB-serial path was already hardened
against the analogous race (
CableSerialPort.writeIfOpen, PR #651), but theBluetooth twin never received the fix.
Root cause: a background
INTENT_ACTION_DISCONNECTbroadcast (rigpowered off, RFCOMM link dropped) runs
disconnect()on another thread,which nulls the
socketfield. A CAT/TX worker already past theconnectedcheck inBluetoothSerialService.write()/BluetoothSerialSocket.write()then dereferences the now-null socket(
socket.write/socket.getOutputStream) — NullPointerException. ThatNPE escapes
BluetoothRigConnector.sendCommand's IOException-only catchand kills the CAT worker with no recovery.
Fix (mirrors
CableSerialPort.writeIfOpen):socketandconnectedvolatilein bothBluetoothSerialSocketandBluetoothSerialService, soconnect / disconnect / read-loop / CAT-TX threads see consistent values
under the Java memory model.
write(), snapshot the volatile socket ONCE into a local, then routeit through a new pure helper
BluetoothSerialSocket.writeIfConnected(connected, sink, data). Aconcurrent
disconnect()that nulls the field after the snapshot can nolonger flip the write into an NPE — the helper reports the torn-down
link as the "not connected"
IOExceptionsendCommandalready handles.How to test
cd ft8af && ./gradlew testDebugUnitTest --tests com.k1af.ft8af.bluetooth.BluetoothSerialWriteTest(JDK 17 on macOS;
cmd.exe /c "gradlew.bat testDebugUnitTest --tests ..."on Windows).Four pure-JVM cases cover: the race case (
connected==truebutsnapshot -> sink is null must throw
IOException("not connected"), notNPE), an already-disconnected link, a successful write, and IOException
propagation from the sink.
./gradlew testDebugUnitTest— passes locally.QSO, then power the rig off during TX. Expected: app logs "not
connected" and keeps running instead of the CAT thread dying; on
repower + reconnect, CAT resumes.