I couldn't reproduce a crash in -doSetErrorInBlock:, but I can reproduce a crash in -triggerEXC_BAD_ACCESS with "-[NSError retain]: message sent to deallocated instance" in debug node (I am not sure if it's due to NSZombie or some other debug options).
The reason for it is that *error in -doSetErrorInBlock: has type NSError * __autoreleasing, and the implementation of -[NSArray enumerateObjectsUsingBlock:] (which is closed-source but it's possible to examine the assembly) happens to internally have an autorelease pool around the execution of the block. An object pointer which is __autoreleasing means that we don't retain it, and we assume it is alive by being retained by some autorelease pool. That means it is bad to assign something to an __autoreleasing variable inside an autorelease pool, and then try to access it after the autorelease pool ended, because the end of the autorelease pool could have deallocated it, so you can be left with a dangling pointer. This section of the ARC spec says:
It is undefined behavior if a non-null pointer is assigned to an
__autoreleasing object while an autorelease pool is in scope and
then that object is read after the autorelease pool’s scope is left.
The reason why the crash message says it is trying to retain it, is because of what happens when you try to pass a "pointer to __strong" (e.g. &error in -triggerEXC_BAD_ACCESS) to a parameter of type "pointer to __autoreleasing" (e.g. the parameter of -doSetErrorInBlock:). As you can see from this section of the ARC spec, a "pass-by-writeback" process happens, where they create a temporary variable of type __autoreleasing, assign the value of the __strong variable to it, make the call, and then assign the value of the __autoreleasing variable back to the __strong variable, so your triggerEXC_BAD_ACCESS method is really kind of like this:
NSError *error = nil;
NSError * __autoreleasing temporary = error;
[self doSetErrorInBlock:&temporary];
error = temporary;
The last step of assigning the value back to the __strong variable performs a retain, and that's when it encounters the deallocated instance.
I could reproduce the same crash in your second example if I change the -runABlock: to:
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj, NSUInteger idx, BOOL *stop))block
{
BOOL anotherWriteback = NO;
@autoreleasepool {
block(@"Some string", 0, &anotherWriteback);
}
}
You shouldn't really use __autoreleasing in a new method that you write. __strong is much better because the strong reference makes sure you don't accidentally have dangling references and problems like that. The main reason why __autoreleasing exists is because back in manual reference counting days, there were no explicit ownership qualifiers, and the "convention" was that retain counts were not transferred into or out of methods, and so objects returned from a method (including objects returned by pointer using an out-parameter) would be autoreleased rather than retained. (And those methods would be responsible for ensuring that the object is still valid when the method returns.) And since your program can be used on different OS versions, they cannot change the behavior of APIs in new OS versions, so they are stuck with this "pointer to __autoreleasing" type. However, in a method that you yourself write in ARC (which does have explicit ownership qualifiers), which is only going to be called by your own ARC code, by all means use __strong. If you wrote your method using __strong, it would not crash (by default pointer-to-object-pointers are interpreted as __autoreleasing, so you have to specify __strong explicitly):
- (void)doSetErrorInBlock:(NSError * __strong *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
}
If you for some reason insist on taking a parameter of type NSError * __autoreleasing *, and want to do the same thing you were doing but safely, you should be using a __strong variable for the block, and only assign it into the __autoreleasing at the end:
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
__block NSError *result;
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
result = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
*error = result;
}