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;
}
}
}
messages and redisplayedInAppMessages are both declared nonatomic (lines 111 and 119). The only @synchronized blocks in the file protect messageDisplayQueue, 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: replaces self.messages (line 412) and then walks _redisplayedInAppMessages in resetRedisplayMessagesBySession (436–443); persistInAppMessageForRedisplay: does [_redisplayedInAppMessages setObject:forKey:] (line 954); deleteInactiveMessage: replaces self.messages (449); deleteOldRedisplayedInAppMessages reads the dictionary again (456–474).
addTriggers: has no thread affinity of its own: it runs evaluateRedisplayedInAppMessages: inline before handing off to the trigger controller (832–841). So an addTrigger from any non-main thread that lands while the main queue is inside one of the writers above is a data race on a mutable NSDictionary and an NSArray swap. 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-onesignal 5.4.5 (OneSignal iOS SDK 5.5.1) in a React Native app with at least one in-app message that has redisplay enabled (so redisplayedInAppMessages is non-empty after the first display).
- On cold start, and again on every
AppState → active, call OneSignal.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_ACCESS in -[__NSDictionaryM objectForKey:] under evaluateRedisplayedInAppMessages:.
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(...)
What happened?
A fatal native crash inside the OneSignal iOS SDK's in-app-message controller, reached from a normal
addTriggercall:Our app is React Native (
react-native-onesignal5.4.5, which bundles OneSignal iOS SDK 5.5.1). The React Native bridge invokesaddTriggeron a background thread, which is what putsaddTriggers:off the main queue.Root cause, from reading the source.
-[OSMessagingController addTriggers:]callsevaluateRedisplayedInAppMessages:on the caller's thread. That method does an unsynchronized read of two collections that the SDK mutates on the main queue:messagesandredisplayedInAppMessagesare both declarednonatomic(lines 111 and 119). The only@synchronizedblocks in the file protectmessageDisplayQueue, not these two.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:andevaluateRedisplayedInAppMessages:between tags5.5.1and5.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 forevaluateRedisplayedInAppMessagesin 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
addTriggershortly 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:
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).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.updateInAppMessagesFromServer:on the main queue) or with a message being persisted for redisplay.EXC_BAD_ACCESSin-[__NSDictionaryM objectForKey:]underevaluateRedisplayedInAppMessages:.A Thread Sanitizer run of the SDK with
addTriggers:driven from aDISPATCH_QUEUE_PRIORITY_DEFAULTglobal queue while a fetch completes should surface the race without needing the crash to reproduce.What did you expect to happen?
addTrigger/addTriggersto 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 touchingmessages/redisplayedInAppMessages, the wayupdateInAppMessagesFromServer:already does. Either dispatching the body ofaddTriggers:onto the main queue, or guarding the two collections with the same lock discipline used formessageDisplayQueue, 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
addTriggerpairs into oneaddTriggerscall. 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
Relevant log output