CVE-2026-5866
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc |
modified |
Files Changed
third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.ccthird_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h
Patch
From fccaeb9e0967fdc628a6057c4140d2be7649a706 Mon Sep 17 00:00:00 2001 From: Dale Curtis <[email protected]> Date: Tue, 17 Mar 2026 17:33:03 -0700 Subject: [PATCH] Ensure AudioRendererMixer holds lock during sink switch This guards `switch_output_device_in_progress_` with a lock that can be held during the final phase of a setSinkId() operation within the AudioRendererMixerInput. It ensures that if Stop() is called, we don't incorrectly reconnect the new sink, and if a device changes is in flight, that we stall the Stop() call. R=tguilbert Fixed: 492218537 Change-Id: I9ec6efb9678762a22b1b1c8a2f8918771c264678 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7673253 Reviewed-by: Thomas Guilbert <[email protected]> Commit-Queue: Dale Curtis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1600910} --- diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc index a3160d1..ce6b35e 100644 --- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc +++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc @@ -96,6 +96,17 @@ } void AudioRendererMixerInput::Stop() { + { + // Prevents race conditions when Stop() is called during a device change. + base::AutoLock auto_lock(device_change_lock_); + if (switch_output_device_in_progress_) { + switch_output_device_in_progress_ = false; + } + } + StopInternal(); +} + +void AudioRendererMixerInput::StopInternal() { // Stop() may be called at any time, if Pause() hasn't been called we need to // remove our mixer input before shutdown. Pause(); @@ -154,10 +165,13 @@ return; } - if (switch_output_device_in_progress_) { - DCHECK(!godia_in_progress_); - pending_device_info_cb_ = std::move(info_cb); - return; + { + base::AutoLock auto_lock(device_change_lock_); + if (switch_output_device_in_progress_) { + DCHECK(!godia_in_progress_); + pending_device_info_cb_ = std::move(info_cb); + return; + } } godia_in_progress_ = true; @@ -192,7 +206,10 @@ media::OutputDeviceStatusCB callback) { // If a GODIA() call is in progress, defer until it's complete. if (godia_in_progress_) { - DCHECK(!switch_output_device_in_progress_); + { + base::AutoLock auto_lock(device_change_lock_); + DCHECK(!switch_output_device_in_progress_); + } // Abort any previous device switch which may be pending. if (pending_switch_cb_) { @@ -214,7 +231,10 @@ return; } - switch_output_device_in_progress_ = true; + { + base::AutoLock auto_lock(device_change_lock_); + switch_output_device_in_progress_ = true; + } // Request a new sink using the new device id. This process may fail, so to // avoid interrupting working audio, don't set any class variables until we @@ -307,45 +327,48 @@ media::OutputDeviceStatusCB switch_cb, scoped_refptr<media::AudioRendererSink> sink, media::OutputDeviceInfo device_info) { - DCHECK(switch_output_device_in_progress_); - switch_output_device_in_progress_ = false; + auto return_status = device_info.device_status(); - if (device_info.device_status() != media::OUTPUT_DEVICE_STATUS_OK) { - sink->Stop(); - std::move(switch_cb).Run(device_info.device_status()); + { + base::AutoLock auto_lock(device_change_lock_); - // Start any pending device info request. - if (pending_device_info_cb_) { - GetOutputDeviceInfoAsync(std::move(pending_device_info_cb_)); + if (device_info.device_status() != media::OUTPUT_DEVICE_STATUS_OK) { + // Case: Device change failed. + sink->Stop(); + } else if (!switch_output_device_in_progress_) { + // Case: Stop() called during device change. + sink->Stop(); + return_status = media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL; + } else { + // Case: Device change succeeded, connect to new sink. + const bool has_mixer = !!mixer_; + const bool is_playing = playing_; + + // This may occur if Start() hasn't yet been called. + if (sink_) { + sink_->Stop(); + } + + sink_ = std::move(sink); + device_info_ = device_info; + device_id_ = device_info.device_id(); + + auto callback = callback_; + StopInternal(); + callback_ = callback; + + if (has_mixer) { + Start(); + if (is_playing) { + Play(); + } + } } - return; + switch_output_device_in_progress_ = false; } - const bool has_mixer = !!mixer_; - const bool is_playing = playing_; - - // This may occur if Start() hasn't yet been called. - if (sink_) { - sink_->Stop(); - } - - sink_ = std::move(sink); - device_info_ = device_info; - device_id_ = device_info.device_id(); - - auto callback = callback_; - Stop(); - callback_ = callback; - - if (has_mixer) { - Start(); - if (is_playing) { - Play(); - } - } - - std::move(switch_cb).Run(device_info.device_status()); + std::move(switch_cb).Run(return_status); // Start any pending device info request. if (pending_device_info_cb_) { diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h index e13df61..f511187b 100644 --- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h +++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h @@ -80,6 +80,7 @@ void OnRenderError(); private: + void StopInternal(); ~AudioRendererMixerInput() override; friend class AudioRendererMixerInputTest; @@ -118,6 +119,9 @@ scoped_refptr<media::AudioRendererSink> sink, media::OutputDeviceInfo device_info); + // Prevents race conditions when Stop() is called during a device change. + base::Lock device_change_lock_; + // AudioParameters received during Initialize(). media::AudioParameters params_; @@ -143,7 +147,8 @@ // exclusive when executing; these flags indicate whether one or the other is // in progress. Each method will use the other method's to defer its action. bool godia_in_progress_ = false; - bool switch_output_device_in_progress_ = false; + bool switch_output_device_in_progress_ GUARDED_BY(device_change_lock_) = + false; // Set by GetOutputDeviceInfoAsync() if a SwitchOutputDevice() call is in // progress. GetOutputDeviceInfoAsync() will be invoked again with this value diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc
index 5c8c9be0..6231309b 100644
--- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc
+++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input_test.cc
@@ -215,6 +215,21 @@
mixer_input_->Stop();
}
+TEST_F(AudioRendererMixerInputTest, StopDuringSwitchOutputDevice) {
+ mixer_input_->Initialize(audio_parameters_, fake_callback_.get());
+ mixer_input_->Start();
+ const std::string kDeviceId("mock-device-id");
+ EXPECT_CALL(*this,
+ SwitchCallbackCalled(media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL));
+ base::RunLoop run_loop;
+ mixer_input_->SwitchOutputDevice(
+ kDeviceId,
+ blink::BindOnce(&AudioRendererMixerInputTest::SwitchCallback,
+ Unretained(this), blink::Unretained(&run_loop)));
+ mixer_input_->Stop();
+ run_loop.Run();
+}
+
// Test SwitchOutputDevice() to the same device as the current (default) device
TEST_F(AudioRendererMixerInputTest, SwitchOutputDeviceToSameDevice) {
mixer_input_->Initialize(audio_parameters_, fake_callback_.get());
Original Bug Report
Use-after-free in AudioRendererMixerInput via setSinkId/createMediaElementSource race
Use-after-free in AudioRendererMixerInput via setSinkId/createMediaElementSource race
Summary
A race condition between HTMLMediaElement.setSinkId() and AudioContext.createMediaElementSource() causes a use-after-free of an AudioRendererMixerInput object. The asynchronous device-switch callback OnDeviceSwitchReady captures stale state snapshots of mixer_ and playing_ without holding sink_lock_, then proceeds to re-register the input with a new mixer based on those snapshots. If SetClient() runs concurrently on the main thread and drops the provider’s reference to the input, the callback becomes the sole owner; when it completes, the input is destroyed while still registered in the mixer’s error_callbacks_ set and the AudioConverter’s transform_inputs_ list. Subsequent audio rendering on the AudioOutputDevice thread dereferences the dangling pointers, producing a heap-use-after-free. This affects all desktop platforms (Linux, macOS, Windows) and requires at least two audio output devices to be present.
Bisect
Introducing Commit: 41607b54686f80bc672c294161ca0da1cf49f89f
- Date: 2018-11-30
- Author: Dale Curtis
- Review: https://chromium-review.googlesource.com/c/chromium/src/+/1347795
This commit converted the <audio> pipeline to use asynchronous device info requests. The prior implementation used a static helper for OnDeviceSwitchReady that could safely outlive the pipeline, but the conversion made it a non-static member function bound via RetainedRef(this) without introducing the synchronization necessary to protect against concurrent SetClient() calls.
Root Cause
AudioRendererMixerInput is explicitly documented as not thread-safe. Its header states that callers should rely on WebAudioSourceProviderImpl::sink_lock_ to serialize access between the main thread (WebAudio APIs) and the media thread (HTMLMediaElement APIs). The OnDeviceSwitchReady callback violates this contract: it runs on the media thread’s task runner without acquiring sink_lock_, yet reads and writes mixer_, playing_, callback_, and sink_.
When SwitchOutputDevice() initiates an asynchronous device query, it binds the completion callback with RetainedRef(this):
// audio_renderer_mixer_input.cc:229-232
new_sink->GetOutputDeviceInfoAsync(
blink::BindOnce(&AudioRendererMixerInput::OnDeviceSwitchReady,
blink::RetainedRef(this), std::move(callback), new_sink));
When the async query completes, OnDeviceSwitchReady snapshots the current state and then tears down and rebuilds the mixer connection:
// audio_renderer_mixer_input.cc:327-353
const bool has_mixer = !!mixer_;
const bool is_playing = playing_;
// ... Stop old sink, update device info ...
auto callback = callback_;
Stop();
callback_ = callback;
if (has_mixer) {
Start(); // Registers in new mixer's error_callbacks_
if (is_playing) {
Play(); // Registers in AudioConverter's transform_inputs_
}
}
The race window opens between the state snapshot and the Start()/Play() calls. During this window, AudioContext.createMediaElementSource() on the main thread synchronously calls WebAudioSourceProviderImpl::SetClient(). With the kDelayStopForMediaElementSourceNode feature disabled (the default), SetClient() acquires sink_lock_ and drops the provider’s reference to the AudioRendererMixerInput:
// web_audio_source_provider_impl.cc:147-155
if (!base::FeatureList::IsEnabled(kDelayStopForMediaElementSourceNode)) {
if (sink_) {
sink_->Stop();
sink_ = nullptr; // Drops scoped_refptr<AudioRendererMixerInput>
}
}
After SetClient() returns, the RetainedRef inside the pending OnDeviceSwitchReady callback is the only remaining reference to the AudioRendererMixerInput. When the callback resumes on the media thread, it uses stale has_mixer = true and is_playing = true to call Start() and Play(), which register the input with a freshly obtained mixer via AddErrorCallback(this) and AddMixerInput(params_, this). The mixer stores these as raw pointers:
// audio_renderer_mixer.h:84-85
base::flat_set<raw_ptr<AudioRendererMixerInput, CtnExperimental>>
error_callbacks_ GUARDED_BY(lock_);
// audio_converter.h:129-130
typedef std::list<raw_ptr<InputCallback, CtnExperimental>> InputCallbackSet;
InputCallbackSet transform_inputs_;
When OnDeviceSwitchReady returns and its BindState is destroyed, the RetainedRef releases the last reference, freeing the 440-byte AudioRendererMixerInput object. The destructor only contains DCHECK(!started_) and DCHECK(!mixer_), which are compiled out in Release and ASAN builds, so it does not unregister from the mixer. The AudioOutputDevice thread then enters AudioRendererMixer::Render(), which calls aggregate_converter_.ConvertWithInfo(), iterating transform_inputs_ and calling ProvideInput() on the dangling pointer.
The raw_ptr<T, CtnExperimental> annotations provide BackupRefPtr protection in production Release builds with PartitionAlloc, but ASAN replaces the allocator, disabling this mitigation entirely. In Release builds without ASAN, the CtnExperimental tag enables BRP quarantine that would catch the dangling access, but the underlying race condition and object lifecycle bug remain.
Reproduce
Tested at commit 457566e1c0b41. Apply patch.diff (adds a 500ms sleep in OnDeviceSwitchReady to widen the race window), then build and run:
git apply issue_setsinkid_mixer_uaf/patch.diff
autoninja -C out/asan-release chrome
A virtual audio sink is required if no secondary hardware output is available:
pactl load-module module-null-sink sink_name=virtual_out sink_properties=device.description=VirtualOutput
Launch:
ASAN_OPTIONS=detect_odr_violation=0 xvfb-run -a out/asan-release/chrome \
--no-sandbox --disable-gpu \
--autoplay-policy=no-user-gesture-required \
--use-fake-device-for-media-stream \
--use-fake-ui-for-media-stream \
--user-data-dir=/tmp/poc-$(date +%s) \
issue_setsinkid_mixer_uaf/poc.html
The renderer process crashes with heap-use-after-free within seconds. Full ASAN log:
==3681971==ERROR: AddressSanitizer: heap-use-after-free on address 0x7c56d3894650 at pc 0x7f1749df1dae bp 0x7b11cfffad50 sp 0x7b11cfffad48
READ of size 8 at 0x7c56d3894650 thread T19 (AudioOutputDevi)
#0 0x7f1749df1dad in media::AudioConverter::SourceCallback(int, media::AudioBus*) media/base/audio_converter.cc:224:33
#1 0x7f1749df0c43 in media::AudioConverter::ProvideInput(int, media::AudioBus*) media/base/audio_converter.cc:266:5
#2 0x7f1749df341d in base::internal::Invoker<...> base/functional/bind_internal.h:740:12
#3 0x7f1749e1dbd0 in base::RepeatingCallback<void (int, media::AudioBus*)>::Run(...) base/functional/callback.h:346:12
#4 0x7f1749eb30ea in media::MultiChannelResampler::ProvideInput(int, int, float*) media/base/multi_channel_resampler.cc:101:14
...
#9 0x7f1749df2ee8 in media::AudioConverter::ConvertWithInfo(...) media/base/audio_converter.cc:160:19
#10 0x7f1749e92eb7 in media::LoopbackAudioConverter::ProvideInput(...) media/base/loopback_audio_converter.cc:21:20
#11 0x7f1749df14a2 in media::AudioConverter::SourceCallback(int, media::AudioBus*) media/base/audio_converter.cc:224:33
#12 0x7f1749df2f31 in media::AudioConverter::ConvertWithInfo(...) media/base/audio_converter.cc:157:5
#13 0x7f16e9658328 in blink::AudioRendererMixer::Render(...) audio_renderer_mixer.cc:155:24
#14 0x7f1749d4cde2 in media::AudioOutputDeviceThreadCallback::Process(unsigned int) audio_output_device_thread_callback.cc:107:21
#15 0x7f1749d0a045 in media::AudioDeviceThread::ThreadMain() audio_device_thread.cc:106:16
0x7c56d3894650 is located 16 bytes inside of 440-byte region [0x7c56d3894640,0x7c56d38947f8)
freed by thread T14 (Media) here:
#0 0x55ab875a2cc2 in operator delete(void*, unsigned long)
#1 0x7f16e9661a35 in base::internal::BindState<...>::Destroy(...) base/memory/ref_counted.h:438:5
previously allocated by thread T0 (chrome) here:
#0 0x55ab875a20bd in operator new(unsigned long)
#1 0x7f16e9662184 in blink::AudioRendererMixerManager::CreateInput(...) base/memory/scoped_refptr.h:151:12
#2 0x7f16e964e44e in blink::AudioDeviceFactory::NewMixableSink(...) audio_device_factory.cc:151:26
SUMMARY: AddressSanitizer: heap-use-after-free media/base/audio_converter.cc:224:33 in media::AudioConverter::SourceCallback(int, media::AudioBus*)
MiraclePtr Status: MANUAL ANALYSIS REQUIRED
A pointer to the same region was extracted from a raw_ptr<T> object prior to this crash.
The complete untruncated ASAN log is in asan.log.
References
- audio_renderer_mixer_input.cc (OnDeviceSwitchReady)
- audio_renderer_mixer_input.h (class declaration)
- web_audio_source_provider_impl.cc (SetClient)
- audio_renderer_mixer.h (error_callbacks_ / raw_ptr)
- audio_converter.h (transform_inputs_ / raw_ptr)
- Introducing CL 1347795
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.
- https://chromium-review.googlesource.com/c/chromium/src/+/1347795
- https://source.chromium.org/chromium/chromium/src/+/main:media/base/audio_converter.h;l=129-130
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/media/audio/audio_renderer_mixer.h;l=84-85
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc;l=308-361
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h;l=37-158
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/media/web_audio_source_provider_impl.cc;l=138-184