You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] SIGSEGV in Hermes during audio data delivery to JS (Android and iOS) #436
On Android, running TTS generation while the CDP/Hermes inspector is attached reliably aborts the app with a null-pointer SIGSEGV on the mqt_v_js thread. No sherpa or app frames appear in the stack — it is entirely inside Hermes' inspector agent.
Environment
apps/sherpa-voice, development build
Pixel 6a (physical), Android 17 / API 37, arm64
Model: vits-icefall-en-low (VITS, 22050 Hz)
Attached via scripts/agentic/app-state.sh (CDP)
Repro
Launch apps/sherpa-voice on a physical device with Metro on :7500
Run any TTS generation, e.g. __AGENTIC__.testTTSFull()
~2s after generation begins, the process dies
Crash
F/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
in tid 6427 (mqt_v_js)
Cause: null pointer dereference
#00 pc 000000000036dbc8 libhermesvm.so
#01 pc 000000000036d8ec libhermesvm.so
...
#12 pc 00000000000703b4 libhermestooling.so
(facebook::react::jsinspector_modern::HermesRuntimeAgentDelegate::Impl::Impl(...))
Not TTS-specific
Reproduced on both the playAudio: true and the unmodified playAudio: false
code paths, with the identical signature (#00 pc 036dbc8, same thread). TTS
init and generation themselves succeed — the native work completes and audio
plays before the inspector thread dies. The trigger appears to be long-running
blocking native work on the JS thread while the inspector is attached, not TTS
logic.
Impact
Agentic/CDP validation of any long-running native call on Android is unreliable:
the app dies mid-run and the CDP session is lost before results can be polled.
Not believed to affect production builds (no inspector attached), but it blocks
the on-device feedback loop described in AGENT_STARTING_TEMPLATE.md.
No android.audioSource, no compression, default intervals. Native recording starts cleanly — the loop is entered and the buffer allocated — and the process dies ~90ms later, as soon as data starts crossing into JS:
D AudioStudio:AudioRecorderManager: Entering recording loop
D AudioStudio:AudioRecorderManager: - Buffer size: 1792 bytes
F libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 5463 (mqt_v_js)
The backtrace is entirely Hermes, unwinding into the RN event loop, with no audio frames and no Java-level exception:
Consistent across 5 runs with identical PC offsets. Raising interval/intervalAnalysis to 60000 and setting enableProcessing: false did not avoid it. Always on mqt_v_js, always fault addr 0x0 (read).
Practical impact: any CDP-driven recording validation dies within a second of starting, so the agentic loop cannot verify recording behaviour on Android. That is how this surfaced — while validating #428, where the same crash occurs with and without the feature's config, which is what rules the feature out as the cause.
Correcting this: the crash is not caused by recording, and it does not block device validation. I claimed it did for an entire session and that was wrong.
What actually triggers it
A CDP evaluation held open while data crosses into JS. Not recording itself.
Measured on Pixel 6a (Android 16), with extractMelSpectrogram:
14:38:44.867 MelSpectrogramJNI: compute done: timeSteps=490, nMels=40 <- native succeeds
14:38:45.059 F libc: Fatal signal 11 (SIGSEGV) ... tid 12407 (mqt_v_js) <- 192ms later, in JS delivery
Native computation completes. The crash lands 192ms afterwards on mqt_v_js, delivering the result while the inspector is attached.
The same run with no CDP evaluation in flight during delivery: same PID before and after, 0 crashes, compute done: timeSteps=490, nMels=40, and the result readable afterwards.
Workaround
Fire and poll. Never hold an eval open across the data handoff:
# WRONG — eval still open when the first buffer lands
app-state.sh eval"__AGENTIC__.startRecording({...}).then(r => globalThis.x = r)"# RIGHT — schedule, let the eval return, poll separately
app-state.sh eval"(() => { setTimeout(() => { __AGENTIC__.startRecording({...}) }, 1500); return 'scheduled' })()"
sleep 6
app-state.sh state
Verified with this pattern on Pixel 6a: 39s recording, 3.48 MB WAV, 0 crashes. Also trimAudio, extractAudioData, extractPreview and extractMelSpectrogram all completed with results.
Still a real bug
A null-pointer dereference in Hermes during RuntimeScheduler_Modern::performMicrotaskCheckpoint is worth fixing — it makes the inspector unusable during data-heavy operations. But it is a debugging-ergonomics problem, not a validation blocker, and I should not have treated it as one.
The workaround is now documented in CLAUDE.md alongside the hard rule that library changes require device validation before merge.
changed the title [-]Android: SIGSEGV in libhermesvm during TTS generation with CDP inspector attached[/-][+]SIGSEGV in Hermes when a CDP evaluation is open during data delivery (Android and iOS)[/+]on Aug 21, 2026
This is not Android-only. The same crash occurs on the iOS simulator, from a report during trim validation:
Process: AudioDevPlayground [58421]
Version: 2.3.2 (1)
Triggered by Thread: 5 com.facebook.react.runtime.JavaScript
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Same signature as Android: null-pointer dereference on the JS thread. Android's version faults on mqt_v_js inside libhermesvm.so under RuntimeScheduler_Modern::performMicrotaskCheckpoint; iOS faults on com.facebook.react.runtime.JavaScript. Both while a CDP evaluation is open as data crosses into JS.
Retitled accordingly — the original title said "during TTS generation", which is also too narrow. Observed triggers so far:
operation
platform
TTS generation
Android
audio recording (~90ms in)
Android
extractMelSpectrogram result delivery
Android
trimAudio validation
iOS simulator
The common factor is an open evaluation during delivery, not any particular API.
Timing evidence
Android, mel spectrogram — native work completes, then the crash lands during handoff:
14:38:44.867 MelSpectrogramJNI: compute done: timeSteps=490, nMels=40
14:38:45.059 F libc: Fatal signal 11 (SIGSEGV) ... tid (mqt_v_js)
Android, TTS — 3.2s after stopTts fires, well clear of the native work:
16:37:55.268 SherpaOnnxTTS: Stopping TTS generation
16:37:58.470 F libc: Fatal signal 11 (SIGSEGV) ... tid (mqt_v_js)
Workaround holds on both
Fire-and-store — schedule the call so the eval returns before data flows, then poll separately — avoids it. Verified on Pixel 6a (39s recording, 3.48 MB WAV, 0 crashes) and used for all iOS trim validation.
Documented in CLAUDE.md; #460 extends that to every recording recipe in the repo, including an executable benchmark script that would otherwise crash the app it measures.
New reproduction on Android, and it narrows the trigger.
Running record-max-duration-validation.json through validate-recipe.js on a Pixel 6a crashes the app reproducibly (3 for 3), at the stop step, even though every recording call in that recipe is now fire-and-store rather than a held-open eval_async:
08-21 17:18:52.738 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR),
fault addr 0x0 in tid 29397 (mqt_v_js), pid 29327
frames: base.apk!libhermesvm.so ... base.apk!libreactnative.so
Same signature as before: mqt_v_js, libhermesvm.so, null read.
What is new is the contrast with a manual run. These do not crash, repeated several times each:
20 getState() evals at ~250ms during a live 6-second recording. Same pid throughout; recording completed (527476 bytes, 5979ms).
12 getState() evals during a notify-only recording (output.primary.enabled: false, intervalAnalysis: 1000). Same pid; ran past 30s.
The same notify-only config with no polling at all.
So plain polling is not sufficient to trigger it, which contradicts the "any concurrent evaluation" reading. The recipe runner does something the manual path does not, and the crash lands at stop rather than at start.
Practical effect on the earlier workaround: fire-and-store avoids the crash at start and lets the recipe get much further than it used to (wait-notify-only-event and assert-notify-only-still-recording now pass on device, maxDurationReached: true, durationMs: 2183), but it is not a complete workaround — stop is still reachable.
One incidental finding while tracing this: set-step-hud and clear-step-hud are not implemented in cdp-bridge.mjs at all, so validate-recipe.js emits HUD warning: ERROR: No device matching ... on every run regardless of whether the device is connected. That message is misleading during triage — it looks like a device-resolution failure when it is an unimplemented command. Worth either implementing or removing the calls.
changed the title [-]SIGSEGV in Hermes when a CDP evaluation is open during data delivery (Android and iOS)[/-][+]SIGSEGV in Hermes during audio data delivery to JS (Android and iOS)[/+]on Aug 21, 2026
Retitled: the old title said "when a CDP evaluation is open", and that mechanism is wrong.
cdp-bridge.mjs passes awaitPromise: false on every Runtime.evaluate, and eval-async is itself fire-and-store internally — it kicks off, returns 'started', and polls with separate evaluations. No CDP request is ever held open, so "an open evaluation" cannot be the trigger.
What is actually measured, on a Pixel 6a:
scenario
result
20 getState() evals at ~250ms during a live 6s recording
survives; 527476 bytes / 5979ms
12 evals during a notify-only recording
survives
notify-only recording, no polling at all
survives
an eval leaving a startRecording() promise outstanding as audio starts
dies in ~90ms
validate-recipe.js, all calls fire-and-store
dies at the stop step, 3/3
So polling is not the trigger, and fire-and-store is not a complete workaround.
The common thread in the crashing cases is an outstanding JS promise from a recording call at the moment audio crosses into JS — but the recipe case crashes at stop, where that framing fits less well, so I would not state a mechanism with confidence yet.
CLAUDE.md has been corrected to describe the measurements rather than the wrong theory. The previous wording caused a reviewer to flag safe polling as a bug, which is a good illustration of why the guess was worth removing.
Reproduced three consecutive times on 2026-08-22, Pixel 6a (Android 17 / API 37), always at the same node.
Recipe: teams/playground/recipes/record-max-duration-validation.json. The smoke flow passes 5/5, then the run dies at wait-notify-only-event — the first node that actually starts audio. Every recording call in the path is fire-and-store.
Zygote: Process 30208 exited due to signal 11 (Segmentation fault)
#02 pc 0000000000275510 base.apk!libhermesvm.so (offset 0x2c3c000)
#03 pc 00000000002750a4 base.apk!libhermesvm.so
#04 pc 000000000025a878 base.apk!libhermesvm.so
#05 pc 00000000002c6bc0 base.apk!libhermesvm.so
#06 pc 00000000000e44a4 base.apk!libhermesvm.so
ActivityManager: Scheduling restart of crashed service .../AudioRecordingService in 1000ms
AudioStudio: AudioRecordingService onCreate
Two things worth noting:
Fire-and-store does not avoid it here. The crash lands at the start of audio, not the stop, contradicting the earlier reading that only the stop step was reachable.
Also corrects a stale claim in CLAUDE.md, now fixed: the recipe path was described as known good after 5962eda's 12/12 run. That run was real, but the path is intermittent rather than fixed.
Activity
abretonc7s commentedon Aug 20, 2026
Reproduces on the recording path too, not just TTS — the title's scope is too narrow.
On a Pixel 6a (Android 16,
bluejay) with the CDP inspector attached, a plain recording is enough:No
android.audioSource, no compression, default intervals. Native recording starts cleanly — the loop is entered and the buffer allocated — and the process dies ~90ms later, as soon as data starts crossing into JS:The backtrace is entirely Hermes, unwinding into the RN event loop, with no audio frames and no Java-level exception:
Consistent across 5 runs with identical PC offsets. Raising
interval/intervalAnalysisto 60000 and settingenableProcessing: falsedid not avoid it. Always onmqt_v_js, alwaysfault addr 0x0 (read).Practical impact: any CDP-driven recording validation dies within a second of starting, so the agentic loop cannot verify recording behaviour on Android. That is how this surfaced — while validating #428, where the same crash occurs with and without the feature's config, which is what rules the feature out as the cause.
abretonc7s commentedon Aug 21, 2026
Correcting this: the crash is not caused by recording, and it does not block device validation. I claimed it did for an entire session and that was wrong.
What actually triggers it
A CDP evaluation held open while data crosses into JS. Not recording itself.
Measured on Pixel 6a (Android 16), with
extractMelSpectrogram:Native computation completes. The crash lands 192ms afterwards on
mqt_v_js, delivering the result while the inspector is attached.The same run with no CDP evaluation in flight during delivery: same PID before and after, 0 crashes,
compute done: timeSteps=490, nMels=40, and the result readable afterwards.Workaround
Fire and poll. Never hold an eval open across the data handoff:
Verified with this pattern on Pixel 6a: 39s recording, 3.48 MB WAV, 0 crashes. Also
trimAudio,extractAudioData,extractPreviewandextractMelSpectrogramall completed with results.Still a real bug
A null-pointer dereference in Hermes during
RuntimeScheduler_Modern::performMicrotaskCheckpointis worth fixing — it makes the inspector unusable during data-heavy operations. But it is a debugging-ergonomics problem, not a validation blocker, and I should not have treated it as one.The workaround is now documented in CLAUDE.md alongside the hard rule that library changes require device validation before merge.
docs: make the validation rule executable, and fix what running it ex…
fix(sherpa-onnx.rn/android): let stopTts interrupt an in-flight gener…
[-]Android: SIGSEGV in libhermesvm during TTS generation with CDP inspector attached[/-][+]SIGSEGV in Hermes when a CDP evaluation is open during data delivery (Android and iOS)[/+]abretonc7s commentedon Aug 21, 2026
This is not Android-only. The same crash occurs on the iOS simulator, from a report during trim validation:
Same signature as Android: null-pointer dereference on the JS thread. Android's version faults on
mqt_v_jsinsidelibhermesvm.sounderRuntimeScheduler_Modern::performMicrotaskCheckpoint; iOS faults oncom.facebook.react.runtime.JavaScript. Both while a CDP evaluation is open as data crosses into JS.Retitled accordingly — the original title said "during TTS generation", which is also too narrow. Observed triggers so far:
extractMelSpectrogramresult deliverytrimAudiovalidationThe common factor is an open evaluation during delivery, not any particular API.
Timing evidence
Android, mel spectrogram — native work completes, then the crash lands during handoff:
Android, TTS — 3.2s after
stopTtsfires, well clear of the native work:Workaround holds on both
Fire-and-store — schedule the call so the eval returns before data flows, then poll separately — avoids it. Verified on Pixel 6a (39s recording, 3.48 MB WAV, 0 crashes) and used for all iOS trim validation.
Documented in CLAUDE.md; #460 extends that to every recording recipe in the repo, including an executable benchmark script that would otherwise crash the app it measures.
fix(agentic): repair the validation tooling the rule points at
abretonc7s commentedon Aug 21, 2026
New reproduction on Android, and it narrows the trigger.
Running
record-max-duration-validation.jsonthroughvalidate-recipe.json a Pixel 6a crashes the app reproducibly (3 for 3), at the stop step, even though every recording call in that recipe is now fire-and-store rather than a held-openeval_async:Same signature as before:
mqt_v_js,libhermesvm.so, null read.What is new is the contrast with a manual run. These do not crash, repeated several times each:
getState()evals at ~250ms during a live 6-second recording. Same pid throughout; recording completed (527476 bytes, 5979ms).getState()evals during a notify-only recording (output.primary.enabled: false,intervalAnalysis: 1000). Same pid; ran past 30s.So plain polling is not sufficient to trigger it, which contradicts the "any concurrent evaluation" reading. The recipe runner does something the manual path does not, and the crash lands at stop rather than at start.
Practical effect on the earlier workaround: fire-and-store avoids the crash at start and lets the recipe get much further than it used to (
wait-notify-only-eventandassert-notify-only-still-recordingnow pass on device,maxDurationReached: true,durationMs: 2183), but it is not a complete workaround — stop is still reachable.One incidental finding while tracing this:
set-step-hudandclear-step-hudare not implemented incdp-bridge.mjsat all, sovalidate-recipe.jsemitsHUD warning: ERROR: No device matching ...on every run regardless of whether the device is connected. That message is misleading during triage — it looks like a device-resolution failure when it is an unimplemented command. Worth either implementing or removing the calls.fix(agentic): correct device handling, benchmark output parsing, and …
fix(agentic): fail on explicit device miss, assert stored outcomes, c…
[-]SIGSEGV in Hermes when a CDP evaluation is open during data delivery (Android and iOS)[/-][+]SIGSEGV in Hermes during audio data delivery to JS (Android and iOS)[/+]abretonc7s commentedon Aug 21, 2026
Retitled: the old title said "when a CDP evaluation is open", and that mechanism is wrong.
cdp-bridge.mjspassesawaitPromise: falseon everyRuntime.evaluate, andeval-asyncis itself fire-and-store internally — it kicks off, returns'started', and polls with separate evaluations. No CDP request is ever held open, so "an open evaluation" cannot be the trigger.What is actually measured, on a Pixel 6a:
getState()evals at ~250ms during a live 6s recordingstartRecording()promise outstanding as audio startsvalidate-recipe.js, all calls fire-and-storeSo polling is not the trigger, and fire-and-store is not a complete workaround.
The common thread in the crashing cases is an outstanding JS promise from a recording call at the moment audio crosses into JS — but the recipe case crashes at stop, where that framing fits less well, so I would not state a mechanism with confidence yet.
CLAUDE.md has been corrected to describe the measurements rather than the wrong theory. The previous wording caused a reviewer to flag safe polling as a bug, which is a good illustration of why the guess was worth removing.
github-actions commentedon Aug 21, 2026
Thank you for submitting an issue. However, it has been automatically closed because: Description of the issue is required.
Please update your issue with complete information following the template and reopen it.
Minimum requirements:
2 remaining items
github-actions commentedon Aug 21, 2026
Thank you for submitting an issue. However, it has been automatically closed because: Description of the issue is required.
Please update your issue with complete information following the template and reopen it.
Minimum requirements:
fix(agentic): resolve the device serial before any subshell, pin the …
ci: only validate issues from people without write access (#471)
fix(agentic): drop a devicectl command that does not exist, gate the …
fix(agentic): accept the bridge's own device label, wait for stored o…
fix(sherpa-onnx.rn/android): let stopTts interrupt an in-flight gener…
fix(agentic): gate on persistent evidence, correct the iOS logging claim
fix(agentic): refuse concurrent runs, retry the stop, correct two sta…
abretonc7s commentedon Aug 22, 2026
Reproduced three consecutive times on 2026-08-22, Pixel 6a (Android 17 / API 37), always at the same node.
Recipe:
teams/playground/recipes/record-max-duration-validation.json. The smoke flow passes 5/5, then the run dies atwait-notify-only-event— the first node that actually starts audio. Every recording call in the path is fire-and-store.Two things worth noting:
AudioRecordingServiceback with no JS to own it.Also corrects a stale claim in CLAUDE.md, now fixed: the recipe path was described as known good after 5962eda's 12/12 run. That run was real, but the path is intermittent rather than fixed.
docs: correct the recipe crash claim with today's evidence
docs: make on-device validation a hard rule (#460)