[0.81] [iOS] Backport TurboModule NSException safe-handling fix for iOS 26#56694
[0.81] [iOS] Backport TurboModule NSException safe-handling fix for iOS 26#56694pangu25 wants to merge 1 commit intofacebook:0.81-stablefrom
Conversation
…acebook#56265) Summary: Pull Request resolved: facebook#56265 When an async void TurboModule method throws an NSException, performVoidMethodInvocation calls convertNSExceptionToJSError which accesses the Hermes JSI runtime from the native method call invoker thread. Since jsi::Runtime is not thread-safe, this causes heap corruption and EXC_BAD_ACCESS crashes across various hermes::vm::* functions. The sibling function performMethodInvocation was already fixed in D71619229 to re-throw the ObjC exception instead of converting to JSError when the call is async. This applies the same fix to performVoidMethodInvocation, which is always async. Related to SEV S641230 (4,550+ Hermes crashes in AMA iOS from OTA bundle 921191722). A JS change behind a QE/MC gate is triggering an NSException in a void TurboModule method for non-employee users, and this bug turns that into widespread memory corruption. This fix prevents the crash, but the triggering diff and throwing TurboModule still need to be identified separately. Matches upstream GitHub issue: https://github.com/facebook/hermes/issues/1957Commits affecting the React Native open source repository must have a changelog entry in the commit summary. Every React Native release has almost 1000 commits, and manually categorizing these commits is very time consuming. --- Changelog: [iOS][Fixed] - Fix Hermes crash when async void TurboModule method throws NSException by re-throwing instead of converting to JSError on wrong thread Reviewed By: javache Differential Revision: D98660782 fbshipit-source-id: bdedc769f17d9aec4156c45d0286c6c31ca006e4 (cherry picked from commit 4083a6f)
|
Hi @pangu25! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
…idMethodInvocation) Root cause: On iOS 26, async void TurboModule methods that throw NSExceptions call convertNSExceptionToJSError which accesses jsi::Runtime from the wrong thread (not thread-safe), causing heap corruption and SIGABRT. Fix: Re-throw the ObjC exception instead of converting to JSError. This matches the fix in performMethodInvocation (already fixed in PR #50193). Applied via eas-build-pre-install.sh hook (runs before pod install on EAS). References: - facebook/react-native#56694 - facebook/react-native#54859 - facebook/react-native#54859 (comment)
The eas-build-pre-install.sh patch was not applied because EAS reused the cached compiled binary (same UUID a240950e-9a9c-3b58-ab2f-5d220f141047 in builds 14, 16, and 17). This Config Plugin patches RCTTurboModule.mm during the Expo prebuild phase (withDangerousMod), which runs BEFORE CocoaPods and BEFORE compilation. This forces EAS to recompile the native code with the fix applied. Fix: In performVoidMethodInvocation @catch block, replace: throw convertNSExceptionToJSError(runtime, exception, ...); with: @throw exception; This prevents jsi::Runtime from being accessed from the wrong thread (not thread-safe), which caused heap corruption and SIGABRT on iOS 26. References: - facebook/react-native#56694 - facebook/react-native#54859
Summary
Backport of #56265 (commit
4083a6ff92b0ffd4f71166848f3d7ce36172fac8,"Fix Hermes crash from TurboModule void method NSException handling")
from
mainto0.81-stable.iOS 26 hardened TurboModule cold-start error handling. On the
unpatched 0.81 branch, ~66% of iPhones (per Apple's Feb 2026
adoption stats) crash at app launch with
EXC_BAD_ACCESS/SIGABRTinsideperformVoidMethodInvocationwhen any async voidTurboModule method throws an
NSException. The unsafe path callsconvertNSExceptionToJSErrorfrom the native method call invokerthread; since
jsi::Runtimeis not thread-safe, this corruptsHermes' heap.
The fix swaps the
throw convertNSExceptionToJSError(...)for aplain
@throw exception;, mirroring how the sibling sync functionperformMethodInvocationwas already fixed in #50193 (D71619229)back in March 2025. Since void methods are always async, there is
no JS caller to receive a converted JSError anyway — the original
JS call has already returned.
The cherry-pick from main applied cleanly (3-line single-file
change, no conflicts). This brings the fix to
0.81-stableso thehuge install base on RN 0.81.x (Expo SDK 54 and the ecosystem
pinned to it) can ship a fix without an SDK upgrade.
Linked issues
Test plan
(linked from Runtime Exceptions in TurboModule Void Methods Crash the App #53960). On unpatched 0.81.5 the "Void Function"
button crashes the app with
EXC_BAD_ACCESS/SIGABRTinsideperformVoidMethodInvocation100% of the time.expected RedBox / error log and the app keeps running.
code path (
performVoidMethodInvocation→convertNSExceptionToJSError→ Hermes heap corruption) and is resolved by this patch.
upstream fix, which has full coverage on
main(sibling testcoverage already lives there for
performMethodInvocation).Pick Request
This is a backport of a merged crash fix to a stable branch.
Marking as a pick request per RN backport conventions.