Search code examples
objective-cobd-ii

How to fix this race condition in Objective C / async problem? 'index 1 beyond bounds for empty array'


Hi i have a Problem with this Library: LTSupportAutomotive

I´m a swift programmer so not really good in objective c.

how to fix this race condition?

I hope someone can help me.

'*** -[__NSArrayM insertObject:atIndex:]: index 1 beyond bounds for empty array'

-(void)asyncEnqueueInternalCommand:(LTOBD2AdapterInternalCommand*)internalCommand
{
    @synchronized(self) {
        [_commandQueue addObject:internalCommand];
    }
}

Source code:

https://github.com/mickeyl/LTSupportAutomotive/blob/f51b962421f211ee6af5c733f79190117d7cac5e/LTSupportAutomotive/LTOBD2Adapter.m

UPDATE 1:

Created a branch with the first fixes: https://github.com/Skyb0rg/LTSupportAutomotive/tree/BugfixMemoryManagement

After adding additional commandQueue i got new errors:

in LTBTLEWriteCharacteristicStream.m

-(void)characteristicDidWriteValue
{
    [self.delegate stream:self handleEvent:NSStreamEventHasSpaceAvailable];
}

this function is crashing with: Selector name found in current argument registers: delegate

Thread 6 Crashed: 0 libobjc.A.dylib 0x00000001bcc19430 objc_retain + 16

1 LTSupportAutomotive 0x00000001093c2a34 -[LTBTLEWriteCharacteristicStream characteristicDidWriteValue] (LTBTLEWriteCharacteristicStream.m:39)

2 LTSupportAutomotive 0x00000001093c2714 -[LTBTLESerialTransporter peripheral:didWriteValueForCharacteristic:error:] (LTBTLESerialTransporter.m:311)

3 CoreBluetooth 0x00000001c35e6ce0 -[CBPeripheral handleAttributeEvent:args:attributeSelector:delegateSelector:delegateFlag:] + 236

4 CoreBluetooth 0x00000001c35e6e40 -[CBPeripheral handleCharacteristicEvent:characteristicSelector:delegateSelector:delegateFlag:] + 128

5 CoreBluetooth 0x00000001c35e24f0 -[CBPeripheral handleMsg:args:] + 352

6 CoreBluetooth 0x00000001c35dcbfc -[CBCentralManager handleMsg:args:] + 200

7 CoreBluetooth 0x00000001c35eb770 __30-[CBXpcConnection _handleMsg:]_block_invoke + 56

8 libdispatch.dylib 0x00000001bd4696c8 _dispatch_call_block_and_release + 20

9 libdispatch.dylib 0x00000001bd46a484 _dispatch_client_callout + 12

10 libdispatch.dylib 0x00000001bd411bd0 _dispatch_lane_serial_drain$VARIANT$mp + 588

11 libdispatch.dylib 0x00000001bd41274c _dispatch_lane_invoke$VARIANT$mp + 480

12 libdispatch.dylib 0x00000001bd411a9c _dispatch_lane_serial_drain$VARIANT$mp + 280

13 libdispatch.dylib 0x00000001bd412718 _dispatch_lane_invoke$VARIANT$mp + 428

14 libdispatch.dylib 0x00000001bd41aeb8 _dispatch_workloop_worker_thread + 596

15 libsystem_pthread.dylib 0x00000001bd64d0dc _pthread_wqthread + 308

16 libsystem_pthread.dylib 0x00000001bd64fcec start_wqthread + 0

and another crash in: LTBTLEReadCharacteristicStream.m

-(void)characteristicDidUpdateValue
{
        NSData* value = _characteristic.value;
        [_buffer appendData:value];
        [self.delegate stream:self handleEvent:NSStreamEventHasBytesAvailable];
}

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[OS_dispatch_data stream:handleEvent:]: unrecognized selector sent to instance 0x281be1b90'

Thread 9 Crashed: 0 libsystem_kernel.dylib 0x00000001bd5c7104 __pthread_kill + 8

1 libsystem_pthread.dylib 0x00000001bd643020 pthread_kill$VARIANT$mp + 376

2 libsystem_c.dylib 0x00000001bd51ed78 abort + 136

3 VW-R-CLUB Member APP 0x00000001045603ac uncaught_exception_handler + 68

4 CoreFoundation 0x00000001bda321e0 __handleUncaughtException + 688

5 libobjc.A.dylib 0x00000001bcc01e4c _objc_terminate() + 108

6 VW-R-CLUB Member APP 0x0000000104555c4c BITCrashUncaughtCXXTerminateHandler() (BITCrashCXXExceptionHandler.mm:183)

7 libc++abi.dylib 0x00000001bcbf50fc std::__terminate(void (*)()) + 12

8 libc++abi.dylib 0x00000001bcbf5188 std::terminate() + 80

9 libdispatch.dylib 0x00000001bd46a498 _dispatch_client_callout + 32

10 libdispatch.dylib 0x00000001bd411bd0 _dispatch_lane_serial_drain$VARIANT$mp + 588

11 libdispatch.dylib 0x00000001bd41274c _dispatch_lane_invoke$VARIANT$mp + 480

12 libdispatch.dylib 0x00000001bd411a9c _dispatch_lane_serial_drain$VARIANT$mp + 280

13 libdispatch.dylib 0x00000001bd412718 _dispatch_lane_invoke$VARIANT$mp + 428

14 libdispatch.dylib 0x00000001bd41aeb8 _dispatch_workloop_worker_thread + 596

15 libsystem_pthread.dylib 0x00000001bd64d0dc _pthread_wqthread + 308

16 libsystem_pthread.dylib 0x00000001bd64fcec start_wqthread + 0


Solution

  • First, the code you show is not the one to be found in github. Do you synchronize all usages of _commandQueue, or only this one? If only this one, why aren't the others synchronized?

    Then, in github I find multiple usages of _commandQueue, some of them in methods that are named async..., but also some not async, like cancelPendingCommands or responseCompleted. You need to find out what is the intention of async in a method name, and why methods like cancelPendingCommands is not somehow async.


    Update

    So the main idea seems to protect all _commandQueue accesses inside the serial _dispatchQueue. This is already the case in the async... methods: They are called from within that queue:

    dispatch_async( _dispatchQueue, ^{
        [self asyncEnqueueInternalCommand:internalCommand];
    });
    

    So you need to ensure that every access to _commandQueue is enqueued in this queue, e.g. change cancelPendingCommands to something like

    -(void)cancelPendingCommands
    {
        dispatch_async( _dispatchQueue, ^{
            // This cancels all but the first command in order to prevent sending a new command while
            // the response to an active command is still pending. OBD2 adapters usually can't cope with
            // that and emit a 'STOPPED' response in that case.
            if ( _hasPendingAnswer )
            {
                NSRange allButTheFirst = NSMakeRange( 1, _commandQueue.count - 1 );
                [_commandQueue removeObjectsInRange:allButTheFirst];
            }
            else
            {
                [_commandQueue removeAllObjects];
            }
        });
    }
    

    (or create a dedicated asyncCancelPendingCommands function, that will be called from cancelPendingCommands from inside a dispatch block. And so on.

    P.S. If you need synchronous execution, you could also use dispatch_sync instead of dispatch_async, but then you have to ensure that you do not create deadlocks.