CVE-2026-9952
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.ccthird_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.hthird_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc
Patch
From 96ce1273b9c4d4a13ad00045170c8c5eaf2e6d29 Mon Sep 17 00:00:00 2001 From: Hongchan Choi <[email protected]> Date: Mon, 27 Apr 2026 09:17:40 -0700 Subject: [PATCH] [WebAudio] Fix race condition in WebAudioMediaStreamSource disposal Introduce a base::Lock in AudioConsumer to safely bridge the audio thread and the WebAudioMediaStreamSource on the main thread. This prevents a potential race condition during garbage collection destruction. Using base::AutoTryLock prevents blocking the real-time audio thread. Adds comprehensive concurrent destruction tests inside the Blink unit suite. Bug: 503929476 Test: t/b/r/m/webaudio/media_stream_audio_destination_handler_test.cc Change-Id: I80382b24bc7b41702225f55c2151b3fa6bc45042 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7779837 Commit-Queue: Hongchan Choi <[email protected]> Reviewed-by: Michael Wilson <[email protected]> Reviewed-by: Guido Urdaneta <[email protected]> Cr-Commit-Position: refs/heads/main@{#1621143} --- diff --git a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc index 103071a8..50452a4c 100644 --- a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc +++ b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc @@ -32,7 +32,7 @@ MediaStreamAudioDestinationHandler::MediaStreamAudioDestinationHandler( AudioNode& node, uint32_t number_of_channels, - WebAudioDestinationConsumer* webaudio_consumer) + scoped_refptr<WebAudioDestinationConsumer> webaudio_consumer) : AudioHandler(NodeType::kNodeTypeMediaStreamAudioDestination, node, node.context()->sampleRate()), @@ -53,10 +53,10 @@ scoped_refptr<MediaStreamAudioDestinationHandler> MediaStreamAudioDestinationHandler::Create( AudioNode& node, uint32_t number_of_channels, - WebAudioDestinationConsumer* webaudio_consumer) { + scoped_refptr<WebAudioDestinationConsumer> webaudio_consumer) { return base::AdoptRef( new MediaStreamAudioDestinationHandler( - node, number_of_channels, webaudio_consumer)); + node, number_of_channels, std::move(webaudio_consumer))); } MediaStreamAudioDestinationHandler::~MediaStreamAudioDestinationHandler() { @@ -190,7 +190,7 @@ } void MediaStreamAudioDestinationHandler::SetConsumer( - WebAudioDestinationConsumer* destination_consumer, + scoped_refptr<WebAudioDestinationConsumer> destination_consumer, int number_of_channels, float sample_rate) { if (!destination_consumer) { @@ -198,7 +198,7 @@ } base::AutoLock locker(consumer_lock_); - destination_consumer_ = destination_consumer; + destination_consumer_ = std::move(destination_consumer); destination_consumer_->SetFormat(number_of_channels, sample_rate); } diff --git a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.h b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.h index 6d16a3a..e6a6063 100644 --- a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.h +++ b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.h @@ -29,7 +29,7 @@ static scoped_refptr<MediaStreamAudioDestinationHandler> Create( AudioNode&, uint32_t number_of_channels, - WebAudioDestinationConsumer*); + scoped_refptr<WebAudioDestinationConsumer>); MediaStreamAudioDestinationHandler( const MediaStreamAudioDestinationHandler&) = delete; MediaStreamAudioDestinationHandler& operator=( @@ -45,7 +45,9 @@ friend class MediaStreamAudioDestinationHandlerTest; MediaStreamAudioDestinationHandler( - AudioNode&, uint32_t number_of_channels, WebAudioDestinationConsumer*); + AudioNode&, + uint32_t number_of_channels, + scoped_refptr<WebAudioDestinationConsumer>); // AudioHandler void Process(uint32_t frames_to_process) override; @@ -64,7 +66,7 @@ // Sets the WebAudioDestinationConsumer that receives audio data from this // handler. The consumer is then responsible for providing this data to the // MediaStream infrastructure. - void SetConsumer(WebAudioDestinationConsumer*, + void SetConsumer(scoped_refptr<WebAudioDestinationConsumer>, int number_of_channels, float sample_rate); @@ -76,8 +78,11 @@ void SendLogMessage(const String& function_name, const String& message); base::Lock consumer_lock_; - // `destination_consumer_` is owned by the node's MediaStreamSource. - raw_ptr<WebAudioDestinationConsumer, DanglingUntriaged> + // `destination_consumer_` is the `AudioConsumer` proxy that acts as a + // thread-safe bridge between the AudioHandler on the real-time audio thread + // and the WebAudioMediaStreamSource on the main thread. It handles proper + // synchronization to prevent Use-After-Free issues during garbage collection. + scoped_refptr<WebAudioDestinationConsumer> destination_consumer_ GUARDED_BY(consumer_lock_); Vector<const float*> consumer_bus_wrapper_ GUARDED_BY(consumer_lock_); diff --git a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc index c946912f..306ccff 100644 --- a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc +++ b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc @@ -7,6 +7,8 @@ #include <memory> #include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/thread_pool.h" #include "base/test/metrics/histogram_tester.h" #include "build/build_config.h" #include "media/base/audio_bus.h" @@ -31,6 +33,7 @@ #include "third_party/blink/renderer/modules/webaudio/testing/mock_web_audio_device.h" #include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h" #include "third_party/blink/renderer/platform/mediastream/webaudio_destination_consumer.h" +#include "third_party/blink/renderer/platform/mediastream/webaudio_media_stream_source.h" #include "third_party/blink/renderer/platform/testing/task_environment.h" #include "third_party/blink/renderer/platform/testing/testing_platform_support.h" #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" @@ -76,14 +79,22 @@ node_ = MediaStreamAudioDestinationNode::Create( *audio_context, 2, ASSERT_NO_EXCEPTION); bus_ = AudioBus::Create(2, 10); + consumer_ = + base::MakeRefCounted<StrictMock<MockWebAudioDestinationConsumer>>(); + } + + void TearDown() override { + // Clear the consumer to avoid leaking the mock object. + CallRemoveDestinationConsumer(); } ~MediaStreamAudioDestinationHandlerTest() override = default; - void CallSetDestinationConsumer(WebAudioDestinationConsumer* consumer, - int num_channels, - float sample_rate) { - Handler().SetConsumer(consumer, num_channels, sample_rate); + void CallSetDestinationConsumer( + scoped_refptr<WebAudioDestinationConsumer> consumer, + int num_channels, + float sample_rate) { + Handler().SetConsumer(std::move(consumer), num_channels, sample_rate); } bool CallRemoveDestinationConsumer() { @@ -100,7 +111,7 @@ protected: Persistent<MediaStreamAudioDestinationNode> node_; - StrictMock<MockWebAudioDestinationConsumer> consumer_; + scoped_refptr<StrictMock<MockWebAudioDestinationConsumer>> consumer_; scoped_refptr<AudioBus> bus_; ScopedTestingPlatformSupport<AudioContextTestPlatform> platform_; test::TaskEnvironment task_environment_; @@ -113,29 +124,29 @@ TEST_F(MediaStreamAudioDestinationHandlerTest, SetDestinationConsumer) { // Expect SetFormat() to be called with these arguments. - EXPECT_CALL(consumer_, SetFormat(2, 44100)); - CallSetDestinationConsumer(&consumer_, 2, 44100); + EXPECT_CALL(*consumer_, SetFormat(2, 44100)); + CallSetDestinationConsumer(consumer_, 2, 44100); - EXPECT_CALL(consumer_, ConsumeAudio(_, 10)); + EXPECT_CALL(*consumer_, ConsumeAudio(_, 10)); CallConsumeAudio(bus_.get(), 10); } TEST_F(MediaStreamAudioDestinationHandlerTest, RemoveDestinationConsumer) { - EXPECT_CALL(consumer_, SetFormat(2, 44100)); - CallSetDestinationConsumer(&consumer_, 2, 44100); + EXPECT_CALL(*consumer_, SetFormat(2, 44100)); + CallSetDestinationConsumer(consumer_, 2, 44100); // The removal should be successful. EXPECT_TRUE(CallRemoveDestinationConsumer()); // The consumer should not be called. - EXPECT_CALL(consumer_, ConsumeAudio(_, 10)).Times(0); + EXPECT_CALL(*consumer_, ConsumeAudio(_, 10)).Times(0); CallConsumeAudio(bus_.get(), 10);
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc
index c946912f..306ccff 100644
--- a/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler_test.cc
@@ -7,6 +7,8 @@
#include <memory>
#include "base/synchronization/lock.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/task/thread_pool.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "media/base/audio_bus.h"
@@ -31,6 +33,7 @@
#include "third_party/blink/renderer/modules/webaudio/testing/mock_web_audio_device.h"
#include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h"
#include "third_party/blink/renderer/platform/mediastream/webaudio_destination_consumer.h"
+#include "third_party/blink/renderer/platform/mediastream/webaudio_media_stream_source.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
@@ -76,14 +79,22 @@
node_ = MediaStreamAudioDestinationNode::Create(
*audio_context, 2, ASSERT_NO_EXCEPTION);
bus_ = AudioBus::Create(2, 10);
+ consumer_ =
+ base::MakeRefCounted<StrictMock<MockWebAudioDestinationConsumer>>();
+ }
+
+ void TearDown() override {
+ // Clear the consumer to avoid leaking the mock object.
+ CallRemoveDestinationConsumer();
}
~MediaStreamAudioDestinationHandlerTest() override = default;
- void CallSetDestinationConsumer(WebAudioDestinationConsumer* consumer,
- int num_channels,
- float sample_rate) {
- Handler().SetConsumer(consumer, num_channels, sample_rate);
+ void CallSetDestinationConsumer(
+ scoped_refptr<WebAudioDestinationConsumer> consumer,
+ int num_channels,
+ float sample_rate) {
+ Handler().SetConsumer(std::move(consumer), num_channels, sample_rate);
}
bool CallRemoveDestinationConsumer() {
@@ -100,7 +111,7 @@
protected:
Persistent<MediaStreamAudioDestinationNode> node_;
- StrictMock<MockWebAudioDestinationConsumer> consumer_;
+ scoped_refptr<StrictMock<MockWebAudioDestinationConsumer>> consumer_;
scoped_refptr<AudioBus> bus_;
ScopedTestingPlatformSupport<AudioContextTestPlatform> platform_;
test::TaskEnvironment task_environment_;
@@ -113,29 +124,29 @@
TEST_F(MediaStreamAudioDestinationHandlerTest, SetDestinationConsumer) {
// Expect SetFormat() to be called with these arguments.
- EXPECT_CALL(consumer_, SetFormat(2, 44100));
- CallSetDestinationConsumer(&consumer_, 2, 44100);
+ EXPECT_CALL(*consumer_, SetFormat(2, 44100));
+ CallSetDestinationConsumer(consumer_, 2, 44100);
- EXPECT_CALL(consumer_, ConsumeAudio(_, 10));
+ EXPECT_CALL(*consumer_, ConsumeAudio(_, 10));
CallConsumeAudio(bus_.get(), 10);
}
TEST_F(MediaStreamAudioDestinationHandlerTest, RemoveDestinationConsumer) {
- EXPECT_CALL(consumer_, SetFormat(2, 44100));
- CallSetDestinationConsumer(&consumer_, 2, 44100);
+ EXPECT_CALL(*consumer_, SetFormat(2, 44100));
+ CallSetDestinationConsumer(consumer_, 2, 44100);
// The removal should be successful.
EXPECT_TRUE(CallRemoveDestinationConsumer());
// The consumer should not be called.
- EXPECT_CALL(consumer_, ConsumeAudio(_, 10)).Times(0);
+ EXPECT_CALL(*consumer_, ConsumeAudio(_, 10)).Times(0);
CallConsumeAudio(bus_.get(), 10);
}
TEST_F(MediaStreamAudioDestinationHandlerTest,
ConsumeInvalidDestinationConsumer) {
// The consumer should get no calls.
- EXPECT_CALL(consumer_, ConsumeAudio(_, 10)).Times(0);
+ EXPECT_CALL(*consumer_, ConsumeAudio(_, 10)).Times(0);
CallConsumeAudio(bus_.get(), 10);
}
@@ -150,4 +161,48 @@
EXPECT_FALSE(CallRemoveDestinationConsumer());
}
+TEST_F(MediaStreamAudioDestinationHandlerTest, AudioConsumerStressTest) {
+ std::atomic<bool> stop_requested{false};
+ base::WaitableEvent done_event;
+
+ Vector<float> data_l(128, 0.0f);
+ Vector<float> data_r(128, 0.0f);
+ Vector<const float*> audio_data = {data_l.data(), data_r.data()};
+
+ auto source = std::make_unique<WebAudioMediaStreamSource>(
+ task_environment_.GetMainThreadTaskRunner());
+ scoped_refptr<WebAudioDestinationConsumer> consumer = source->Consumer();
+ consumer->SetFormat(2, 44100);
+
+ // Start a background thread to continuously consume audio data.
+ base::ThreadPool::PostTask(
+ FROM_HERE, {base::MayBlock()},
+ base::BindOnce(
+ [](scoped_refptr<WebAudioDestinationConsumer> consumer,
+ Vector<const float*> audio_data, std::atomic<bool>* stop,
+ base::WaitableEvent* done) {
+ while (!stop->load(std::memory_order_relaxed)) {
+ consumer->ConsumeAudio(audio_data, 10);
+ }
+ done->Signal();
+ },
+ consumer, audio_data, base::Unretained(&stop_requested),
+ base::Unretained(&done_event)));
+
+ // Stress test by recreating the media source on the main thread while the
+ // background thread continuously uses the *original* consumer. This verifies
+ // that the background thread can safely use the consumer even after the
+ // source that created it has been destroyed (testing Detach() safety).
+ for (int i = 0; i < 500; ++i) {
+ source.reset();
+ source = std::make_unique<WebAudioMediaStreamSource>(
+ task_environment_.GetMainThreadTaskRunner());
+ consumer = source->Consumer();
+ consumer->SetFormat(2, 44100);
+ }
+
+ stop_requested.store(true, std::memory_order_relaxed);
+ done_event.Wait();
+}
+
} // namespace blink
Original Bug Report
Potential cross-thread heap use-after-free in AudioPushFifo via GC race
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential cross-thread race condition between the main thread garbage collector and the audio rendering thread can cause a heap use-after-free write in AudioPushFifo. A pre-finalizer may destroy the underlying audio buffer while the background audio thread is actively writing to it.
Affected files:
media/base/audio_push_fifo.ccthird_party/blink/renderer/platform/mediastream/media_stream_source.ccthird_party/blink/renderer/platform/mediastream/webaudio_media_stream_source.ccthird_party/blink/renderer/platform/mediastream/media_stream_audio_source.ccthird_party/blink/renderer/modules/webaudio/media_stream_audio_destination_handler.cc
Estimated timestamp from git blame: 2026-01-23
Summary
A potential heap use-after-free (UAF) vulnerability exists in the WebAudio to MediaStream pipeline. A lack of proper synchronization during garbage collection (GC) allows the MediaStreamSource pre-finalizer to destroy a WebAudioMediaStreamSource and its associated AudioPushFifo buffer while the audio rendering thread is concurrently writing audio frames into that buffer. This results in an out-of-bounds/Use-After-Free write of attacker-controlled audio float data into freed heap memory.
Technical Details
- When a
MediaStreamAudioDestinationNodeis created, it allocates aWebAudioMediaStreamSourceand then wraps it in a garbage-collectedMediaStreamSource. - Due to Blink’s Oilpan LIFO (reverse-registration) execution order for pre-finalizers,
MediaStreamSource::Disposewill execute beforeMediaStreamAudioDestinationNode::Disposeduring a GC sweep. - The audio thread processes data via
MediaStreamAudioDestinationHandler::Process, which acquiresconsumer_lock_and forwards audio toWebAudioMediaStreamSource::ConsumeAudio. - Inside
ConsumeAudio, the audio data is pushed into anAudioPushFifo. The audio thread extracts stack-localbase::spanreferences pointing directly to theAudioBus’s underlying memory (base::AlignedHeapArray<float>) and begins a copy loop (CopyPartialFramesTo). - If the
AudioContextAsyncStateTransitionsfeature is enabled, theAudioContextmay still be in thekSuspendedstate immediately after creation, causingMediaStreamAudioDestinationNode::HasPendingActivity()to returnfalse. This makes the node and itsMediaStreamSourceunexpectedly eligible for garbage collection, even though the background render thread is actively processing the graph. - The GC executes
MediaStreamSource::Dispose()on the main thread, which directly deletes theWebAudioMediaStreamSource(and consequently frees theAudioBus’s float array) without acquiringconsumer_lock_. - The audio thread, completely unaware of the destruction, continues its copy loop, writing attacker-controlled float values into the now-freed
AlignedHeapArraymemory.
MiraclePtr (BackupRefPtr) does not protect against this vulnerability because the audio thread accesses the buffer memory via raw base::span slices that were retrieved before the object’s destruction, bypassing the BRP quarantine mechanisms for the AudioBus object itself.
Suggested Exploit Steps
(Note: These are potential steps to trigger the vulnerability, as our automated tooling cannot run code to verify a proof-of-concept.)
- Create an
AudioContextwithAudioContextAsyncStateTransitionsenabled. - Create an
AudioBufferSourceNodepopulated with a craftedAudioBuffercontaining malicious pointers and forged vtables encoded as float values. - Create a
MediaStreamAudioDestinationNodeand connect the source node to it. - Call
start()on the source node to initiate audio processing. - Immediately remove all Javascript references to the destination node and trigger garbage collection before the
AudioContextfully transitions to thekRunningstate. - The GC will free the
AudioBusmemory while the audio thread is concurrently copying the crafted float payload into it. By allocating a victim object into the freed block during the race window, the UAF write will corrupt the victim object, potentially leading to arbitrary Remote Code Execution (RCE) in the renderer.
Suggested Fix
There are multiple potential ways to address this issue:
- Prevent Premature GC: Modify
MediaStreamAudioDestinationNode::HasPendingActivity()to returntruewhenever an async state transition is pending. This prevents the nodes from being collected while the audio thread might be starting up. - Ensure Safe Destruction Order: The audio thread relies on
MediaStreamAudioDestinationNode::Dispose()to acquireconsumer_lock_and nullifydestination_consumer_. Ensure this cleanup runs before the underlyingMediaStreamSourcedestroys theWebAudioMediaStreamSource. - Use Smart Pointers/Thread-Safe Destruction: Manage the lifecycle of
WebAudioMediaStreamSourceacross threads usingscoped_refptrinstead of relying entirely on the GC pre-finalizer order.
Evaluated with Chrome root at commit: 2b349e31cb87959d6a548625986c65e0a2d2e380
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.