Repository navigation
[Bug]: EXC_BAD_ACCESS in -[OSMessagingController evaluateRedisplayedInAppMessages:] — addTriggers: reads messages/redisplayedInAppMessages off the main thread with no synchronization #1753
Description
What happened?
A fatal native crash inside the OneSignal iOS SDK's in-app-message controller, reached from a normal addTrigger call:
EXC_BAD_ACCESS (KERN_INVALID_ADDRESS at 0x10) — thread 2, a background dispatch worker
-[__NSDictionaryM objectForKey:]
-[OSMessagingController evaluateRedisplayedInAppMessages:] OSMessagingController.m:824
-[OSMessagingController addTriggers:] OSMessagingController.m:833
+[OneSignalInAppMessages addTrigger:withValue:] OneSignalInAppMessages.m:85
-[RCTOneSignalEventEmitter addTrigger:value:]
facebook::react::ObjCTurboModule::performVoidMethodInvocation
Our app is React Native (react-native-onesignal 5.4.5, which bundles OneSignal iOS SDK 5.5.1). The React Native bridge invokes addTrigger on a background thread, which is what puts addTriggers: off the main queue.
Root cause, from reading the source. -[OSMessagingController addTriggers:] calls evaluateRedisplayedInAppMessages: on the caller's thread. That method does an unsynchronized read of two collections that the SDK mutates on the main queue:
// OSMessagingController.m @ 5.5.1 (lines 822–829); identical at 5.6.3
- (void)evaluateRedisplayedInAppMessages:(NSArray<NSString *> *)newTriggersKeys {
for (OSInAppMessageInternal *message in _messages) {
if ([_redisplayedInAppMessages objectForKey:message.messageId] &&
[self.triggerController hasSharedTriggers:message newTriggersKeys:newTriggersKeys]) {
message.isTriggerChanged = true;
}
}
}messagesandredisplayedInAppMessagesare both declarednonatomic(lines 111 and 119). The only@synchronizedblocks in the file protectmessageDisplayQueue, not these two.- Writers, all of which run on the main queue via
dispatch_async(dispatch_get_main_queue(), …)from the IAM fetch completion or from message display:updateInAppMessagesFromServer:replacesself.messages(line 412) and then walks_redisplayedInAppMessagesinresetRedisplayMessagesBySession(436–443);persistInAppMessageForRedisplay:does[_redisplayedInAppMessages setObject:forKey:](line 954);deleteInactiveMessage:replacesself.messages(449);deleteOldRedisplayedInAppMessagesreads the dictionary again (456–474). addTriggers:has no thread affinity of its own: it runsevaluateRedisplayedInAppMessages:inline before handing off to the trigger controller (832–841). So anaddTriggerfrom any non-main thread that lands while the main queue is inside one of the writers above is a data race on a mutableNSDictionaryand anNSArrayswap. The faulting frame (objectForKey:dereferencing offset 0x10) is consistent with the dictionary being read while its storage is being rewritten.
I diffed addTriggers: and evaluateRedisplayedInAppMessages: between tags 5.5.1 and 5.6.3 (released 2026-09-21): they are byte-identical, and no release note between those tags mentions triggers, redisplay, or thread safety in this area. Upgrading would not change the outcome. I did not find an existing issue for evaluateRedisplayedInAppMessages in this tracker.
Observed once so far, on one device (iPhone14,3 / iOS 26.1), on 2026-09-19 at 22:10 UTC. Our app calls addTrigger shortly after cold start and after returning to the foreground, i.e. exactly when the SDK is fetching and saving in-app messages, which is the window in which this can fire.
Steps to reproduce?
Not reproduced deterministically; the window is a few milliseconds per fetch. The shape is:
- Integrate
react-native-onesignal5.4.5 (OneSignal iOS SDK 5.5.1) in a React Native app with at least one in-app message that has redisplay enabled (soredisplayedInAppMessagesis non-empty after the first display). - On cold start, and again on every
AppState→active, callOneSignal.InAppMessages.addTrigger(key, value)from JS. In React Native the TurboModule invocation runs on a background queue, so-[OSMessagingController addTriggers:]executes off the main thread. - Overlap that call with the SDK's in-app-message fetch completing (
updateInAppMessagesFromServer:on the main queue) or with a message being persisted for redisplay. - Occasionally:
EXC_BAD_ACCESSin-[__NSDictionaryM objectForKey:]underevaluateRedisplayedInAppMessages:.
A Thread Sanitizer run of the SDK with addTriggers: driven from a DISPATCH_QUEUE_PRIORITY_DEFAULT global queue while a fetch completes should surface the race without needing the crash to reproduce.
What did you expect to happen?
addTrigger / addTriggers to be safe to call from any thread (the public API documents no main-thread requirement, and the React Native wrapper does not marshal to main), or at minimum for the SDK to hop to the main queue before touching messages / redisplayedInAppMessages, the way updateInAppMessagesFromServer: already does. Either dispatching the body of addTriggers: onto the main queue, or guarding the two collections with the same lock discipline used for messageDisplayQueue, would close this.
Our mitigation in the meantime (app side): hold trigger writes for ~3 s after SDK init and after each foreground transition, and coalesce per-event addTrigger pairs into one addTriggers call. That narrows the window but cannot close it.
OneSignal iOS SDK version
Release 5.5.1 (via react-native-onesignal 5.4.5); affected code unchanged through 5.6.3
iOS version
26
Specific iOS version
- iOS 26.1 (iPhone 13 Pro Max, iPhone14,3)
Relevant log output
Exception Type: EXC_BAD_ACCESS
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000010
Crashed thread: 2 (dispatch worker, not main)
0 CoreFoundation -[__NSDictionaryM objectForKey:]
1 OneSignalInAppMessages -[OSMessagingController evaluateRedisplayedInAppMessages:] OSMessagingController.m:824
2 OneSignalInAppMessages -[OSMessagingController addTriggers:] OSMessagingController.m:833
3 OneSignalInAppMessages +[OneSignalInAppMessages addTrigger:withValue:] OneSignalInAppMessages.m:85
4 <app> -[RCTOneSignalEventEmitter addTrigger:value:]
5 <app> facebook::react::ObjCTurboModule::performVoidMethodInvocation(...)
6 <app> facebook::react::ObjCTurboModule::performMethodInvocation(...)
Activity