Skip to content

Use-after-free in iOS/Android recorder: buffer pointer dereferenced after callback returns #1053

@christian-apollo

Description

@christian-apollo

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

  1. Open a long-running recording session via AudioRecorder.onAudioReady(...) and recorder.start() — anything that keeps the CoreAudio input render callback firing for minutes.
  2. Cause heavy CPU contention on the device (background work, low-power mode, etc.).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions