Skip to content

[BUG] SIGSEGV in Hermes during audio data delivery to JS (Android and iOS) #436

Closed

Description

@abretonc7s

Summary

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

  1. Launch apps/sherpa-voice on a physical device with Metro on :7500
  2. Attach CDP (scripts/agentic/app-state.sh eval ...)
  3. Run any TTS generation, e.g. __AGENTIC__.testTTSFull()
  4. ~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.

Found while validating #434.

Activity

  1. abretonc7s commented on Aug 20, 2026

    @abretonc7s
    CollaboratorAuthor

    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:

    __AGENTIC__.startRecording({ sampleRate: 44100, channels: 1 })

    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:

    #00-#06  libhermesvm.so
    #07      RuntimeScheduler_Modern::performMicrotaskCheckpoint(jsi::Runtime&)
    #08      RuntimeScheduler_Modern::runEventLoopTick(jsi::Runtime&, Task&)
    #09      RuntimeScheduler_Modern::runEventLoop(jsi::Runtime&)
    

    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.

  2. abretonc7s commented on Aug 21, 2026

    @abretonc7s
    CollaboratorAuthor

    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.

  3. 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
  4. abretonc7s commented on Aug 21, 2026

    @abretonc7s
    CollaboratorAuthor

    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.

  5. abretonc7s commented on Aug 21, 2026

    @abretonc7s
    CollaboratorAuthor

    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.

  6. 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
  7. abretonc7s commented on Aug 21, 2026

    @abretonc7s
    CollaboratorAuthor

    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.

  8. github-actions commented on Aug 21, 2026

    @github-actions

    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:

    • Descriptive title that starts with [BUG]
    • Environment details (expo-audio-studio version, Platform & OS)
    • Clear description of the issue
  9. 2 remaining items

  10. github-actions commented on Aug 21, 2026

    @github-actions

    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:

    • Descriptive title that starts with [BUG]
    • Environment details (expo-audio-studio version, Platform & OS)
    • Clear description of the issue
  11. abretonc7s commented on Aug 22, 2026

    @abretonc7s
    CollaboratorAuthor

    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:

    1. 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.
    2. The service restart afterwards is what leaves the orphaned foreground notification tracked in Android: AudioRecordingService stays in foreground after recording stops #474 — ActivityManager brings AudioRecordingService back 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions