Skip to content

iOS 26: SIGABRT (NSJSONSerialization throws uncatchable NSException) in didReceiveIncomingPushWithPayload: when the VoIP payload isn't JSON-safe #122

@ArditXhaferi

Description

@ArditXhaferi

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

  1. Build a dev client against the iOS 26 SDK on a physical device; register for VoIP pushes via this library.
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions