Description
IOSRecorderCallback::receiveAudioData and AndroidRecorderCallback::receiveAudioData push the raw audio buffer pointer they receive from the platform onto an SPSC queue and dereference it asynchronously from the offloader worker thread. The buffer is only valid for the duration of the synchronous platform callback (CoreAudio render callback on iOS, Oboe data callback on Android), so by the time the worker thread reads from it the memory may have been reused or freed. This is a classic use-after-free.
The hot loop that crashes (iOS):
// IOSRecorderCallback.mm — receiveAudioData
offloader_->getSender()->send({inputBuffer, numFrames}); // pointer goes onto a cross-thread queue
// IOSRecorderCallback.mm — taskOffloaderFunction (runs on a different thread)
for (size_t i = 0; i < bufferFormat_.channelCount; ++i) {
memcpy(
converterInputBuffer_.mutableAudioBufferList->mBuffers[i].mData,
inputBuffer->mBuffers[i].mData, // <-- inputBuffer may be freed here
inputBuffer->mBuffers[i].mDataByteSize);
}
The Android path has the same shape: void *data from Oboe is pushed onto the queue and read later from the worker.
Production stack trace (iOS)
App version: react-native-audio-api 0.12.1, iOS, hit on a real device:
Crashed: Thread
0 libsystem_platform.dylib 0x9c0 _platform_memmove + 144
1 ApolloScooters 0x1eecb84 audioapi::IOSRecorderCallback::taskOffloaderFunction(CallbackData) + 164 (IOSRecorderCallback.mm:164)
2 ApolloScooters 0x1eed310 std::__thread_proxy<...IOSRecorderCallback::prepare(AVAudioFormat*, unsigned long)::$_0&...>(void*) + 81
3 libsystem_pthread.dylib 0x4438 _pthread_start + 136
4 libsystem_pthread.dylib 0x8cc thread_start + 8
+ 164 lands inside the memcpy loop on line 164–169 of IOSRecorderCallback.mm.
Why it's hard to MRE
The bug is a memory race: the offloader worker has to be preempted long enough that CoreAudio reuses the buffer slot before the memcpy reads it, then it has to read into unmapped or zero-mapped pages to actually crash (otherwise it just silently consumes garbage audio). Reproducing it deterministically would require slowing the worker thread or dirtying buffers under the audio system. The bug is, however, clearly visible from inspection, and it shows up in production crash reports under sustained mic load (continuous wake-word / VAD streaming).
Steps to reproduce in production
- Open a long-running recording session via
AudioRecorder.onAudioReady(...) and recorder.start() — anything that keeps the CoreAudio input render callback firing for minutes.
- Cause heavy CPU contention on the device (background work, low-power mode, etc.).
- Eventually the SPSC worker is delayed long enough that the source buffer slot is recycled before its memcpy runs → segfault inside
_platform_memmove.
Fix
PR: #1054 — copies the buffer into an owned allocation inside receiveAudioData and frees it after taskOffloaderFunction is done (along both fast and resampling paths). Same approach for both iOS (AudioBufferList + per-channel mData) and Android (ma_malloc/ma_free).
Environment
- Library: react-native-audio-api 0.12.1 (also reproduced upstream
main by inspection)
- Platforms: iOS (confirmed crash in the wild); Android has the same shape and is fixed in the same PR
Description
IOSRecorderCallback::receiveAudioDataandAndroidRecorderCallback::receiveAudioDatapush the raw audio buffer pointer they receive from the platform onto an SPSC queue and dereference it asynchronously from the offloader worker thread. The buffer is only valid for the duration of the synchronous platform callback (CoreAudio render callback on iOS, Oboe data callback on Android), so by the time the worker thread reads from it the memory may have been reused or freed. This is a classic use-after-free.The hot loop that crashes (iOS):
The Android path has the same shape:
void *datafrom Oboe is pushed onto the queue and read later from the worker.Production stack trace (iOS)
App version: react-native-audio-api 0.12.1, iOS, hit on a real device:
+ 164lands inside thememcpyloop on line 164–169 ofIOSRecorderCallback.mm.Why it's hard to MRE
The bug is a memory race: the offloader worker has to be preempted long enough that CoreAudio reuses the buffer slot before the memcpy reads it, then it has to read into unmapped or zero-mapped pages to actually crash (otherwise it just silently consumes garbage audio). Reproducing it deterministically would require slowing the worker thread or dirtying buffers under the audio system. The bug is, however, clearly visible from inspection, and it shows up in production crash reports under sustained mic load (continuous wake-word / VAD streaming).
Steps to reproduce in production
AudioRecorder.onAudioReady(...)andrecorder.start()— anything that keeps the CoreAudio input render callback firing for minutes._platform_memmove.Fix
PR: #1054 — copies the buffer into an owned allocation inside
receiveAudioDataand frees it aftertaskOffloaderFunctionis done (along both fast and resampling paths). Same approach for both iOS (AudioBufferList+ per-channelmData) and Android (ma_malloc/ma_free).Environment
mainby inspection)