Repository navigation
SIGSEGV in ~CallbackWrapper when a TurboModule callback is destroyed off the JS thread during runtime teardown #59578
Description
Description
SyncCallback's destructor (and so AsyncCallback's, which owns one) can run on any thread. It calls wrapper_.lock(), so for a short time that thread holds a strong reference to the CallbackWrapper:
destroy() removes the collection's reference, so that local strong reference is always the last one, and ~CallbackWrapper runs ~jsi::Function on the background thread. If the runtime is torn down in that window (~TurboModuleBinding clears the LongLivedObjectCollection, then Hermes frees the memory behind the jsi::PointerValue), the result is a use-after-free in PointerValue::invalidate().
This breaks the contract in LongLivedObject.h: "ensure you only hold weak references to the object outside the JS thread to avoid accessing deallocated values when the JS VM is shutdown."
We see this in production on Android (React Native 0.84, Hermes V1, New Architecture / bridgeless). It is a native SIGSEGV on the fbjni HybridData destructor thread, right after the user closes a React Native screen and its ReactHost / runtime is destroyed. The code is the same on main (checked at 7225d43).
The race on Android:
- A TurboModule method takes a callback or a promise.
createJavaCallback(JavaTurboModule.cpp) wraps anAsyncCallback<>in aJCxxCallbackImpl. For promises, the callback that is not called (for examplerejectafterresolve) stays alive until the Java object is collected. LegacyReactContextBaseJavaModulemodules go through the same function in bridgeless mode. - The Java
Callbackobject is garbage collected. fbjni destroys the C++JCxxCallbackImplon itsHybridDatadestructor thread, which runs~AsyncCallback→~SyncCallback→wrapper_.lock()→destroy(). - At the same time, the React host thread destroys the instance:
~ReactInstance→~HermesRuntime→ finalizers →~TurboModuleBinding→LongLivedObjectCollection::clear(). Then Hermes deletes the chunks that hold thePointerValues. - The destructor thread releases its strong reference →
~CallbackWrapper→~jsi::Function→~Pointer→ptr_->invalidate()on freed memory.
Steps to reproduce
Use the Android reproducer app linked below (React Native 0.84.0 template, release build, no React Native patches):
- A native module receives 20,000 JS callbacks and keeps them without calling them.
- After 500 ms, the module drops them and forces GC, so fbjni destroys the
JCxxCallbackImpls on itsHybridDatadestructor thread. - At the same moment it calls
ReactHost.reload(), so the runtime is torn down while those destructors run. Busy threads (2 × CPU cores) keep the CPU loaded, so the destructor thread gets preempted. - The app repeats this about once per second and shows the cycle count.
On a Galaxy S26 Ultra (Android 17), stock 0.84.0 crashed in 4 of 7 runs of 150 s: 2 of 3 with the prebuilt AAR, 2 of 4 with React Native built from source. Each crash was a SIGSEGV on the HybridData Dest thread, 6 to 11 s after launch, with the stack below. With the fix from the linked PR (built from source), 0 of 8 runs crashed. Each of those runs completed 86 to 108 cycles in 150 s.
React Native Version
0.84.0 (also present on main at 7225d43)
Affected Platforms
Runtime - Android
Output of npx @react-native-community/cli info
System:
OS: macOS 27.0.1
CPU: (18) arm64 Apple M5 Max
Binaries:
Node: 22.16.0
npm: 10.9.2
Languages:
Java: 17.0.19
npmPackages:
"@react-native-community/cli": 20.1.0
react: 19.2.3
react-native: 0.84.0
Stacktrace or Logs
Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid ... (HybridData Dest)
facebook::jsi::Pointer::~Pointer() jsi/jsi.h:672 ptr_->invalidate()
facebook::jsi::Object::~Object() jsi/jsi.h
facebook::jsi::Function::~Function() jsi/jsi.h
facebook::react::CallbackWrapper::~CallbackWrapper() react/bridging/CallbackWrapper.h:21 (implicit)
facebook::react::SyncCallback<void()>::~SyncCallback() react/bridging/Function.h:127 end of wrapper_.lock() scope
facebook::react::AsyncCallback<>::~AsyncCallback() react/bridging/Function.h:22
createJavaCallback(...) lambda destructor react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp:123
facebook::react::JCxxCallbackImpl::~JCxxCallbackImpl() ReactAndroid/src/main/jni/react/jni/JCallback.h:25
libfbjni.so
com.facebook.jni.HybridData$Destructor.destruct
com.facebook.jni.DestructorThread$1.run
This is the symbolized frame chain, innermost first, from the reproducer app. Frames #00 to #03 are in `libreactnative.so` (Build ID `7e30e03647a3148904c2390445561c90f979a562`, the prebuilt 0.84.0 AAR). We symbolized them with `ndk-stack` and the unstripped prefab copy of `libreactnative.so` from the same AAR. Inlined std frames are not shown. The production crash has the same chain.
MANDATORY Reproducer
https://github.com/l2hyunwoo/react-native-sync-callback-repro
Screenshots and Videos
repro-base.mp4
Stock 0.84.0 (prebuilt AAR): the process dies during cycle 65, 69 s after launch (SIGSEGV on HybridData Dest). The video ends at the crash.
repro-fix.mp4
With the fix (built from source): about 120 cycles over 170 s without a crash.
Activity
react-native-bot commentedon Oct 28, 2026
Warning
Unsupported version: It looks like your issue or the example you provided uses an unsupported version of React Native.
Due to the number of issues we receive, we're currently only accepting new issues against one of the supported versions. Please upgrade to latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If you cannot upgrade, please open your issue on StackOverflow to get further community support.
react-native-bot commentedon Oct 28, 2026
Warning
Unsupported version: It looks like your issue or the example you provided uses an unsupported version of React Native.
Due to the number of issues we receive, we're currently only accepting new issues against one of the supported versions. Please upgrade to latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If you cannot upgrade, please open your issue on StackOverflow to get further community support.