Summary
On the iOS 26 SDK, an incoming VoIP push reliably crashes the app with SIGABRT / abort() the moment the PushKit payload reaches the React Native bridge. The crash originates inside this library, in +[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:forType:], which forwards the raw, unguarded payload.dictionaryPayload to the bridge as an event body. When that dictionary contains any value NSJSONSerialization rejects, +[NSJSONSerialization dataWithJSONObject:options:error:] throws an Objective-C NSException, which is uncatchable from a Swift AppDelegate do/catch and from the bridge call site, so it propagates to objc_terminate() → abort().
This is the same class of bug Expo just fixed in expo-notifications (BackgroundEventTransformer) — see prior art in expo/expo#45198 (merged) / expo/expo#43889 — but that fix is in a different code path, so this library still crashes.
Environment
- iOS: 26.x (built against the iOS 26 SDK; does not reproduce on iOS 18)
- Device: physical iPhone (PushKit required; not reproducible in Simulator)
react-native-voip-push-notification: 3.3.3 (latest)
- React Native: 0.79
- AppDelegate language: Swift
What happens
EXC_CRASH (SIGABRT), Termination Reason: SIGNAL 6 Abort trap: 6. The lastExceptionBacktrace (the actual throw) — observed identically across 9 crash reports — is:
0 libobjc.A.dylib objc_exception_throw
1 Foundation +[NSJSONSerialization dataWithJSONObject:options:error:]
... <RN bridge frames serializing the event body>
N <app> RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:forType:
N+1 UIKitCore -[UIApplication pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:]
N+2 PushKit __74-[PKPushRegistry remoteUserNotificationPayloadReceived:completionHandler:]_block_invoke
Root cause
In ios/RNVoipPushNotification/RNVoipPushNotificationManager.m, +didReceiveIncomingPushWithPayload:forType: does:
[voipPushManager sendEventWithNameWrapper:RNVoipPushRemoteNotificationReceivedEvent
body:payload.dictionaryPayload];
payload.dictionaryPayload is forwarded verbatim and the RN bridge serializes it with NSJSONSerialization. If the dictionary contains anything JSON rejects — most commonly:
- a lone / unpaired UTF-16 surrogate in a string (e.g. a
caller_name whose emoji was truncated by an upstream byte/length limit). Note this passes +isValidJSONObject: yet still makes dataWithJSONObject: throw;
NaN / Infinity numbers;
- a non-
NSString dictionary key, or a non-JSON value type (NSDate, NSData, …);
then dataWithJSONObject: raises an NSInvalidArgumentException. Because it is a raised ObjC exception (not an NSError), neither the Swift AppDelegate nor the bridge can catch it under the iOS 26 toolchain, and the process aborts. Pre-iOS-26 toolchains masked this; the iOS 26 SDK surfaces it as a hard crash.
Minimal repro
- Build a dev client against the iOS 26 SDK on a physical device; register for VoIP pushes via this library.
- Send a VoIP push whose payload contains a string with an unpaired UTF-16 surrogate (or a
NaN), e.g. a caller_name ending in a half emoji.
- The push is received → the app aborts with the backtrace above before any JS runs.
Proposed fix
Guard the serialization at the library's own call site so a bad PushKit payload degrades gracefully instead of aborting the app: sanitize/validate the dictionary, pre-flight the exact NSJSONSerialization call, and forward only a proven-serializable body (or a minimal fallback that preserves the call) instead of letting the bridge throw. I have a working fix and will open a PR against this method — happy to match maintainer preference (full recursive sanitizer vs. a minimal isValidJSONObject: + @try/@catch guard).
Summary
On the iOS 26 SDK, an incoming VoIP push reliably crashes the app with
SIGABRT/abort()the moment the PushKit payload reaches the React Native bridge. The crash originates inside this library, in+[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:forType:], which forwards the raw, unguardedpayload.dictionaryPayloadto the bridge as an event body. When that dictionary contains any valueNSJSONSerializationrejects,+[NSJSONSerialization dataWithJSONObject:options:error:]throws an Objective-CNSException, which is uncatchable from a Swift AppDelegatedo/catchand from the bridge call site, so it propagates toobjc_terminate()→abort().This is the same class of bug Expo just fixed in
expo-notifications(BackgroundEventTransformer) — see prior art in expo/expo#45198 (merged) / expo/expo#43889 — but that fix is in a different code path, so this library still crashes.Environment
react-native-voip-push-notification: 3.3.3 (latest)What happens
EXC_CRASH (SIGABRT),Termination Reason: SIGNAL 6 Abort trap: 6. ThelastExceptionBacktrace(the actual throw) — observed identically across 9 crash reports — is:Root cause
In
ios/RNVoipPushNotification/RNVoipPushNotificationManager.m,+didReceiveIncomingPushWithPayload:forType:does:payload.dictionaryPayloadis forwarded verbatim and the RN bridge serializes it withNSJSONSerialization. If the dictionary contains anything JSON rejects — most commonly:caller_namewhose emoji was truncated by an upstream byte/length limit). Note this passes+isValidJSONObject:yet still makesdataWithJSONObject:throw;NaN/Infinitynumbers;NSStringdictionary key, or a non-JSON value type (NSDate,NSData, …);then
dataWithJSONObject:raises anNSInvalidArgumentException. Because it is a raised ObjC exception (not anNSError), neither the Swift AppDelegate nor the bridge can catch it under the iOS 26 toolchain, and the process aborts. Pre-iOS-26 toolchains masked this; the iOS 26 SDK surfaces it as a hard crash.Minimal repro
NaN), e.g. acaller_nameending in a half emoji.Proposed fix
Guard the serialization at the library's own call site so a bad PushKit payload degrades gracefully instead of aborting the app: sanitize/validate the dictionary, pre-flight the exact
NSJSONSerializationcall, and forward only a proven-serializable body (or a minimal fallback that preserves the call) instead of letting the bridge throw. I have a working fix and will open a PR against this method — happy to match maintainer preference (full recursive sanitizer vs. a minimalisValidJSONObject:+@try/@catchguard).