CVE-2026-17708
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
AUCallbackProxymedia/audio/apple/audio_low_latency_input.cc |
modified | |
ifmedia/audio/apple/audio_low_latency_input.cc |
modified |
Files Changed
media/audio/apple/audio_low_latency_input.cc
Patch
From e95a85087818fe24e065e389ff699e12c05e7322 Mon Sep 17 00:00:00 2001
From: Thomas Guilbert <tguilbert@chromium.org>
Date: Thu, 11 Jun 2026 14:13:53 -0700
Subject: [PATCH] Prevent UAF on CoreAudio teardown failure
If we fail to stop CoreAudio after attaching a data callback, pending
callbacks might access `AUAudioInputStream` after its destruction.
This CL fixes the issue by adding a simple proxy object between
CoreAudio and `AUAudioInputStream`, which directly forwards
`OnDataIsAvailable()` to the stream. If aren't able to stop or destroy
CoreAudio, we detach the proxy (which stops forwarding calls) and
intentionally leak the proxy object, ensuring that the access memory
remains valid.
Note: this logic mirrors the logic in catap_audio_input_stream.mm
Bug: 519738647
Change-Id: I466d68cdbbed59af2af376ac6959a709c5d99af2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7914270
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1645599}
---
diff --git a/media/audio/apple/audio_low_latency_input.cc b/media/audio/apple/audio_low_latency_input.cc
index 5c576a1f..ad1b0aa 100644
--- a/media/audio/apple/audio_low_latency_input.cc
+++ b/media/audio/apple/audio_low_latency_input.cc
@@ -9,6 +9,7 @@
#include <memory>
#include <string>
+#include <string_view>
#include <utility>
#include "base/apple/foundation_util.h"
@@ -16,6 +17,7 @@
#include "base/apple/scoped_cftyperef.h"
#include "base/apple/scoped_mach_port.h"
#include "base/compiler_specific.h"
+#include "base/debug/leak_annotations.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
@@ -74,6 +76,44 @@
constexpr char kChosenSampleFormatHistogram[] =
"Media.Audio.Capture.Mac.AUAudioInputStream.ChosenSampleFormat";
+// Proxy to intercept callbacks and prevent Use-After-Free on teardown failure.
+class AUCallbackProxy {
+ public:
+ explicit AUCallbackProxy(raw_ptr<AUAudioInputStream> stream)
+ : stream_(stream) {}
+
+ void Detach() {
+ base::AutoLock al(lock_);
+ stream_ = nullptr;
+ }
+
+ OSStatus OnDataIsAvailable(AudioUnitRenderActionFlags* flags,
+ const AudioTimeStamp* time_stamp,
+ UInt32 bus_number,
+ UInt32 number_of_frames) {
+ base::AutoLock al(lock_);
+ if (stream_) {
+ return stream_->OnDataIsAvailable(flags, time_stamp, bus_number,
+ number_of_frames);
+ }
+ return kAudioUnitErr_Uninitialized;
+ }
+
+ private:
+ base::Lock lock_;
+ raw_ptr<AUAudioInputStream> stream_ GUARDED_BY(lock_);
+};
+
+// Logs and returns true if `result` is an error.
+static bool CheckAndLogFailure(OSStatus result, std::string_view message) {
+ if (result == noErr) {
+ return false;
+ }
+
+ OSSTATUS_DLOG(ERROR, result) << message;
+ return true;
+}
+
// Returns true if the format flags in |format_flags| has the "non-interleaved"
// flag (kAudioFormatFlagIsNonInterleaved) cleared (set to 0).
static bool FormatIsInterleaved(UInt32 format_flags) {
@@ -347,9 +387,10 @@
// Register the input procedure for the AUHAL. This procedure will be called
// when the AUHAL has received new data from the input device.
+ data_callback_proxy_ = std::make_unique<AUCallbackProxy>(this);
AURenderCallbackStruct callback;
callback.inputProc = &DataIsAvailable;
- callback.inputProcRefCon = this;
+ callback.inputProcRefCon = data_callback_proxy_.get();
result = AudioUnitSetProperty(
audio_unit_, kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, AUElement::OUTPUT, &callback, sizeof(callback));
@@ -487,9 +528,10 @@
// Register the input procedure for the AUHAL. This procedure will be called
// when the AUHAL has received new data from the input device.
+ data_callback_proxy_ = std::make_unique<AUCallbackProxy>(this);
AURenderCallbackStruct callback;
callback.inputProc = &DataIsAvailable;
- callback.inputProcRefCon = this;
+ callback.inputProcRefCon = data_callback_proxy_.get();
result = AudioUnitSetProperty(
audio_unit_, kAudioOutputUnitProperty_SetInputCallback,
@@ -693,6 +735,8 @@
if (IsRunning())
return;
+ stop_failed_ = false;
+
#if BUILDFLAG(IS_MAC)
// Check if we should defer Start() for http://crbug.com/160920.
if (manager_->ShouldDeferStreamStart()) {
@@ -767,6 +811,8 @@
// Stop the I/O audio unit.
OSStatus result = AudioOutputUnitStop(audio_unit_);
DCHECK_EQ(result, noErr);
+ stop_failed_ |= CheckAndLogFailure(result, "Failed to stop audio unit.");
+
// Add a DCHECK here just in case. AFAIK, the call to AudioOutputUnitStop()
// seems to set this state synchronously, hence it should always report
// false after a successful call.
@@ -776,8 +822,7 @@
// It does not allocate or free memory resources.
result = AudioUnitReset(audio_unit_, kAudioUnitScope_Global, 0);
DCHECK_EQ(result, noErr);
- OSSTATUS_DLOG_IF(ERROR, result != noErr, result)
- << "Failed to stop acquiring data";
+ stop_failed_ |= CheckAndLogFailure(result, "Failed to stop acquiring data");
}
SetInputCallbackIsActive(false);
@@ -989,12 +1034,12 @@
DCHECK_EQ(bus_number, 1u);
// No data buffer should be allocated at this stage.
DCHECK(!io_data);
- AUAudioInputStream* self = reinterpret_cast<AUAudioInputStream*>(context);
+ AUCallbackProxy* proxy = reinterpret_cast<AUCallbackProxy*>(context);
// Propagate render action flags, time stamp, bus number and number
// of frames requested to the AudioUnitRender() call where the actual data
// is received from the input device via the output scope of the audio unit.
- return self->OnDataIsAvailable(flags, time_stamp, bus_number,
- number_of_frames);
+ return proxy->OnDataIsAvailable(flags, time_stamp, bus_number,
+ number_of_frames);
}
OSStatus AUAudioInputStream::OnDataIsAvailable(
@@ -1272,6 +1317,8 @@
if (!audio_unit_)
return;
+ bool close_failed = false;
+
// Clear the input callback.
AURenderCallbackStruct callback;
callback.inputProc = nullptr;
@@ -1281,8 +1328,8 @@
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.";
+ close_failed |=
+ CheckAndLogFailure(result, "Failed to clear AU input callback.");
if (use_voice_processing_) {
AURenderCallbackStruct playout_callback;
@@ -1292,17 +1339,31 @@
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.";
+ close_failed |=
+ CheckAndLogFailure(result, "Failed to clear AU render callback.");
}
result = AudioUnitUninitialize(audio_unit_);
- OSSTATUS_DLOG_IF(ERROR, result != noErr, result)
- << "AudioUnitUninitialize() failed.";
+ close_failed |= CheckAndLogFailure(result, "AudioUnitUninitialize() failed.");
result = AudioComponentInstanceDispose(audio_unit_);
- OSSTATUS_DLOG_IF(ERROR, result != noErr, result)
- << "AudioComponentInstanceDispose() failed.";
+ close_failed |=
+ CheckAndLogFailure(result, "AudioComponentInstanceDispose() failed.");
audio_unit_ = 0;
+
+ if (data_callback_proxy_) {
Original Bug Report
Potential Use-After-Free in AUAudioInputStream on CoreAudio teardown failure
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) vulnerability exists in AUAudioInputStream on macOS and iOS when the CoreAudio teardown sequence fails. In release builds, API teardown failures are not handled, causing the stream object to be deleted unconditionally while the real-time callback remains active. This can result in a Use-After-Free when the CoreAudio thread executes utilizing the freed stream context.
Affected files:
media/audio/apple/audio_low_latency_input.ccmedia/audio/apple/audio_low_latency_input.h
Estimated timestamp from git blame: 2011-10-05
Description
There is a potential Use-After-Free (UAF) vulnerability in AUAudioInputStream when the macOS CoreAudio driver fails to tear down or stop the active callback thread during stream closure.
AUAudioInputStream registers a raw this pointer with CoreAudio as the callback context (inputProcRefCon):
// media/audio/apple/audio_low_latency_input.cc:350-355 (non-VPIO path)
AURenderCallbackStruct callback;
callback.inputProc = &DataIsAvailable;
callback.inputProcRefCon = this;
result = AudioUnitSetProperty(
audio_unit_, kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, AUElement::OUTPUT, &callback, sizeof(callback));
And similarly under the VoiceProcessing (VPIO) path (media/audio/apple/audio_low_latency_input.cc:490-506).
During stream shutdown, Stop() and CloseAudioUnit() attempt to halt and dispose of the audio unit. However, the OSStatus results of the teardown functions are only checked by DCHECK or OSSTATUS_DLOG_IF macros, which compile to no-ops in release builds:
// media/audio/apple/audio_low_latency_input.cc:763-764
OSStatus result = AudioOutputUnitStop(audio_unit_);
DCHECK_EQ(result, noErr);
And in CloseAudioUnit():
// media/audio/apple/audio_low_latency_input.cc:1236-1241
OSStatus result = AudioUnitUninitialize(audio_unit_);
OSSTATUS_DLOG_IF(ERROR, result != noErr, result) << "AudioUnitUninitialize() failed.";
result = AudioComponentInstanceDispose(audio_unit_);
OSSTATUS_DLOG_IF(ERROR, result != noErr, result) << "AudioComponentInstanceDispose() failed.";
Following these calls, Close() unconditionally proceeds to delete the stream:
// media/audio/apple/audio_low_latency_input.cc:796-800
CloseAudioUnit();
manager_->ReleaseInputStream(this);
// media/audio/audio_manager_base.cc:750
delete stream;
If the teardown calls fail (e.g. due to hardware unplug/transient state changes during destruction), CoreAudio might keep the real-time callback thread active. Since the raw this context pointer has not been invalidated or successfully cleared, the next callback iteration triggers a UAF on the deleted AUAudioInputStream context:
// media/audio/apple/audio_low_latency_input.cc:958-974
OSStatus AUAudioInputStream::DataIsAvailable(void* context, ...) {
AUAudioInputStream* self = reinterpret_cast<AUAudioInputStream*>(context);
return self->OnDataIsAvailable(flags, time_stamp, bus_number, number_of_frames);
}
As the raw callback context is held internally by macOS CoreAudio (outside of Chromium’s heap controls), it is not protected by MiraclePtr (i.e. not a raw_ptr<>).
Potential Attack Steps
Note: Our tooling agent does not currently have the capability to execute code; the following is a potential sequence of events that could lead to triggering this vulnerability.
- A compromised renderer process with mic capture permissions requests the creation of an audio input stream using
blink.mojom.RendererAudioInputStreamFactory.CreateStream. - Once initialized, the attacker causes rapid, continuous AEC re-initializations by calling
AssociateInputAndOutputForAecrepeatedly, which drivesReinitializeVoiceProcessingAudioUnit()and forces frequent Stop-Dispose-Open cycles on the audio device. - During this high-frequency cycle, the attacker induces a hardware disconnection event or races with OS power-state transitions (e.g., system sleep or unplugging an audio device) to cause CoreAudio APIs to fail and return an error code.
- The renderer drops its audio Mojo pipes, which triggers stream shutdown. In the release build,
AudioOutputUnitStopandAudioComponentInstanceDisposefail silently, leaving the stream’s callback thread active. - The
AUAudioInputStreaminstance is deleted, and the subsequent real-time callback invocation on the dangling pointer context triggers a Use-After-Free inside the sandboxed Audio service process.
Suggested Fix
We recommend implementing a thread-safe proxy mechanism similar to the sibling class CatapAudioInputStream (defined in media/audio/mac/catap_audio_input_stream.mm).
By routing callbacks through a separately allocated CatapIoProcProxy that holds a weak reference or a raw_ptr to the stream with proper thread synchronization (locking), you can safely disconnect the callback interface. If the synchronous teardown fails, the proxy object can be intentionally leaked (using ANNOTATE_LEAKING_OBJECT_PTR) to allow any remaining orphaned OS callbacks to access valid memory rather than causing a Use-After-Free.
Evaluated with Chrome root at commit: 9ebf4302210513a012c901d87a2668b3aadf8cc1
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.