Skip to content

fix(ios): guard PushKit payload serialization to prevent iOS 26 SIGABRT (NSJSONSerialization throws uncatchable NSException)#123

Open
ArditXhaferi wants to merge 1 commit into
react-native-webrtc:masterfrom
ArditXhaferi:fix/ios26-pushkit-json-serialization-sigabrt
Open

fix(ios): guard PushKit payload serialization to prevent iOS 26 SIGABRT (NSJSONSerialization throws uncatchable NSException)#123
ArditXhaferi wants to merge 1 commit into
react-native-webrtc:masterfrom
ArditXhaferi:fix/ios26-pushkit-json-serialization-sigabrt

Conversation

@ArditXhaferi

Copy link
Copy Markdown

Fixes #122.

Problem

On the iOS 26 SDK, +didReceiveIncomingPushWithPayload:forType: forwards the raw payload.dictionaryPayload to the bridge as an event body. The bridge serializes it with +[NSJSONSerialization dataWithJSONObject:options:error:]. If the payload contains any JSON-unsafe value (unpaired UTF-16 surrogate, NaN/Infinity, non-string key, non-JSON value type), dataWithJSONObject: raises an Objective-C NSException. That exception is uncatchable from a Swift AppDelegate and from the bridge call site under iOS 26 → objc_terminate()abort() (SIGABRT). Pre-iOS-26 toolchains masked this; the iOS 26 SDK surfaces it as a hard crash.

Same bug class Expo fixed in expo-notifications (a different code path): expo/expo#45198.

Fix

Validate/sanitize the payload at this library's own call site and forward only a body proven serializable; otherwise forward a minimal fallback dict so the call still surfaces instead of aborting the process. Wrapped in +isValidJSONObject: + @try/@catch so it can never raise past the library.

  • Valid payloads are byte-for-byte unchanged — only the previously-aborting path is altered (abort → safe fallback + one NSLog).
  • This intentionally guards only this library's own call site (no global NSJSONSerialization swizzle), so it cannot affect any other library or the host app's serialization behavior.
  • The string guard uses NSJSONSerialization itself as the oracle (original → lossy-ASCII → "") because an unpaired surrogate passes +isValidJSONObject: yet still throws in dataWithJSONObject:.

Verification

No iOS unit-test harness exists in the repo, so verified by (a) a dev client built against the iOS 26 SDK on a physical device — VoIP pushes that previously aborted now ring normally — and (b) a standalone Foundation test of the guard against real iOS Foundation:

payload before after
caller_name = lone UTF-16 surrogate abort (SIGABRT) serializes ✓
NaN number abort serializes ✓
NSDate value abort serializes ✓
normal payload (incl. emoji) ok unchanged / preserved ✓

+didReceiveIncomingPushWithPayload: forwarded the raw payload.dictionaryPayload
to the RN bridge, which serializes it with NSJSONSerialization. A JSON-unsafe
value (unpaired UTF-16 surrogate, NaN/Inf, non-string key/value type) makes
+dataWithJSONObject:options:error: raise an ObjC NSException that is uncatchable
from a Swift AppDelegate / the bridge under the iOS 26 SDK, aborting the app.

Pre-flight the serialization at the call site and forward only a proven-safe
body (or a minimal fallback) so a bad payload can never abort the process.

Fixes react-native-webrtc#122.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Guards the PushKit payload forwarded to the React Native bridge to prevent iOS 26 crashes caused by NSJSONSerialization raising an Objective-C NSException when the payload is not JSON-safe.

Changes:

  • Added recursive payload sanitization and a “pre-flight” serialization check to ensure the event body is always JSON-serializable.
  • Updated didReceiveIncomingPushWithPayload:forType: to forward a proven-safe body (or a minimal fallback) instead of the raw payload.dictionaryPayload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +60
if ([object isKindOfClass:[NSNumber class]]) {
double value = [(NSNumber *)object doubleValue];
if (isnan(value) || isinf(value)) { return @0; } // NaN/Infinity are not valid JSON
return object;
}
Comment on lines +70 to +75
[(NSDictionary *)object enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
NSString *stringKey = [key isKindOfClass:[NSString class]] ? RNVoipSafeString((NSString *)key) : RNVoipSafeString([key description]);
if (stringKey.length > 0) {
result[stringKey] = RNVoipSanitizeJSONObject(value);
}
}];
Comment on lines +98 to +99
NSLog(@"[RNVoipPushNotification] PushKit payload not JSON-serializable after sanitize; forwarding minimal fallback.");
NSDictionary *raw = [rawPayload isKindOfClass:[NSDictionary class]] ? rawPayload : @{};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants