CVE-2026-17784
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/audio/apple/audio_low_latency_input.cc |
modified |
Files Changed
media/audio/apple/audio_low_latency_input.cc
Patch
From 9e0ead35e21e9eabf0649ef090f7b61498eaeef9 Mon Sep 17 00:00:00 2001 From: Thomas Guilbert <[email protected]> Date: Tue, 02 Jun 2026 11:31:41 -0700 Subject: [PATCH] Guard input stream internals behind lock on Apple This CL updates input streams on Apple devices to acquire locks when setting and clearing render callbacks. This should remove the potential for race conditions between pending `OnDataIsAvailable()` callbacks and normal stream control operations (e.g. `Start()`/`Stop()`...). This change also guards glitch reporting behind lock, to mirror the behavior we already use on the output side, in audio_auhaul.cc. Change-Id: Ia4ad56c33c852a1291b43af00bc2190f56416443 Bug: 513694032 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884659 Commit-Queue: Thomas Guilbert <[email protected]> Reviewed-by: Dale Curtis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1640334} --- diff --git a/media/audio/apple/audio_low_latency_input.cc b/media/audio/apple/audio_low_latency_input.cc index 02ddb6a..5c576a1f 100644 --- a/media/audio/apple/audio_low_latency_input.cc +++ b/media/audio/apple/audio_low_latency_input.cc @@ -659,7 +659,8 @@ kAudioUnitScope_Global, AUElement::INPUT, ¤t_agc_setting, &property_size); if (result != noErr) { - HandleError(result, "Error reading System AGC property"); + base::AutoLock al(lock_); + HandleErrorAndNotify_Locked(result, "Error reading System AGC property"); return; } @@ -674,7 +675,8 @@ &new_agc_setting, sizeof(new_agc_setting)); if (result != noErr) { - HandleError(result, "Error setting System AGC property"); + base::AutoLock al(lock_); + HandleErrorAndNotify_Locked(result, "Error setting System AGC property"); return; } @@ -687,7 +689,6 @@ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DVLOG(1) << __FUNCTION__ << " this " << this; DCHECK(callback); - DCHECK(!sink_); DLOG_IF(ERROR, !audio_unit_) << "Open() has not been called successfully"; if (IsRunning()) return; @@ -708,7 +709,11 @@ } #endif - sink_ = callback; + { + base::AutoLock al(lock_); + DCHECK(!sink_); + sink_ = callback; + } last_success_time_ = base::TimeTicks::Now(); // Don't disable built-in noise suppression when using VPAU. @@ -777,8 +782,11 @@ SetInputCallbackIsActive(false); ReportAndResetStats(); - sink_ = nullptr; - fifo_.Clear(); + { + base::AutoLock al(lock_); + sink_ = nullptr; + fifo_.Clear(); + } got_input_callback_ = false; } @@ -932,6 +940,15 @@ DCHECK_EQ(result, noErr); } + // Temporarily clear the sink under lock so in-flight callbacks drain + // before we close the AudioUnit. + AudioInputCallback* temp_sink = nullptr; + { + base::AutoLock al(lock_); + temp_sink = sink_; + sink_ = nullptr; + } + CloseAudioUnit(); // Reset things to a state similar to before the audio unit was opened. @@ -942,6 +959,12 @@ OpenVoiceProcessingAU(); + // Restore the sink under lock. + { + base::AutoLock al(lock_); + sink_ = temp_sink; + } + if (was_running) { result = AudioOutputUnitStart(audio_unit_); if (result != noErr) { @@ -979,6 +1002,11 @@ const AudioTimeStamp* time_stamp, UInt32 bus_number, UInt32 number_of_frames) { + base::AutoLock al(lock_); + if (!sink_) { + return kAudioUnitErr_Uninitialized; + } + TRACE_EVENT1("audio", "AUAudioInputStream::OnDataIsAvailable", "frames", number_of_frames); @@ -1077,13 +1105,14 @@ LOG(ERROR) << "Too long sequence of " << err << " errors!"; } - HandleError(result, "AudioUnitRender() failed"); + HandleErrorAndNotify_Locked(result, "AudioUnitRender() failed"); return result; } OSStatus AUAudioInputStream::Provide(UInt32 number_of_frames, AudioBufferList* io_data, const AudioTimeStamp* time_stamp) { + lock_.AssertAcquired(); TRACE_EVENT1("audio", "AUAudioInputStream::Provide", "number_of_frames", number_of_frames); glitch_helper_.OnFramesReceived(*time_stamp, number_of_frames); @@ -1199,8 +1228,17 @@ GetInputCallbackIsActive() ? err : (err * -1)); SendLog(base::StringPrintf("%s at line %d", message, location.line_number()), err); - if (sink_) +} + +void AUAudioInputStream::HandleErrorAndNotify_Locked( + OSStatus err, + const char* message, + const base::Location& location) { + lock_.AssertAcquired(); + HandleError(err, message, location); + if (sink_) { sink_->OnError(); + } } void AUAudioInputStream::SetInputCallbackIsActive(bool enabled) { @@ -1233,7 +1271,32 @@ DVLOG(1) << __FUNCTION__ << " this " << this; if (!audio_unit_) return; - OSStatus result = AudioUnitUninitialize(audio_unit_); + + // Clear the input callback. + AURenderCallbackStruct callback; + callback.inputProc = nullptr; + callback.inputProcRefCon = nullptr; + OSStatus result = AudioUnitSetProperty( + audio_unit_, kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + use_voice_processing_ ? AUElement::INPUT : AUElement::OUTPUT, &callback, + sizeof(callback)); + OSSTATUS_DLOG_IF(ERROR, result != noErr, result) + << "Failed to clear AU input callback."; + + if (use_voice_processing_) { + AURenderCallbackStruct playout_callback; + playout_callback.inputProc = nullptr; + playout_callback.inputProcRefCon = nullptr; + result = + AudioUnitSetProperty(audio_unit_, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, AUElement::OUTPUT, + &playout_callback, sizeof(playout_callback)); + OSSTATUS_DLOG_IF(ERROR, result != noErr, result) + << "Failed to clear AU render callback."; + } + + result = AudioUnitUninitialize(audio_unit_); OSSTATUS_DLOG_IF(ERROR, result != noErr, result) << "AudioUnitUninitialize() failed."; result = AudioComponentInstanceDispose(audio_unit_); @@ -1243,17 +1306,24 @@ } void AUAudioInputStream::ReportAndResetStats() { - std::optional<std::string> log_message = glitch_helper_.LogAndReset("AU in"); + std::optional<std::string> log_message; + size_t local_number_of_frames_provided = 0; + { + base::AutoLock al(lock_); + log_message = glitch_helper_.LogAndReset("AU in");
Original Bug Report
Potential Use-After-Free in AUAudioInputStream due to race with CoreAudio callback
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) exists in AUAudioInputStream on macOS because it does not synchronize teardown with real-time CoreAudio callbacks. Unlike its output counterpart, AUHALStream, this class fails to clear its callback context or use locks to protect member state during shutdown. A compromised renderer can potentially trigger this race to achieve memory corruption in the sandboxed Audio Service process.
Affected files:
media/audio/apple/audio_low_latency_input.ccmedia/audio/apple/audio_low_latency_input.hmedia/audio/audio_manager_base.cc
Estimated timestamp from git blame: 2018-05-24
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in AUAudioInputStream on macOS. The issue arises from a lack of synchronization between the Audio Manager thread (which handles object lifetime) and the real-time CoreAudio thread (which handles audio data callbacks). This allows a system callback to dereference a stale this pointer after the object has been deleted.
Root Cause Analysis
In media/audio/apple/audio_low_latency_input.cc, AUAudioInputStream registers a static callback DataIsAvailable with the CoreAudio framework, passing a raw this pointer as the context (inputProcRefCon).
There are two primary concerns with the current implementation:
- Lack of Synchronization: There is no mutex or lock protecting access to member variables (like
sink_andfifo_) within the real-time callback. - Late-Arriving Callbacks: It is a known behavior of the macOS CoreAudio
HALOutputunit that callbacks can be invoked even afterAudioOutputUnitStop()has been called. The relatedAUHALStreamclass (for audio output) explicitly documents this risk and implements abase::Lockand callback-clearing logic to mitigate it.AUAudioInputStreamlacks these defenses.
When Close() is called, the AUAudioInputStream object is deleted. If a late CoreAudio callback arrives after this deletion, it dereferences the stale this pointer, leading to a UAF.
Potential Attack Vector
A compromised renderer with microphone permissions can access this path through the blink.mojom.RendererAudioInputStreamFactory interface. By repeatedly triggering AEC reinitialization (via AssociateInputAndOutputForAec) or rapidly opening and closing streams, an attacker can widen the race window and potentially achieve a UAF write primitive.
Impact
This is a potential heap Use-After-Free write. Because the context is a raw pointer passed to a system API, Chromium’s MiraclePtr mitigation does not provide protection. Successful exploitation could lead to arbitrary code execution within the sandboxed macOS Audio Service process.
Suggested Steps to Trigger (Potential)
- From a compromised renderer, request an audio input stream with the
ECHO_CANCELLEReffect enabled. - Start recording to activate the CoreAudio real-time thread callbacks.
- In a loop, invoke
AssociateInputAndOutputForAecwith alternating output device IDs to force frequent reinitialization of the voice processing unit. - Simultaneously initiate stream closure while trying to reclaim the freed
AUAudioInputStreammemory via other allocations in the Audio Service. - Observe if the late callback triggers a crash or memory corruption when accessing the
sink_member.
Recommended Fix
Apply the synchronization pattern used in AUHALStream:
- Add a
base::LocktoAUAudioInputStreamand acquire it withinOnDataIsAvailableand teardown methods. - In
Stop()andClose(), explicitly clear the CoreAudio input callback by setting thekAudioOutputUnitProperty_SetInputCallbackproperty to NULL before disposing of the Audio Unit.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.