fix: prevent EXC_BAD_ACCESS in Promise destructor after runtime teardown#3744
fix: prevent EXC_BAD_ACCESS in Promise destructor after runtime teardown#3744tbrushwyler wants to merge 3 commits intoShopify:mainfrom
Conversation
|
I have signed the CLA! |
wcandillon
left a comment
There was a problem hiding this comment.
This would cause a memory leak which maybe is ok because this situation can only happen in DEV mode or during OTA. Still I think there is a proper way to do it, I can give it a shot if you want.
It also only protect the destructor not the reject/resolve itself.
…ardown Replace the memory-leak workaround (heap-leaking JSI Function pointers) with proper lifecycle management via RuntimeLifecycleMonitor. The Promise now registers as a listener and releases its JSI objects in onRuntimeDestroyed while the runtime is still valid. - Register/unregister with RuntimeLifecycleMonitor in constructor/destructor - Guard resolve/reject with mutex and runtimeDestroyed_ flag - Release JSI Functions in onRuntimeDestroyed before the runtime is torn down - Call removeListener outside the lock to keep locking hierarchy simple - Add debug-only logging for dropped resolve/reject calls during teardown
0ee69de to
5d1d3bf
Compare
Good catch - I've amended the commit to fix the memory leak with better cleanup and to make resolve() & reject() no-ops if the runtime is gone. Feel free to adjust if you see a better way to do it. |
wcandillon
left a comment
There was a problem hiding this comment.
Thank you for looking into this and I like it a lot. Added a small comment for onRuntimeDestroyed and ~Promise.
I'm always struggling to think through these use-cases. One approach I am thinking about (not sure if it's good or not) would be to use jsi::Runtime* as a key in the RuntimeLifecycleMonitor so we can safely check if it's there or not.
| } | ||
| } | ||
|
|
||
| void JsiPromises::Promise::onRuntimeDestroyed(jsi::Runtime *) { |
There was a problem hiding this comment.
Here we do not check if the destroyed runtime is the one we hold a reference to? We should add a check there no?
|
|
||
| JsiPromises::Promise::~Promise() { | ||
| bool shouldRemove = false; | ||
| { |
There was a problem hiding this comment.
That's look elegant to me. And it think this is safe if we set runtimeDestroyed_ = true; if the runtime ref match the one Promise hold.
|
@tbrushwyler do you have tips on how to reproduce the issue? I run a jsi promise that settles if the runtime gets destroyed: But doing hot reload while running this function doesn't seem to cause a crash. In the debugger I have a breakpoint on _promise->resolve that gets hit when doing hot reload but I am not having any crash. |
Only set runtimeDestroyed_ and release JSI functions when the destroyed runtime matches the one this Promise holds a reference to.
|
@wcandillon here's the stack trace for the crash that this change fixes: This is an intermittent issue that we see, exacerbated by the way that we're creating individual activities/view controllers that host a RN bundle. We see the crash when we navigate away from one of these RN views before its Skia promises have all completed. |
|
I will still try to reproduce the issue if I can. Now on your side this patch is fixing the issue for you? Do you have a small example project to test it? |
|
@wcandillon I do not have a minimal repro, but this does fix the issue for me. |
Summary
Fixes a crash (
EXC_BAD_ACCESS/ null pointer dereference injsi::Pointer::~Pointer()) that occurs when aJsiPromises::Promiseis destroyed after the JSI runtime has been torn down.This commonly happens when async operations like
Skia.Data.fromURI()(used byuseImage,useFont,useSVG,useImageAsTexture) complete after the React Native bridge has shut down — for example, when a user navigates away from a screen while images are still loading, or when the app reloads.Stack trace
Root cause
Promiseholdsjsi::Functionmembers (resolve_,reject_) whose destructors calljsi::Pointer::~Pointer(), which accesses the JSI runtime to release the underlyingPointerValue. When the runtime has already been torn down, this dereferences an invalid pointer.Fix
Add an explicit destructor that
std::moves thejsi::Functionmembers to the heap before implicit member destruction runs. After the move, the member's internalptr_is null (perjsi::Pointer's move constructor:other.ptr_ = nullptr), so implicit destruction safely skips the runtime access. The heap copies are intentionally leaked (~32 bytes per Promise) since there is no safe way to release JSI pointers without a valid runtime.Test plan
yarn tscpassesyarn lintpassesyarn testpasses (for@shopify/react-native-skia)useImage)