Skip to content

[Bug]: EXC_BAD_ACCESS in -[OSMessagingController evaluateRedisplayedInAppMessages:] — addTriggers: reads messages/redisplayedInAppMessages off the main thread with no synchronization #1753

Description

@duro

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:

  1. 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).
  2. 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.
  3. 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.
  4. 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(...)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions