Skip to content

iOS: deadlock (0x8BADF00D watchdog) with iosVoiceProcessing=true — handleEngineConfigurationChange isInUse vs AVAudioEngine dealloc dispatch_sync (nightly) #1298

Open

Description

@antoniosb

Summary

With new AudioRecorder({ iosVoiceProcessing: true }) on the current nightly, the app deadlocks on the main thread as soon as playback starts through an AudioContext, and iOS kills it with watchdog code 0x8BADF00D ("scene-update watchdog transgression … is stuck (deadlock)").

The crash report shows a lock-order inversion between the engine-configuration-change handler and the engine rebuild. PR #1290 ("fix(ios): engine-worker deadlock") moves the isInUse check inside the main-queue block, which is exactly the ordering in this trace — this issue is filed so that PR has something to close, and to add a reproducible stack.

Environment

  • react-native-audio-api 1.0.0-nightly-1966b4a-20260918 (audio-api-nightly dist-tag)
  • React Native 0.86.3, Expo SDK 57 (dev-client build, new architecture)
  • iOS 26.6.2, iPhone 13 Pro (iPhone14,2), Xcode 27.0 (27A266a)
  • Does not reproduce on 0.13.4 (which has no iosVoiceProcessing option) with the same JS.

Repro

AudioManager.setAudioSessionOptions({
  iosCategory: "playAndRecord",
  iosMode: "voiceChat",
  iosOptions: ["defaultToSpeaker", "allowBluetoothHFP"],
});
await AudioManager.setAudioSessionActivity(true);

const rec = new AudioRecorder({ iosVoiceProcessing: true, androidInputPreset: "voiceCommunication" });
rec.onAudioReady({ sampleRate: 16000, bufferLength: 640, channelCount: 1 }, (ev) => {
  /* Float32 → Int16 → WebSocket */
});
await rec.start(); // → {"status":"success"}

// Later, when the first remote PCM chunk arrives:
const ctx = new AudioContext({ sampleRate: 24000 });
const buf = ctx.createBuffer(1, samples, 24000);
// ... copyToChannel, createBufferSource, start()

Sequence observed: recorder starts fine → first TTS chunk arrives → AudioContext is created and a buffer scheduled → UI freezes → ~10 s later the watchdog kills the process.

Voice-processing IO changes the IO unit configuration, so AVAudioEngineConfigurationChangeNotification fires while the engine is being (re)built.

Crash report (excerpt)

Termination: FRONTBOARD 0x8BADF00D — scene-update watchdog transgression: app<me.duoai.mobilepoc> is stuck (deadlock)

Thread 0 (main, triggered):

libsystem_kernel   kevent_id
libdispatch        _dispatch_kq_poll
libdispatch        _dispatch_event_loop_wait_for_ownership
libdispatch        __DISPATCH_WAIT_FOR_QUEUE__
libdispatch        _dispatch_sync_f_slow
AVFAudio           -[AVAudioEngine dealloc]
app                -[AudioEngine setAudioEngine:]
app                -[AudioEngine destroyAudioEnginePreservingSessionDeactivationState:]
app                -[AudioEngine rebuildAudioEngine]
app                -[AudioEngine rebuildAudioEngineAndResumeIfNeeded]
app                -[AudioEngine restartAudioEngine]
app                __61-[SystemNotificationManager handleEngineConfigurationChange:]_block_invoke
libdispatch        _dispatch_main_queue_drain

Thread "engine":

libsystem_kernel   __psynch_mutexwait
libc++             std::recursive_mutex::lock()
app                std::scoped_lock<std::recursive_mutex>::scoped_lock
app                -[AudioEngine isInUse]
app                -[SystemNotificationManager handleEngineConfigurationChange:]
CoreFoundation     __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
Foundation         -[NSNotificationCenter postNotificationName:object:userInfo:]
AVFAudio           AVAudioEngineImpl::IOUnitConfigurationChanged() block
libdispatch        _dispatch_source_invoke / _dispatch_lane_serial_drain

Main holds the engine recursive_mutex and dispatch_syncs onto the engine queue from -[AVAudioEngine dealloc]; the engine queue is blocked in isInUse waiting for that same mutex.

Full .ips available on request.

