software-mansion / software-mansion/react-native-audio-api

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

Open
#1,298 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug ios pkg:audio-api
Dominant language
C++
Stars
839
Forks
92
Avg merge
4d 15h
Merged PRs (30d)
27

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-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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with PR #1290 and the AudioEngine methods shown in the crash stack, especially rebuildAudioEngine, destroyAudioEnginePreservingSessionDeactivationState:, and setAudioEngine:. Reproduce the recorder and AudioContext sequence with iosVoiceProcessing enabled, then verify that the main-thread watchdog deadlock no longer occurs and that the stack is covered by a reproducible regression case.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, ios, react-native
Domain
audio-video-rtc, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.