Reentrancy and Callback Race Conditions

Race conditions take many forms. One that is widely known happens in multithreaded applications due to concurrency. Yet they may happen in single-threaded code as well due to the majority of the functions being non-reentrant.
Reentrancy is usually discussed alongside signals and interrupts: when a function interrupted in the middle of execution is being called again (or reentered) from the interruption handler. A reentrant function is expected to perform the invocation from the interruption handler and then continue the interrupted execution as if nothing happened. If it does not, then it’s considered non-reentrant and extra caution is required.
Both signals and interrupts exist since ancient times and therefore exist only as lore for many. After all only selected few need to write in plain C and bother themselves with such technicalities. In the modern times we have layers and layers of abstraction that shield end-user code. See Python’s PEP 475 for one of such abstractions.
Except the safety is false for all code that relies on synchronous callbacks. Remember that any function call is essentially an interruption and function being called is an interruption handler.
Many UI frameworks belong to that category. Callbacks are pretty much standard to communicate state changes. It’s practically impossible to follow what is calling what. If you happened to write such code recently, ask yourself whether you can guarantee that one function will never end up calling the other.
Mitigating reentrancy in Swift and Objective-C
In frameworks Apple ships for all its platforms both Observation and Delegation are heavily used and encouraged patterns. That means your average iOS app is full of disguised synchronous callbacks. Out of the box all methods are considered reentrant which in turn allows you to hit most obscure bugs, especially if you use Key-Value Observing (ask Brent Simmons how).
Here I present the technique to protect your code from unexpected reentrancy. The core of the idea is to have a canary that would assert in runtime if given method is being reentred.
Solution for Objective-C I relies on the relatively modern __cleanup__
attribute:
void _NoReentryScopeEnter(BOOL *aVar)
{
if (*aVar)
[NSException raise:NSInternalInconsistencyException format:@"the method is not reentrable"];
*aVar = YES;
}
void _NoReentryScopeLeave(BOOL **aVar)
{
**aVar = NO;
}#define _NoReentryScope(var, aVarPtr) \
__attribute__((__cleanup__(_NoReentryScopeLeave))) BOOL *var = aVarPtr; \
_NoReentryScopeEnter(var);#define NoReentryScope(aVarPtr) \
_NoReentryScope(OS_CONCAT(NoReentryScope, __COUNTER__), aVarPtr)void WithNoReentryScope(BOOL *aCanary _Nonnull, (NS_NOESCAPE void (^)(void))aBlock _Nonnull)
{
NoReentryScope(aCanary)
aBlock();
}
Which can be used like this:
@implementation
{
id _myKey;
int _internalState;
BOOL _canary;
}- (void)doSomething
{
WithNoReentryScope(&_canary, ^{
_internalState = ...;
}
}- (void)setMyKey:(id)newValue
{
if (_internalState == ...)
{
...
} {
WithNoReentryScope(&_canary, ^{
[self willChangeValueForKey:@"myKey"];
} } switch (_internalState)
{
...
} {
WithNoReentryScope(&_canary, ^{
[self didChangeValueForKey:@"myKey"];
}
}
}
Here the _internalState
is protected from unexpected mutations by KVO callbacks assigned to myKey
. What mutations are unexpected? If you take a closer look you will notice that _internalState
is used before and after the -willChangeValueForKey:
callback which may try to change it.
This may appear quite verbose but in reality you only need protection when using external callbacks. It only as verbose as synchronization primitives in concurrent code.
It’s best to execute callbacks in asynchronous manner by enqueuing requests and then letting a run loop to execute them in order (this approach is as ancient as signals and interrupts).
Only use synchronous callbacks when absolutely necessary, e.g. when you want to push an update on screen within a single frame. Carefully consider vectors of reentrant “attacks”. Remember, it’s easy to fall into the serenity trap due to availability and ease-of-use modern frameworks put at your disposal.