Repository navigation
iOS: deadlock (0x8BADF00D watchdog) with iosVoiceProcessing=true — handleEngineConfigurationChange isInUse vs AVAudioEngine dealloc dispatch_sync (nightly) #1298
Description
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-api1.0.0-nightly-1966b4a-20260918(audio-api-nightlydist-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 noiosVoiceProcessingoption) 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
antoniosb commentedon Sep 18, 2026
Follow-up: I applied the one-hunk change from #1290 locally (moving the
isInUsecheck 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):
JS thread: blocked on the engine
recursive_mutexin-[AudioEngine isEngineRunning]←IOSAudioPlayer::isRunning()←AudioContext::isDriverRunning()←AudioParam::scheduleAudioEvent(anAudioParam.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 postsAVAudioEngineConfigurationChangeNotification, which callsrestartAudioEngine→rebuildAudioEngine→applyVoiceProcessing→setVoiceProcessingEnabled:again, all while holding the engine mutex on the main thread and withAURemoteIO::Initializewaiting 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 acrosssetVoiceProcessingEnabled:.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 })+ anAudioContextscheduling buffers.closetcaiman commentedon Sep 18, 2026
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.
vnekhai-personal commentedon Oct 8, 2026
Device evidence with #1330 applied (iPhone 16 Pro Max,
iosVoiceProcessing: true, nightly1.0.0-nightly-bbc3870-20261008+ #1330 atfa7aa74).#1330 fixes the freeze for us, but some sessions then get no input at all. From the device log:
DefaultChatMode = VoiceChat, and the session posts aRouteConfigurationChange. Within a few hundred milliseconds the input is stopped and started again.We're now testing these changes on top of #1330, modeled on WebRTC's iOS audio device and LiveKit's
AudioEngineDevice:AVAudioEngineConfigurationChangeNotificationwhile 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.startAndReturnError:up to 5 times, 100 ms apart..voiceChatmode before enabling voice processing, as WebRTC does.Will report back once it has run on a device; happy to share the patch.
vnekhai-personal commentedon Oct 8, 2026
Update: on the first device build with the changes above (on top of #1330, same iPhone 16 Pro Max,
iosVoiceProcessing: truerequested), 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.
TowhidKashem commentedon Oct 18, 2026
just commenting to subscribe for updates (please ignore)
closetcaiman commentedon Oct 19, 2026
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.