CVE-2026-9952
Overview
Background
- `WebAudioDestinationConsumer`
- Interface implemented by
WebAudioMediaStreamSource; itsConsumeAudio()copies float audio data into anAudioPushFifo/AudioBusbacking buffer. - Oilpan pre-finalizers
- Blink GC callbacks run on the main thread during a sweep;
MediaStreamSource::Disposecan delete theWebAudioMediaStreamSourcewhile the audio thread still holds references into its buffers. - `raw_ptr<..., DanglingUntriaged>`
- A non-owning MiraclePtr; the
DanglingUntriagedtag explicitly opts out of dangling-pointer protection, and does not keep the pointee alive.
Root Cause Analysis
Pre-patch, MediaStreamAudioDestinationHandler held its consumer as raw_ptr<WebAudioDestinationConsumer, DanglingUntriaged> destination_consumer_ with the comment “destination_consumer_ is owned by the node’s MediaStreamSource.” That is a non-owning reference: the consumer’s lifetime was controlled entirely by the garbage-collected MediaStreamSource. When Oilpan ran MediaStreamSource::Dispose on the main thread (reachable prematurely once the node’s HasPendingActivity() returned false), it could destroy the WebAudioMediaStreamSource and free the underlying AudioBus float storage. Meanwhile the audio rendering thread, inside Process() → ConsumeAudio(), was concurrently copying audio frames into that storage. Although consumer_lock_ guarded reads/writes of the destination_consumer_ field, it did not keep the pointee alive, so the handler could be holding a dangling consumer whose buffer had just been freed — a cross-thread heap use-after-free write of audio data. The DanglingUntriaged tag means even MiraclePtr/BRP offered no protection, and the audio thread’s base::span slices into the buffer bypass BRP on the AudioBus object entirely.
The patch converts ownership to a reference-counted handle: the constructor, Create, and SetConsumer now take scoped_refptr<WebAudioDestinationConsumer> (moved into place under consumer_lock_), and the member becomes scoped_refptr<WebAudioDestinationConsumer> destination_consumer_ GUARDED_BY(consumer_lock_). The handler now holds a strong reference, so the consumer (and its buffer) cannot be destroyed while the handler still references it, regardless of GC pre-finalizer ordering. The commit message notes an AudioConsumer proxy with a base::Lock/base::AutoTryLock bridge; that proxy’s own source file is not present in this diff preview (the preview truncates after the handler files and the test), so the full locking story is inferred from the message while the shown change clearly establishes shared-ownership lifetime.
Attack Path
- Enable async transitions
Create an
AudioContextunder theAudioContextAsyncStateTransitionsfeature so the context can sit inkSuspendedright after creation (inferred from the report; the gating feature is not in this diff). - Build the graph
Create a
MediaStreamAudioDestinationNodeand connect a source node whoseAudioBufferholds attacker-chosen float payloads. - Start rendering
Call
start(), so the audio thread beginsProcess()→ConsumeAudio()and extractsbase::spanslices into theAudioBusstorage. - Make the node collectible
Drop all JS references; with the context still non-running,
HasPendingActivity()returns false and the node/MediaStreamSourcebecome GC-eligible. - Race the GC
Force a GC so
MediaStreamSource::Disposefrees theWebAudioMediaStreamSource/AudioBuson the main thread while the audio thread is still inside its copy loop. - Corrupt reclaimed memory (inferred) The audio thread writes attacker-controlled floats into the freed block; landing a victim object there yields a controlled UAF write in the renderer, potentially RCE. Reclaim/control are not shown by the diff.
Impact Assessment
AudioConsumer locking proxy are not fully visible in this diff preview.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
Audit Directions
- Other `DanglingUntriaged` audio/media membersGrep
third_party/blink/renderer/platform/mediastreamandmodules/webaudioforraw_ptr<..., DanglingUntriaged>fields touched by the real-time audio thread; each is a candidate for the same lifetime-vs-GC race. - GC eligibility vs. live threadsReview every
HasPendingActivity()override for WebAudio nodes to ensure a node cannot become collectible while a background render thread still processes its graph. - Lock-guards-field-not-pointeeLook for
base::Lock-guarded raw pointers whose pointee is owned elsewhere (especially by Oilpan); the lock serializes access but does not extend lifetime. - `base::span`/`AudioBus` slice captureAudit code that extracts spans into
AudioBus/AlignedHeapArraystorage on one thread while another thread can free the owning object.
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.