Runtime crash due to asynchronous Promise resolution in Android/Kotlin classes
What's happening?
Crash happen with the following exception "terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?". The issue is reproducible in my team's app. It is also appears in Nitro example app by letting it running idle (perhaps garbage collection process will run in background).
Reproduceable Code
The test is added in PR reproduce the runtime error. Technically, it happens for any async Promise resolution, followed by GC in JavaScript
createTest(
'Async Promise resolution does not cause a garbage-collection crash',
async () =>
(
// This test demonstrates a crashing bug seen in Nitro modules 0.25.2, only on
// Android (Kotlin). Run this test individually (not in "Run All Tests") after a
// fresh launch of NitroExample app for best repeatability of the crash. If
// repeated runs of this test do not reproduce a crash, you may need to increase
// memory use (or decrease available memory) to encourage garbage collection.
// The crash logs this message:
// "terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?"
// from the "hades" thread, which is used for garbage collection in the Hermes JS
// engine. The crash results from execution of the C++ Promise destructor on a thread
// that's not attached to the JVM. The origin is noted in the native backtrace as:
// "(margelo::nitro::Promise<double>::~Promise()+96)"
await it(async () => {
return timeoutedPromise(async (complete) => {
// This section creates a Nitro Promise that resolves asynchronously. The crash
// seen on Android/Kotlin in Nitro modules 0.25.2 requires that this is resolved
// after a delay, and that garbage collection happens after it is resolved.
const asyncPromise = testObject.callbackAsyncPromise(() => new Promise((resolve) =>
setTimeout(() => {
resolve(13)
}, 1_000)
));
// This section only exists to exercise the JS garbage collector, by creating
// large strings that temporarily consume enough memory to trigger GC. Values
// can be adjusted to increase memory use.
const garbage: Array<string> = []
let countdown = 200
let resolveGarbagePromise = () => {}
const garbagePromise = new Promise<void>(resolve => {
resolveGarbagePromise = resolve
})
let interval = setInterval(() => {
if (countdown-- > 0) {
if (countdown % 10 == 0) {
garbage.length = 0
} else {
garbage.push("x".repeat(2_000_000))
}
} else {
clearInterval(interval)
resolveGarbagePromise()
}
}, 50)
// This section waits on the async things to complete before passing the test.
// As of Nitro modules 0.25.2, this test should succeed for Swift and C++, and
// and it should frequently crash the Android app for Kotlin.
const result = await asyncPromise
await garbagePromise
complete(result)
}, 20_000)
})
)
.didNotThrow()
.equals(13)
Relevant log output
2025-05-09 12:12:53.770 10740-10766 libc++abi com.margelo.nitroexample E terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?
2025-05-09 12:12:53.770 10740-10766 libc com.margelo.nitroexample A Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 10766 (hades), pid 10740 (lo.nitroexample)
2025-05-09 12:12:53.806 10794-10794 crash_dump64 pid-10794 I obtaining output fd from tombstoned, type: kDebuggerdTombstoneProto
2025-05-09 12:12:53.808 197-197 tombstoned tombstoned I received crash request for pid 10766
2025-05-09 12:12:53.808 10794-10794 crash_dump64 pid-10794 I performing dump of process 10740 (target tid = 10766)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:13/TE1A.240213.009/12342917:user/release-keys'
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Revision: '0'
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A ABI: 'arm64'
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Timestamp: 2025-05-09 12:12:53.812017936-0700
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Process uptime: 16s
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Cmdline: com.margelo.nitroexample
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A pid: 10740, tid: 10766, name: hades >>> com.margelo.nitroexample <<<
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A uid: 10175
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A Abort message: 'terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?'
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x0 0000000000000000 x1 0000000000002a0e x2 0000000000000006 x3 000000726b40e620
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x4 646277641f736766 x5 646277641f736766 x6 646277641f736766 x7 7f7f7f7f7f7f7f7f
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x8 00000000000000f0 x9 00000075a1baea00 x10 0000000000000001 x11 00000075a1becde4
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x12 00000075ae737020 x13 000000007fffffff x14 0000000000333b90 x15 000011b106f9f6df
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x16 00000075a1c51d58 x17 00000075a1c2ec70 x18 000000726aae8000 x19 00000000000029f4
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x20 0000000000002a0e x21 00000000ffffffff x22 000000726b40e750 x23 000000726b40e790
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x24 000000726b40e840 x25 00000004f83f88c8 x26 0000000000000030 x27 0000000000000638
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A x28 00000004f8400000 x29 000000726b40e6a0
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A lr 00000075a1bde968 sp 000000726b40e600 pc 00000075a1bde994 pst 0000000000001000
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A backtrace:
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #00 pc 0000000000051994 /apex/com.android.runtime/lib64/bionic/libc.so (abort+164) (BuildId: 4e07915368c859b1910c68c84a8de75f)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #01 pc 00000000000a0210 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libc++_shared.so (offset 0x70c000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #02 pc 000000000009eeb0 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libc++_shared.so (offset 0x70c000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #03 pc 000000000009f374 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libc++_shared.so (offset 0x70c000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #04 pc 000000000009f314 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libc++_shared.so (offset 0x70c000) (std::terminate()+56) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #05 pc 000000000025c2a8 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #06 pc 0000000000283968 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #07 pc 0000000000313ce8 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (margelo::nitro::Promise<margelo::nitro::image::Person>::~Promise()+96) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #08 pc 0000000000313bb4 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #09 pc 0000000000314fec /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #10 pc 00000000003149dc /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libNitroImage.so (offset 0x1b8000) (BuildId: d081aff7f0dbf08942e19340f69dba7533485e6d)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #11 pc 00000000000ba748 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #12 pc 0000000000168a70 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #13 pc 000000000016a45c /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #14 pc 000000000016f3f8 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #15 pc 000000000016e518 /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #16 pc 000000000016e3dc /data/app/~~Ii46yVgEXLzQEpLHNMKpEQ==/com.margelo.nitroexample-6r1jzf7RijwR2suKifbQbA==/base.apk!libhermes.so (offset 0x874000) (BuildId: a6f7855bb11897bd23c29123dc102e3f0f9b55b7)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #17 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 4e07915368c859b1910c68c84a8de75f)
2025-05-09 12:12:53.970 10794-10794 DEBUG pid-10794 A #18 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 4e07915368c859b1910c68c84a8de75f)
2025-05-09 12:12:53.980 197-197 tombstoned tombstoned E Tombstone written to: tombstone_03
From my team's investigation using the above base addresses and the virtual addresses from the call stack and the name of the thread we can see that:
- Based on the thread name 'hades' and looking up the Hermes code base, that thread is a GC thread
- frame 10 confirms that Hermes was finalizing host function context: facebook::hermes::HermesRuntimeImpl::HFContext::finalize
- frame 9 confirm that it was park of the GC cylce: hermes::vm::HadesGC::OldGen::sweepNext Looking at frame 5 we can tell that Nitros promise object that is JSI bound is being destroyed
From the above observations, what happens is that Herme's GC finalizes JSI bound Nitro promise for return output which is still pending (not resolved or rejected). That promise also holds resources to invoke Kotlin/Java's callback (reject/resolve callbacks).
When those resources are attempted to be freed as part of the promise destruction it crashes the app because those are JNI bound resources attempted to be freed on non JNI attached thread ('hades' thread is not attached to the JNI).
Device
Android 14 device and emulator
Nitro Modules Version
0.25.1
Nitrogen Version
0.25.1
Can you reproduce this issue in the Nitro Example app here?
Yes, I can reproduce the same issue in the Example app here: https://github.com/mrousavy/nitro/pull/664
Additional information
- [x] I am using Expo
- [x] I am using Nitrogen (nitro-codegen)
- [x] I have read and followed the Troubleshooting Guide.
- [x] I created a reproduction PR to reproduce this issue here in the nitro repo. (See Contributing for more information)
- [x] I searched for similar issues in this repository and found none.
Hey thanks for the detailed bug report! Can you try to patch this fix into Nitro Core to see if that fixes your bug? https://github.com/mrousavy/nitro/commit/0589b256332c7d08370546f1652a0cd1cc467673
Thanks @mrousavy . I have tried with the latest code from main branch in a new PR https://github.com/mrousavy/nitro/pull/673 When click on running the test in example app "Async Promise resolution does does not cause a garbage-collection crash", it still crash the app. Here is the full stack trace
2025-05-23 12:39:48.005 16595-16648 libc++abi com.margelo.nitroexample E terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?
2025-05-23 12:39:48.005 16595-16648 libc com.margelo.nitroexample A Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 16648 (hades), pid 16595 (lo.nitroexample)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.036.A4/12096271:user/release-keys'
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Revision: '0'
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A ABI: 'arm64'
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Timestamp: 2025-05-23 12:39:48.063167455-0400
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Process uptime: 112s
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Cmdline: com.margelo.nitroexample
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A pid: 16595, tid: 16648, name: hades >>> com.margelo.nitroexample <<<
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A uid: 10191
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A Abort message: 'terminating due to uncaught exception of type std::runtime_error: Unable to retrieve jni environment. Is the thread attached?'
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x0 0000000000000000 x1 0000000000004108 x2 0000000000000006 x3 000000784a2ff640
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x4 646277641f736766 x5 646277641f736766 x6 646277641f736766 x7 7f7f7f7f7f7f7f7f
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x8 00000000000000f0 x9 0000007afacb4090 x10 0000000000000001 x11 0000007afad07058
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x12 000000006830a4d4 x13 000000007fffffff x14 00000000001b0822 x15 0000005770d5e0d9
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x16 0000007afad74d08 x17 0000007afad48e90 x18 0000007849324000 x19 00000000000040d3
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x20 0000000000004108 x21 00000000ffffffff x22 000000784a2ff770 x23 000000784a2ff7b0
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x24 000000784a2ff860 x25 0000000000000006 x26 00000035fcc00000 x27 b400007b2c161190
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A x28 0000000000000130 x29 000000784a2ff6c0
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A lr 0000007afacf89b8 sp 000000784a2ff620 pc 0000007afacf89e4 pst 0000000000001000
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A 19 total frames
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A backtrace:
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #00 pc 00000000000669e4 /apex/com.android.runtime/lib64/bionic/libc.so (abort+164) (BuildId: a87908b48b368e6282bcc9f34bcfc28c)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #01 pc 00000000000a0210 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libc++_shared.so (offset 0xd24000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #02 pc 000000000009eeb0 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libc++_shared.so (offset 0xd24000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #03 pc 000000000009f374 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libc++_shared.so (offset 0xd24000) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #04 pc 000000000009f314 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libc++_shared.so (offset 0xd24000) (std::terminate()+56) (BuildId: 6783a7f3a0d9c67c55a74cadc441ed55aaa493da)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #05 pc 0000000000274158 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #06 pc 000000000029622c /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (facebook::jni::base_owned_ref<facebook::jni::detail::JTypeFor<facebook::jni::HybridClass<margelo::nitro::JPromise, facebook::jni::detail::BaseHybridClass>::JavaPart, facebook::jni::JObject, void>::_javaobject*, facebook::jni::GlobalReferenceAllocator>::reset(facebook::jni::detail::JTypeFor<facebook::jni::HybridClass<margelo::nitro::JPromise, facebook::jni::detail::BaseHybridClass>::JavaPart, facebook::jni::JObject, void>::_javaobject*)+248) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #07 pc 0000000000297818 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #08 pc 0000000000288b84 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (margelo::nitro::Promise<double>::~Promise()+96) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #09 pc 0000000000288a50 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #10 pc 000000000030b190 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #11 pc 00000000002dae1c /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libNitroImage.so (offset 0x5ec000) (BuildId: 33fab5f1f5c457e2716b7192e261a52423f7dbb8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #12 pc 00000000000eae58 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libhermes.so (offset 0xe8c000) (BuildId: 84c243bb28f42eda0d52512da73b6dc33714c1f8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #13 pc 00000000002450dc /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libhermes.so (offset 0xe8c000) (BuildId: 84c243bb28f42eda0d52512da73b6dc33714c1f8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #14 pc 000000000024dad4 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libhermes.so (offset 0xe8c000) (BuildId: 84c243bb28f42eda0d52512da73b6dc33714c1f8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #15 pc 000000000024cce0 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libhermes.so (offset 0xe8c000) (BuildId: 84c243bb28f42eda0d52512da73b6dc33714c1f8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #16 pc 000000000024cab0 /data/app/~~95rW28Bv2IyfHwDJxLJStw==/com.margelo.nitroexample-Ak-ivVjCfEsBmygusAAIIQ==/base.apk!libhermes.so (offset 0xe8c000) (BuildId: 84c243bb28f42eda0d52512da73b6dc33714c1f8)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #17 pc 00000000000cb6a8 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: a87908b48b368e6282bcc9f34bcfc28c)
2025-05-23 12:39:48.250 16764-16764 DEBUG pid-16764 A #18 pc 000000000006821c /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: a87908b48b368e6282bcc9f34bcfc28c)
2025-05-23 12:39:48.259 206-206 tombstoned tombstoned E Tombstone written to: tombstone_01
Good day, I have picked interest in this git issues and I would like to solve it since I have experience with kotlin and react native
FYI, I attempted to update the de-constructor logic of JPromise.hpp in https://github.com/mrousavy/nitro/commit/84580e6630e0a9c13f3a780ce34ea0fd6016a7f3 but it doesn't fix this issue yet.
@dantrongamz your fix in ~JPromise looks like it should solve the issue - are you sure it still crashes with the same?
Hmm it's crashing here
Okay 40 minutes later I found the bug.
The problem is that the ~Promise() destructor: https://github.com/mrousavy/nitro/blob/2ae634c81f570a098ae0a393d970e448a339d0d2/packages/react-native-nitro-modules/cpp/core/Promise.hpp#L36-L41
Is obviously destroying it's fields: https://github.com/mrousavy/nitro/blob/2ae634c81f570a098ae0a393d970e448a339d0d2/packages/react-native-nitro-modules/cpp/core/Promise.hpp#L248-L252
And one of those fields contains a JNI value. To be more explicit, it's one of the OnResolvedFunc inside the _onResolvedListeners; https://github.com/mrousavy/nitro/blob/2ae634c81f570a098ae0a393d970e448a339d0d2/packages/react-native-nitro-modules/cpp/core/Promise.hpp#L251
Because if we take a look at the usage of Promise in JFunc_std__shared_ptr_Promise_double__ , we can see that the function passed to addOnResolvedListener(...) captures __promise - a JNI value: https://github.com/mrousavy/nitro/blob/2ae634c81f570a098ae0a393d970e448a339d0d2/packages/react-native-nitro-image/nitrogen/generated/android/c%2B%2B/JFunc_std__shared_ptr_Promise_double__.hpp#L65-L77
... So this call here: https://github.com/mrousavy/nitro/blob/2ae634c81f570a098ae0a393d970e448a339d0d2/packages/react-native-nitro-image/nitrogen/generated/android/c%2B%2B/JFunc_std__shared_ptr_Promise_double__.hpp#L69-L71
Captures __promise by value, and after it already completed, the function here is the last strong reference to __promise. Once the holder Promise gets destroyed, so does this function, and so do all of it's captured members - aka __promise.
Since this is happening from JS' GC on a Hades Thread, it's not really possible to keep this deterministic. By design, obviously. I'll think of something.
PR is up: https://github.com/mrousavy/nitro/pull/683
Thanks a bunch for this tricky fix! Our team has been eagerly waiting!
Thanks a bunch for this tricky fix!
Sure thing! Was a fun one.
Our team has been waiting for it for ages.
Next time just contact us through my agency or do a GitHub sponsorship - I only do open-source if it's in my own interest too. If someone has a bug or feature I don't experience myself, I usually don't work on it unless I'm paid to.
I'm seeing a similar error with the latest version, but I can't seem to repro by using the example. The stacktrace references this callback EventHandle:
export interface EventHandle {
remove(): void;
}
....
subscribe(listener: () => void): EventHandle;
I tried to reproduce it on this branch to no avail https://github.com/mrousavy/nitro/compare/main...joprice:nitro:subscribe#diff-f638d88bea117f8133046adda5f40a5ffc7edb6939e83f436749df48b62b1e1dR318
~~Odd thing is, in my own repo where it's failing, I commented out all usage of the EventHandle type and it still fails after a few reloads of react refresh.~~
I'm able to consistently reproduce this (but still only in my project) with an effect that has a teardown:
useEffect(() => {
const sub = Shake.addListener(() => {
...
});
return () => {
sub.remove();
};
}, []);
If I remove the cleanup function, it doesn't crash.
Here's the stack: Looks like it's deallocating the global ref and the jni is not available. I tried sprinkling in some jni::ThreadScope ts; as I saw in some other issues, but the jni doesn't seem to ever be available. Perhaps a custom function needs to be used there so the jni can be grabbed in the destructor in stead of an anonymous lambda?
abort 0x000000732b756134
[Inlined] facebook::jni::GlobalReferenceAllocator::verifyReference(_jobject *) const ReferenceAllocators-inl.h:104
[Inlined] facebook::jni::GlobalReferenceAllocator::deleteReference(_jobject *) const ReferenceAllocators-inl.h:97
facebook::jni::base_owned_ref::reset(facebook::jni::detail::JTypeFor<…>::_javaobject *) References-inl.h:293
[Inlined] facebook::jni::base_owned_ref::reset() References-inl.h:286
[Inlined] facebook::jni::base_owned_ref::~base_owned_ref() References-inl.h:272
[Inlined] margelo::nitro::audioplayer::JEventHandle::toCpp() const::'lambda'()::operator()() const::'lambda'()::~() JEventHandle.hpp:49
[Inlined] margelo::nitro::JSIConverter<std::__ndk1::function<void ()>, void>::toJSI(facebook::jsi::Runtime&, std::__ndk1::function<void ()>&&)::'lambda'(facebook::jsi::Runtime&, facebook::jsi::Value const&, facebook::jsi::Value const*, unsigned long)::~() JSIConverter+Function.hpp:51
[Inlined] std::__ndk1::function::~function() Function.h:969
[Inlined] std::__ndk1::__invoke[abi:nn180000]<…>($_0 &) Invoke.h:344
[Inlined] std::__ndk1::__invoke_void_return_wrapper::__call[abi:nn180000]<…>($_0 &) Invoke.h:419
[Inlined] std::__ndk1::__function::__alloc_func::operator()[abi:nn180000]() Function.h:166
std::__ndk1::__function::__func::operator()() Function.h:308
[Inlined] std::__ndk1::__function::__value_func::operator()[abi:nn180000]() const Function.h:425
[Inlined] std::__ndk1::function::operator()() const Function.h:978
[Inlined] decltype(std::declval<hermes::vm::HadesGC::Executor::Executor()::'lambda'()>()()) std::__ndk1::__invoke[abi:nn180000]<hermes::vm::HadesGC::Executor::Executor()::'lambda'()>(hermes::vm::HadesGC::Executor::Executor()::'lambda'()&&) Invoke.h:344
[Inlined] void std::__ndk1::__thread_execute[abi:nn180000]<std::__ndk1::unique_ptr<std::__ndk1::__thread_struct, std::__ndk1::default_delete<std::__ndk1::__thread_struct>>, hermes::vm::HadesGC::Executor::Executor()::'lambda'()>(std::__ndk1::tuple<std::__ndk1::unique_ptr<std::__ndk1::__thread_struct, std::__ndk1::default_delete<std::__ndk1::__thread_struct>>, hermes::vm::HadesGC::Executor::Executor()::'lambda'()>&, std::__ndk1::__tuple_indices<...>) thread.h:190
std::__ndk1::__thread_proxy[abi:nn180000]<…>(void *) thread.h:199
I got it to stop crashing by wrapping it in a type that uses ThreadScope in the destructor:
template<typename T>
struct JniSafeUnsubscriber {
facebook::jni::global_ref<T> ref;
explicit JniSafeUnsubscriber(facebook::jni::global_ref<T> r)
: ref(std::move(r)) {}
~JniSafeUnsubscriber() {
if (ref) {
facebook::jni::ThreadScope ts;
ref.reset();
}
}
T *operator->() {
return ref.get();
}
T &operator*() {
return *ref;
}
explicit operator bool() const {
return static_cast<bool>(ref);
}
JniSafeUnsubscriber(const JniSafeUnsubscriber &) = delete;
JniSafeUnsubscriber &operator=(const JniSafeUnsubscriber &) = delete;
JniSafeUnsubscriber(JniSafeUnsubscriber &&) = default;
JniSafeUnsubscriber &operator=(JniSafeUnsubscriber &&) = default;
};
....
auto unsubscribeRef = jni::make_global(unsubscribe);
auto safeWrapper = std::make_shared<JniSafeUnsubscriber<JFunc_void::javaobject>>(
std::move(unsubscribeRef));
return [safeWrapper = std::move(safeWrapper)]() mutable -> void {
auto &originalRef = safeWrapper->ref;
return originalRef->invoke();
};
where would that code be? is there anything that Nitro currently does wrong? is there a place where Nitro should add a ThreadScope?
Yea this is in the generated cpp code for an interface with a callback defined in a nitro file so nitro would want to update its codegen to make sure any closure that captures a jni value is wapped in an wrapper that handles jni scope. I think other types like promise do this already. It’s just cpp closures which have an automatically generated destructor that will call the destructor of captured arguments. This is assuming ownership moves into that function. I’m not familiar with the jni library for cpp to know whether there’s some other approach with global refs like capturing by a weak ref and letting them be cleaned up automatically but I think there was a pr that explicitly introduced the global ref for similar reasons.
Ahh yea it's because the closure holds something that can be destroyed from an arbitrary thread (Hades GC)
How often did you actually see this error and can you reproduce it in your app in debug?
Ever 5th save or so. It was driving me crazy. I almost started rewriting the android target in kotlin haha but finally bit the bullet and spent two days getting to the bottom of it. I can reproduce it easily but I can’t seem to recreate it in the demo app in the nitro repo.
Fixed in https://github.com/mrousavy/nitro/pull/1052