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.
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

1 participant