Activity

  1. antoniosb commented on Sep 18, 2026

    @antoniosb
    Author

    Follow-up: I applied the one-hunk change from #1290 locally (moving the isInUse check inside the main-queue block) and rebuilt. The original inversion is gone, but the app still hangs and is killed by the watchdog, now at a different place:

    Thread 0 (main):

    libsystem_kernel        mach_msg2_trap
    libEmbeddedSystemAUs    AURemoteIO::Initialize()
    libEmbeddedSystemAUs    ausdk::AUBase::DoInitialize()
    AudioToolboxCore        AudioUnitInitialize
    AudioToolboxCore        -[AUAudioUnitV2Bridge allocateRenderResourcesAndReturnError:]
    AVFAudio                AVAudioIONodeImpl::GetOutputFormat(unsigned long)
    AVFAudio                -[AVAudioNode outputFormatForBus:]
    AVFAudio                -[AVAudioIONode setVoiceProcessingEnabled:error:]
    app                     -[AudioEngine applyVoiceProcessing]
    app                     -[AudioEngine materializeTrackedNodesIfNeeded]
    app                     -[AudioEngine rebuildAudioEngine]
    app                     -[AudioEngine rebuildAudioEngineAndResumeIfNeeded]
    app                     -[AudioEngine restartAudioEngine]
    

    JS thread: blocked on the engine recursive_mutex in -[AudioEngine isEngineRunning] ← IOSAudioPlayer::isRunning() ← AudioContext::isDriverRunning() ← AudioParam::scheduleAudioEvent (an AudioParam.value = assignment from JS).

    Termination reason this time: 0x8BADF00D … Failed to terminate gracefully after 5.0s.

    Reading: setVoiceProcessingEnabled: itself changes the IO unit configuration, which posts AVAudioEngineConfigurationChangeNotification, which calls restartAudioEngine → rebuildAudioEngine → applyVoiceProcessing → setVoiceProcessingEnabled: again, all while holding the engine mutex on the main thread and with AURemoteIO::Initialize waiting on the audio server. So #1290 fixes the first inversion, but the voice-processing path also needs to (a) not re-apply voice processing from inside the config-change rebuild when it is already enabled, and (b) not hold the engine mutex across setVoiceProcessingEnabled:.

    Same environment as above (nightly 1.0.0-nightly-1966b4a-20260918, RN 0.86.3, iOS 26.6.2, iPhone 13 Pro). Repro is identical: new AudioRecorder({ iosVoiceProcessing: true }) + an AudioContext scheduling buffers.

  2. closetcaiman commented on Sep 18, 2026

    @closetcaiman
    Member

    Hi @antoniosb, thank you so much for both reports (especially for the second one). I'm working on the fix now and hope to have the PR ready soon.

  3. added
    bugSomething isn't working as expected or produces unexpected errors
    iosNative iOS implementation, CoreAudio/AVFoundation bindings, or iOS-specific issues
    pkg:audio-apiCore React Native audio control system compliant with the W3C Web Audio API spec
    on Sep 18, 2026
  4. vnekhai-personal commented on Oct 8, 2026

    @vnekhai-personal

    Device evidence with #1330 applied (iPhone 16 Pro Max, iosVoiceProcessing: true, nightly 1.0.0-nightly-bbc3870-20261008 + #1330 at fa7aa74).

    #1330 fixes the freeze for us, but some sessions then get no input at all. From the device log:

    • Every start: the voice-processing unit opens with its formats at 0 Hz, sets DefaultChatMode = VoiceChat, and the session posts a RouteConfigurationChange. Within a few hundred milliseconds the input is stopped and started again.
    • Good session: the restarted input ran until the session ended (17 s).
    • Bad session, right after an app relaunch: the restarted input ran 0.33 s, was started again, and stopped 15 ms later. Further start attempts over the next ~0.4 s never got it running again. That looks like the case raised in Fix/ios voice processing #1330's review: a recorder left without a sink by a failed rebuild.

    We're now testing these changes on top of #1330, modeled on WebRTC's iOS audio device and LiveKit's AudioEngineDevice:

    1. Ignore AVAudioEngineConfigurationChangeNotification while the engine still runs. Once it has stopped, start the same engine again, and rebuild only if the input format changed or the restart fails.
    2. Retry startAndReturnError: up to 5 times, 100 ms apart.
    3. Wait up to 0.5 s for a non-zero input format before connecting the sink.
    4. Reconnect a recorder that lost its sink at the next route change with a valid input format.
    5. App side: put the session in .voiceChat mode before enabling voice processing, as WebRTC does.

    Will report back once it has run on a device; happy to share the patch.

  5. vnekhai-personal commented on Oct 8, 2026

    @vnekhai-personal

    Update: on the first device build with the changes above (on top of #1330, same iPhone 16 Pro Max, iosVoiceProcessing: true requested), sessions get input from the start: spoken turns are heard and answered.

    We didn't capture a device log on this run, so we can't yet say which of the five changes matters most, or rule out that our app's own fallback (a plain recorder once a voice-processing one stalls) carried a session. The original failure was intermittent and hit the session right after an app relaunch, so next we'll test across relaunches with the log running and report back.

  6. TowhidKashem commented on Oct 18, 2026

    @TowhidKashem

    just commenting to subscribe for updates (please ignore)

  7. closetcaiman commented on Oct 19, 2026

    @closetcaiman
    Member

    Hey @vnekhai-personal and @antoniosb,

    I updated #1317 with additional fixes on top. I'm also looking to cover the follow-up bugs in upcoming PRs, but while testing on this PR, I haven't run into any issues with voiceProcessing. If you get a chance to test it out, please let me know how it performs.

  8. linked a pull request that will close this issuefix(ios): vpio driver loop #1317on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working as expected or produces unexpected errorsiosNative iOS implementation, CoreAudio/AVFoundation bindings, or iOS-specific issuespkg:audio-apiCore React Native audio control system compliant with the W3C Web Audio API spec

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions