Fix thread safety crash with NSLock

- Replace problematic messageQueue.sync with NSLock for peerNicknames
- Prevents deadlock when accessing from within messageQueue
- Add thread-safe access to all peerNicknames read/write operations
- Fixes EXC_BREAKPOINT crash from recursive queue access

The previous approach using messageQueue.sync caused deadlock when
getAllConnectedPeerIDs was called from within the messageQueue itself.
This commit is contained in:
jack
2025-07-04 14:12:59 +02:00
parent a59c3f7ec6
commit 3f882eec0f
+33 -14
View File
@@ -30,15 +30,8 @@ class BluetoothMeshService: NSObject {
private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:] private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:]
private var characteristic: CBMutableCharacteristic! private var characteristic: CBMutableCharacteristic!
private var subscribedCentrals: [CBCentral] = [] private var subscribedCentrals: [CBCentral] = []
private var _peerNicknames: [String: String] = [:] private var peerNicknames: [String: String] = [:]
private var peerNicknames: [String: String] { private let peerNicknamesLock = NSLock()
get {
messageQueue.sync { _peerNicknames }
}
set {
messageQueue.async(flags: .barrier) { self._peerNicknames = newValue }
}
}
private var activePeers: Set<String> = [] // Track all active peers private var activePeers: Set<String> = [] // Track all active peers
private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers
private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery
@@ -662,7 +655,10 @@ class BluetoothMeshService: NSObject {
func getPeerNicknames() -> [String: String] { func getPeerNicknames() -> [String: String] {
return peerNicknames peerNicknamesLock.lock()
let copy = peerNicknames
peerNicknamesLock.unlock()
return copy
} }
func getPeerRSSI() -> [String: NSNumber] { func getPeerRSSI() -> [String: NSNumber] {
@@ -730,7 +726,10 @@ class BluetoothMeshService: NSObject {
} }
// Safely check if peer has a nickname (thread-safe) // Safely check if peer has a nickname (thread-safe)
let hasNickname = self._peerNicknames[peerID] != nil peerNicknamesLock.lock()
let hasNickname = peerNicknames[peerID] != nil
peerNicknamesLock.unlock()
if hasNickname { if hasNickname {
return peerID return peerID
} }
@@ -1030,7 +1029,9 @@ class BluetoothMeshService: NSObject {
// Received broadcast message // Received broadcast message
// Store nickname mapping // Store nickname mapping
peerNicknamesLock.lock()
peerNicknames[senderID] = message.sender peerNicknames[senderID] = message.sender
peerNicknamesLock.unlock()
let messageWithPeerID = BitchatMessage( let messageWithPeerID = BitchatMessage(
sender: message.sender, sender: message.sender,
@@ -1112,9 +1113,11 @@ class BluetoothMeshService: NSObject {
// Received private message // Received private message
// Store nickname mapping if we don't have it // Store nickname mapping if we don't have it
peerNicknamesLock.lock()
if peerNicknames[senderID] == nil { if peerNicknames[senderID] == nil {
peerNicknames[senderID] = message.sender peerNicknames[senderID] = message.sender
} }
peerNicknamesLock.unlock()
let messageWithPeerID = BitchatMessage( let messageWithPeerID = BitchatMessage(
sender: message.sender, sender: message.sender,
@@ -1239,7 +1242,9 @@ class BluetoothMeshService: NSObject {
let isFirstAnnounce = !announcedPeers.contains(senderID) let isFirstAnnounce = !announcedPeers.contains(senderID)
// Store the nickname // Store the nickname
peerNicknamesLock.lock()
peerNicknames[senderID] = nickname peerNicknames[senderID] = nickname
peerNicknamesLock.unlock()
// Stored nickname // Stored nickname
// Updated nicknames // Updated nicknames
@@ -1312,7 +1317,9 @@ class BluetoothMeshService: NSObject {
} }
// Clean up peer data // Clean up peer data
peerNicknamesLock.lock()
peerNicknames.removeValue(forKey: senderID) peerNicknames.removeValue(forKey: senderID)
peerNicknamesLock.unlock()
} else { } else {
// Failed to parse leave packet // Failed to parse leave packet
} }
@@ -1646,7 +1653,11 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
announcedToPeers.remove(peerID) announcedToPeers.remove(peerID)
// Only show disconnect if we have a resolved nickname // Only show disconnect if we have a resolved nickname
if let nickname = peerNicknames[peerID], nickname != peerID { peerNicknamesLock.lock()
let nickname = peerNicknames[peerID]
peerNicknamesLock.unlock()
if let nickname = nickname, nickname != peerID {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname) self.delegate?.didDisconnectFromPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
@@ -1935,7 +1946,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
for peerID in peersToRemove { for peerID in peersToRemove {
activePeers.remove(peerID) activePeers.remove(peerID)
announcedToPeers.remove(peerID) announcedToPeers.remove(peerID)
if let nickname = peerNicknames[peerID] { peerNicknamesLock.lock()
let nickname = peerNicknames[peerID]
peerNicknamesLock.unlock()
if let nickname = nickname {
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname) self.delegate?.didDisconnectFromPeer(nickname)
} }
@@ -2085,7 +2100,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
// Sending cover traffic // Sending cover traffic
// Send as a private message so it's encrypted // Send as a private message so it's encrypted
sendPrivateMessage(dummyContent, to: randomPeer, recipientNickname: peerNicknames[randomPeer] ?? "unknown") peerNicknamesLock.lock()
let recipientNickname = peerNicknames[randomPeer] ?? "unknown"
peerNicknamesLock.unlock()
sendPrivateMessage(dummyContent, to: randomPeer, recipientNickname: recipientNickname)
} }
private func generateDummyContent() -> String { private func generateDummyContent() -> String {