Hard to notice multithreading bug

During working on my UIBezierPath reimplementation to enable checking for crossing paths I encountered a bug.

The bug was really hard to debug. It happened randomly. App was generally unstable, it was crashing randomly and in random functions, both mine and Apple's dealloc, retain, etc.

I was quite happy that I spotted the bug by accident after a few hours of debugging, because I may have been stuck with it for a few days. Check for yourself, if you can spot the bug.

This is a piece of code that enumerates all the self.paths drawn by the user and checks if any paths cross at all. We do the checking on self.checkQueue which is a parallel NSOperationQueue. We check all the (n^2)/2 possibilities by rejecting other index pairs. We add the crossing paths to an NSMutableArray after detecting a collision, synchronized of course, as this is a parallel queue.

NSMutableArray *operations = [NSMutableArray array];

[self.paths.copy enumerateObjectsUsingBlock:^(ABPath *checkedPath,
                                                    NSUInteger idx,
                                                    BOOL *stop) {
    
    [self.paths.copy enumerateObjectsUsingBlock:^(ABPath *path,
                                                        NSUInteger idx2,
                                                        BOOL *stop2) {
   
         [operations addObject:[NSBlockOperation blockOperationWithBlock:^{
            
            if (idx2 >= idx) {
                *stop2 = YES;
                return;
            }
            
            if ([checkedPath isCrossing:path]) {
            	@synchronized(self.collisions){
                    [self.collisions addObject:checkedPath];
	                [self.collisions addObject:path];
                }
            }
         }]];
    }];
}];

for(NSOperation *o in operations) {
    [self.checkQueue addOperation:o];
}

The crashes usually happened in isCrossing: or around that point, in some internal Apple functions called by retain or any other function that in generally is well tested and shouldn't crash. The random crashes in weird functions are usually caused by a multithreading bug.

Solution

The problem was in this line:

*stop2 = YES;

As the Apple documentation states:

stop - A reference to a Boolean value. The block can set the value to YES to stop further processing of the array. The stop argument is an out-only argument. You should only ever set this Boolean to YES within the Block.

We were assigning a value to the Boolean in the block, but in an NSOperation. The operation was then stored in an NSArray and scheduled to run on the self.checkQueue. The stop pointer stopped being valid after we left the enumerateObjectsUsingBlock: method. The NSOperations were run after finishing both of the enumerateObjectsUsingBlock: statements.

So what really happened, we were writing a value of 1 (YES) into a pointer pointing somewhere around our call stack. stop storage is created on the stack in the enumerateObjectsUsingBlock: function and then it's removed from the stack. If we write a value into a pointer that is pointing near our call stack, on a separate thread, then the currently run functions can crash. Functions like dealloc, retain or some other internal function that happens to be on the stack.

The fix was to move the part of the code:

if (idx2 >= idx) {
    *stop2 = YES;
    return;
}

before the NSOperation. Then the stop2 pointer is still valid and we can safely stop the enumeration as we initially wanted, instead of crashing the whole App.