Repository navigation
SIGSEGV on JS thread's own event loop under rapid AudioBufferSourceNode + onEnded churn #1285
Description
Activity
This issue was filed with the help of Claude Code (Anthropic's AI coding assistant), which built and ran the MRE, captured the crash logs/backtraces, and drafted the report. The repro app, the crash finding, and the (ruled-out) unregisterHandler fix attempt were all verified live on-device before writing this up, per the template's request for AI-filed issues to create and verify the MRE first.
Some additional negative results from further digging tonight, since they meaningfully narrow the search even without a fix:
Ruled out: a generic
RuntimeScheduler_Modern/Hermes bug independent of this library. Added a control test to the repro app with the identical timing/interval/GC-pressure shape as the churn tests, but with zeroreact-native-audio-apicalls (noAudioContext, no nodes at all - just a tightawait delay(...)loop plus concurrent busy-work to force GC activity). Two full clean runs (6,000 iterations total) produced no crash. Soreact-native-audio-apigenuinely is necessary to trigger this, even though its frames never appear in the crash backtrace itself - the corruption must happen inside it somewhere, surfacing later.Ruled out:
setExternalMemoryPressureover-reporting.AudioBufferSourceNodeHostObject::setBuffer()reportsbufferHostObject->getSizeInBytes()as fresh external memory pressure on every newAudioBufferSourceNodeHostObject, even whengetOrCreateImmutableCopy()means the real backing memory is shared/cached rather than actually reallocated per node - with the repro's original 240s buffer (~84MB), that's a huge amount of reported-but-not-real pressure accumulating across thousands of churned nodes, which seemed like a very plausible way to push Hermes's GC heuristics into a bad state. Tested directly by shrinking the buffer to 5s (~1.76MB reported pressure per node, ~48x smaller) and reran the churn - crashed just as reliably, with the identical signature and backtrace. So the absolute pressure value isn't what's driving this either.Traced the onEnded wiring end-to-end (
AudioScheduledSourceNode.ts->AudioEventEmitter/AudioEventSubscription->AudioEventHandlerRegistryHostObject::addAudioEventListener/removeAudioEventListener) - confirmedAudioEventHandlerRegistry::registerHandler/unregisterHandlerare the only two places that ever touch theshared_ptr<jsi::Function>map, so there isn't another obvious cross-thread release in that specific code path I've missed.At this point I don't have a good way to narrow this further without either symbolicated Hermes internals (the prebuilt
libhermesvm.soshipped with this RN version is stripped -#00-#18of every capture are unresolved) or an ASan-instrumented build to catch the actual corrupting write directly. Both are a meaningfully bigger undertaking than tonight's investigation, so pausing here rather than continuing to guess - hopefully the ruled-out list above at least saves someone time.- addedbugSomething isn't working as expected or produces unexpected errorsSomething isn't working as expected or produces unexpected errors
on Sep 14, 2026 Ok, coming back with the explanation: the repro crashes with
--------- beginning of crash 2026-09-23 10:55:17.913 12892-13093 HermesGC com.audioapileakrepro I OOM: [RNBridgeless] reason = Max heap size was exceeded (1 from category: vm_allocate_category), numCollections = 1517, heapSize = 8388608, allocated = 4891976, va = 8388608, external = 86470180092. 2026-09-23 10:55:17.918 12892-13093 libc com.audioapileakrepro A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 13093 (mqt_v_js), pid 12892 (dioapileakrepro)which basically points to the fact that app crashed, because it used too much memory and heap has just ended. Looking at your code that stops the
AudioBufferSourceNodeconst stopAndDisconnectCurrent = () => { const current = currentRef.current; if (!current) return; try { current.source.stop(contextRef.current!.currentTime); } catch { // already stopped - fine. } current.source.disconnect(); current.gain.disconnect(); currentRef.current = null; };
you are not clearing up the event, call the
current.source.onEnded = nullbefore disconnecting the source.Adding that line allows the app to finish the 4000 cycle without killing the app.
The reason for it is that underlying event captures the node, which makes the node without event unregistered undestructible. Thank you for pointing out this issue, we need to make up the mind internally, but it probably would be the good idea to unregister the event under the hood when calling
disconnecton the node, it would make sense.- added a commit that references this issue
on Sep 23, 2026 I see that it is solved on your side, closing.
Yeah, sorry, just forgot to follow it up here, I appreciate your help!
Reacted by Michał Dydek
Description
A native
SIGSEGV(fault addr0x0) crashes the whole app on the JS thread itself (mqt_v_js), reproducible under rapidAudioBufferSourceNodecreation/teardown churn where each node has a realonEndedlistener attached. The crash happens inside the JS thread's own event loop / microtask checkpoint machinery - the backtrace shows noreact-native-audio-apiframes at all, which suggests the actual corruption happens earlier and the crash only surfaces later when Hermes touches the (already-corrupted) heap during its own bookkeeping.Expected: no crash, regardless of how quickly source nodes are churned.
Crash signature:
Backtrace (trimmed, full untrimmed logcat available on request):
i.e. the crash surfaces while the JS thread is running its own
RuntimeSchedulerevent loop, processing a runnable that was posted onto it via JNI (theCallInvoker/invokeAsyncpath).Also seen in production, not just synthetic churn: the same signature (
SIGSEGV,mqt_v_js, fault addr0x0) has occurred intermittently (~5 times across a multi-hour session) in a separate real app doing ordinary rapid track-switching during audio playback (React Native 0.85.0), so this isn't purely a synthetic-stress-test artifact - though the synthetic repro below is far more reliable for actually catching it.A ruled-out hypothesis, in case it saves someone time: given the crash's relationship to rapid source-node churn with an attached event listener, I suspected
AudioEventHandlerRegistry::unregisterHandler()releasing its lastshared_ptr<jsi::Function>reference off the JS thread - it's called from~EventCaller(), which runs on whatever thread destroys the owning node, andAudioGraphManager'snodeDestructor_(AudioDestructor) tears nodes down on a dedicated background thread rather than the JS thread. I patchedunregisterHandler()to extract the handler under the existing lock and post its actual release onto the JS thread viacallInvoker_->invokeAsync(...)instead of letting it destruct in place. This did not fix the crash - rebuilt with that patch applied (confirmed via.sotimestamps postdating the source edit), reran the identical repro, and got the same signature/backtrace shape. So either the corruption comes from somewhere else entirely, or there's another cross-thread release of a JSI object I haven't found (e.g.AudioEventHandlerRegistry's own destructoreventHandlers_.clear(), or something outside this file).I did create and verify the MRE below before filing this (built and ran it, reproduced the crash on both the unpatched library and with the ruled-out patch applied).
Steps to reproduce
npm install,npx react-native run-androidAudioBuffers, genuine node overlap instead of strict stop-then-create, background GC pressure)adb logcatforFatal signal 11- it's a race, not deterministic, so it may take a few runs (observed anywhere from the first run to a handful of runs) to landEach iteration: creates a fresh
AudioBufferSourceNode+GainNodepair wrapping anAudioBuffer(silent, prebuilt once), registers a realsource.onEnded = () => {}listener, starts it, waits a short interval, thenstop()s anddisconnect()s the previous pair.Snack or a link to a repository
https://github.com/WentTheFox/AudioApiLeakRepro
(A separate, smaller repo originally built for #1263 - this is a distinct bug from that one, unrelated to the
AudioBuffercopy-caching fix in #1281/#1283.)React Native Audio API version
0.13.3
React Native version
0.87.0 (repro app) / 0.85.0 (the separate production app mentioned above)
Platforms
Android
JavaScript runtime
Hermes
Workflow
React Native
Architecture
Fabric (New Architecture)
Build type
Debug app & dev bundle
Device
Real device
Device model
Samsung Galaxy S24+ (SM-S926B), Android 16
Acknowledgements
Yes