Skip to content

Advertisement timers and CBMNative peripherals are not thread-safe #114

Closed
@nathandud

Description

@nathandud

Problem

The CoreBluetoothMock library suffers from periodic crashes due to a number of errors related to concurrent access. While most internal CBMCentralManagerMock instances are protected from concurrent access with DispatchQueue(label: "Mutex"), neither the mutable advertisement timers in CBMCentralMock nor the list of peripherals in CBMCentralManagerNative are thread-safe.

Example

Here is a basic unit test that sets up a mock peripheral and simulates it

import XCTest
import CoreBluetoothMock
@testable import BluetoothMockExample

final class BluetoothMockExampleTests: XCTestCase {
  let device = CBMPeripheralSpec
    .simulatePeripheral(proximity: CBMProximity.near)
    .advertising(
      advertisementData: [
        CBMAdvertisementDataLocalNameKey: "Test_Device",
        CBMAdvertisementDataServiceUUIDsKey: CBMUUID(),
        CBMAdvertisementDataIsConnectable: true as NSNumber
      ]
    )
    .connectable(
      name: "Test_Device",
      services: [
        CBMServiceMock(
          type: CBMUUID(),
          primary: true,
          characteristics: [CBMCharacteristicMock(type: CBMUUID(), properties: [.read])]
        )
      ],
      delegate: nil
    )
    .build()
  
  override func tearDown() async throws {
    CBMCentralManagerMock.tearDownSimulation()
  }
  
  func testSimulation() throws {
    CBMCentralManagerMock.simulatePeripherals([device])
  }
}

When run a single time, this test passes ~99.5% of the time. When tested repeatedly (1000x), however, the tests begin crashing with various errors including:

  • "-[__NSCFNumber countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...

  • malloc: double free for ptr ...

  • EXC_BAD_ACCESS (code=1, address=...)

RepeatedTestCrash-CoreBluetoothMock.mp4

Recommended Solution

Instead of wrapping every access to the advertisement timers or native peripherals in a sync or async block with a local queue, I recommend implementing a thread-safe wrapper similar to what @wcharysz-allegion has done in his fork - see main...wcharysz-allegion:IOS-CoreBluetooth-Mock:main

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