CVE-2026-10939
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
WebRtcAudioRendererTrackSourceTestthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
WebRtcAudioRendererTrackSourceTestthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
ifthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc |
modified | |
ifthird_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc |
modified |
Files Changed
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc
Patch
From 723e87b3942ed64372fe54014c790745c16f4f73 Mon Sep 17 00:00:00 2001 From: Johannes Kron <[email protected]> Date: Wed, 22 Apr 2026 15:34:12 -0700 Subject: [PATCH] Break circular reference on WebRTC source termination The WebRtcAudioDeviceImpl and WebRtcAudioRenderer hold references to each other during normal operation. To prevent memory leaks or unexpected behavior when the audio source is terminated, this circular dependency must be explicitly broken. This change introduces a DisconnectSource() method to the audio renderer. When WebRtcAudioDeviceImpl::Terminate() is called, the device now clears its internal capturers, releases its lock to avoid deadlocks, and calls this new method to drop the renderer's reference to the source. Additionally, WebRtcAudioRenderer is updated to gracefully handle a null source. Access to the source is strictly guarded by locks, preventing crashes if operations like stopping or switching the output device occur after the source has been disconnected. Fixed: 503502607 Change-Id: I6f1491aabb7de9734213a79e119dfc1dd3d0b4c7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7780754 Reviewed-by: Guido Urdaneta <[email protected]> Commit-Queue: Johannes Kron <[email protected]> Cr-Commit-Position: refs/heads/main@{#1619143} --- diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc index 98e10d07..d6ba2ba 100644 --- a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc +++ b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc @@ -13,6 +13,7 @@ #include "base/memory/raw_ptr.h" #include "base/memory/scoped_refptr.h" #include "base/run_loop.h" +#include "base/test/test_future.h" #include "base/time/time.h" #include "build/build_config.h" #include "media/audio/audio_sink_parameters.h" @@ -36,6 +37,7 @@ #include "third_party/blink/public/web/web_view.h" #include "third_party/blink/renderer/modules/mediastream/media_stream_audio_renderer.h" #include "third_party/blink/renderer/modules/peerconnection/mock_peer_connection_dependency_factory.h" +#include "third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h" #include "third_party/blink/renderer/platform/mediastream/media_stream_audio_source.h" #include "third_party/blink/renderer/platform/mediastream/media_stream_component.h" #include "third_party/blink/renderer/platform/mediastream/media_stream_component_impl.h" @@ -550,6 +552,51 @@ loop.Run(); } +TEST_F(WebRtcAudioRendererTest, SourceDisconnectedOnDeviceTerminate) { + scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device( + new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()); + + // Alias the ADM interface to avoid inline static_casts. + webrtc::AudioDeviceModule* adm = audio_device.get(); + adm->Init(); + + // Instantiate the renderer directly to bypass test helper mocks. + auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>( + scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_, + *web_local_frame_, base::UnguessableToken::Create(), + kDefaultOutputDeviceId, base::RepeatingCallback<void()>()); + + media::AudioSinkParameters params; + EXPECT_CALL(*audio_device_factory_platform_, + MockNewAudioRendererSink(blink::WebAudioDeviceSourceType::kWebRtc, + web_local_frame_.get(), _)) + .WillOnce(SaveArg<2>(¶ms)); + + // Connect the device and renderer. + EXPECT_TRUE(audio_device->SetAudioRenderer(renderer.get())); + + auto renderer_proxy = + renderer->CreateSharedAudioRendererProxy(stream_descriptor_); + + // Terminate the device to trigger DisconnectSource(). + adm->Terminate(); + + // Nullify local pointers to drop the reference and avoid triggering the + // dangling pointer detector. + adm = nullptr; + audio_device = nullptr; + + // Force the renderer to access `source_`. It should gracefully fail with an + // internal error instead of triggering a UAF. + base::test::TestFuture<media::OutputDeviceStatus> future; + renderer_proxy->SwitchOutputDevice(kOtherOutputDeviceId, + future.GetCallback()); + EXPECT_EQ(future.Get(), media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL); + // Clean up. + renderer_proxy->Start(); + renderer_proxy->Stop(); +} + class WebRtcAudioRendererTrackSourceTest : public WebRtcAudioRendererTest { public: WebRtcAudioRendererTrackSourceTest() { diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc index dfb5d189..5935f96 100644 --- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc +++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc @@ -212,13 +212,21 @@ StopRecording(); StopPlayout(); + // Temporarily hold the audio renderer so that we can disconnect the source + // from it after we have released the lock, avoiding a deadlock. + scoped_refptr<blink::WebRtcAudioRenderer> renderer_to_disconnect; { base::AutoLock auto_lock(lock_); DCHECK(!renderer_ || !renderer_->IsStarted()) << "The shared audio renderer shouldn't be running"; + renderer_to_disconnect = renderer_; capturers_.clear(); } + if (renderer_to_disconnect) { + renderer_to_disconnect->DisconnectSource(); + } + initialized_ = false; return 0; } diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc index 632689b4..18417a8 100644 --- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc +++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc @@ -345,14 +345,19 @@ // User must call Play() before any audio can be heard. state_ = kPaused; + source_->SetOutputDeviceForAec(output_device_id_); } - source_->SetOutputDeviceForAec(output_device_id_); sink_->Start(); sink_->Play(); // Not all the sinks play on start. return true; } +void WebRtcAudioRenderer::DisconnectSource() { + base::AutoLock auto_lock(lock_); + source_ = nullptr; +} + scoped_refptr<MediaStreamAudioRenderer> WebRtcAudioRenderer::CreateSharedAudioRendererProxy( MediaStreamDescriptor* media_stream_descriptor) { @@ -466,8 +471,10 @@ return; audio_stream_tracker_.reset(); - source_->RemoveAudioRenderer(this); - source_ = nullptr; + if (source_) { + source_->RemoveAudioRenderer(this); + source_ = nullptr; + } state_ = kUninitialized; } @@ -514,18 +521,23 @@ SendLogMessage( UNSAFE_TODO(String::Format("%s({device_id=%s} [state=%s])", __func__, device_id.c_str(), StateToString(state_)))); - if (!source_) { + + bool has_source = false; + { + base::AutoLock auto_lock(lock_); + has_source = (source_ != nullptr); + if (has_source) { + DCHECK_NE(state_, kUninitialized); + } + } + + if (!has_source) { SendLogMessage(String::Format( "%s => (ERROR: OUTPUT_DEVICE_STATUS_ERROR_INTERNAL)", __func__)); std::move(callback).Run(media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL); return; } - { - base::AutoLock auto_lock(lock_); - DCHECK_NE(state_, kUninitialized); - } - auto* web_frame = static_cast<WebLocalFrame*>(WebFrame::FromCoreFrame(source_frame_)); if (!web_frame) { @@ -568,9 +580,11 @@ output_device_id_ = String::FromUtf8(device_id); { base::AutoLock auto_lock(lock_); - source_->AudioRendererThreadStopped();
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
index 98e10d07..d6ba2ba 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
@@ -13,6 +13,7 @@
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
+#include "base/test/test_future.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "media/audio/audio_sink_parameters.h"
@@ -36,6 +37,7 @@
#include "third_party/blink/public/web/web_view.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_audio_renderer.h"
#include "third_party/blink/renderer/modules/peerconnection/mock_peer_connection_dependency_factory.h"
+#include "third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_audio_source.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_component.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_component_impl.h"
@@ -550,6 +552,51 @@
loop.Run();
}
+TEST_F(WebRtcAudioRendererTest, SourceDisconnectedOnDeviceTerminate) {
+ scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device(
+ new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>());
+
+ // Alias the ADM interface to avoid inline static_casts.
+ webrtc::AudioDeviceModule* adm = audio_device.get();
+ adm->Init();
+
+ // Instantiate the renderer directly to bypass test helper mocks.
+ auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>(
+ scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_,
+ *web_local_frame_, base::UnguessableToken::Create(),
+ kDefaultOutputDeviceId, base::RepeatingCallback<void()>());
+
+ media::AudioSinkParameters params;
+ EXPECT_CALL(*audio_device_factory_platform_,
+ MockNewAudioRendererSink(blink::WebAudioDeviceSourceType::kWebRtc,
+ web_local_frame_.get(), _))
+ .WillOnce(SaveArg<2>(¶ms));
+
+ // Connect the device and renderer.
+ EXPECT_TRUE(audio_device->SetAudioRenderer(renderer.get()));
+
+ auto renderer_proxy =
+ renderer->CreateSharedAudioRendererProxy(stream_descriptor_);
+
+ // Terminate the device to trigger DisconnectSource().
+ adm->Terminate();
+
+ // Nullify local pointers to drop the reference and avoid triggering the
+ // dangling pointer detector.
+ adm = nullptr;
+ audio_device = nullptr;
+
+ // Force the renderer to access `source_`. It should gracefully fail with an
+ // internal error instead of triggering a UAF.
+ base::test::TestFuture<media::OutputDeviceStatus> future;
+ renderer_proxy->SwitchOutputDevice(kOtherOutputDeviceId,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL);
+ // Clean up.
+ renderer_proxy->Start();
+ renderer_proxy->Stop();
+}
+
class WebRtcAudioRendererTrackSourceTest : public WebRtcAudioRendererTest {
public:
WebRtcAudioRendererTrackSourceTest() {
Original Bug Report
Potential Use-After-Free in WebRtcAudioRenderer via dangling source_ pointer
Flapjack, 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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A potential Use-After-Free exists in WebRtcAudioRenderer because it retains a dangling raw pointer to its source (WebRtcAudioDeviceImpl) after the source is destroyed. In cross-frame scenarios, the source can be destroyed asynchronously on the signaling thread while the renderer is kept alive by another frame, leading to a race condition where the audio thread accesses freed memory.
Affected files:
third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
Estimated timestamp from git blame: 2026-01-09
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in the WebRTC audio rendering pipeline in Blink. The WebRtcAudioRenderer class maintains a raw_ptr to a WebRtcAudioRendererSource (which is implemented by WebRtcAudioDeviceImpl). When the WebRtcAudioDeviceImpl is destroyed asynchronously on the signaling thread, it fails to safely nullify this pointer in the renderer. In cross-frame scenarios, the WebRtcAudioRenderer can outlive the WebRtcAudioDeviceImpl, leaving the high-priority audio thread to access freed memory during its callback cycle.
Note: The following sequences are potential steps derived from deep code analysis. Our tooling agent does not yet have the ability to execute code to produce a working proof of concept.
Technical Details & Lifetime Mismatch
In third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.h, WebRtcAudioRenderer holds a reference to its source:
raw_ptr<WebRtcAudioRendererSource> source_;
To avoid a reference cycle, WebRtcAudioDeviceImpl holds a scoped_refptr to the renderer, while the renderer holds a raw_ptr back to the device.
When a MediaStreamTrack is created in Frame A and passed to an <audio> element in Frame B, Frame B’s WebMediaPlayerMS creates a proxy that holds a scoped_refptr to the WebRtcAudioRenderer. The renderer points to Frame A’s WebRtcAudioDeviceImpl.
Potential Trigger Sequence
An attacker could potentially trigger this vulnerability using the following steps:
- Setup: Create a webpage with two iframes (Frame A and Frame B). In Frame A, establish an
RTCPeerConnectionand receive a remote audioMediaStreamTrack. - Transfer: Pass this
MediaStreamfrom Frame A to Frame B viapostMessageand play it in an<audio>element in Frame B. - Destruction: Remove Frame A from the DOM. This causes Frame A’s
ExecutionContextto be destroyed, and itsPeerConnectionDependencyFactoryis marked for Garbage Collection. - Pre-Finalizer Execution: During GC sweeping on the main thread,
CleanupPeerConnectionFactoryruns. It posts a task to the WebRTC Signaling Thread to release thePeerConnectionFactory. - Asynchronous Teardown: Concurrently, closing the
RTCPeerConnectionposts a task to the Signaling Thread. The track state changes to “Ended”, which posts an asynchronous notification back to the Main Thread so Frame B can stop playback. - The Race Condition: On the Signaling Thread, the
PeerConnectionFactoryand itsConnectionContextare destroyed. This drops the last references to theWebRtcAudioDeviceImpl. - Dangling Pointer:
WebRtcAudioDeviceImpl::Terminate()is called, settinginitialized_ = false, but failing to clear the renderer’ssource_pointer. The object is then destroyed, but theWebRtcAudioRenderersurvives because Frame B’sWebMediaPlayerMSstill holds ascoped_refptrto it (as the Main Thread hasn’t processed the track-ended event yet). - UAF Execution: The hardware audio thread fires its 10ms callback, executing
WebRtcAudioRenderer::Render(), which callssource_->RenderData(...). Sincesource_is now a danglingraw_ptr, a Use-After-Free occurs.
Impact
On desktop platforms where MiraclePtr (BackupRefPtr) is enabled, this vulnerability typically results in a deterministic crash (Denial of Service). However, MiraclePtr is currently disabled by default in Renderer processes on non-desktop Android platforms due to performance constraints. On these platforms, the memory is immediately returned to the allocator. Because the UAF triggers a virtual method call (RenderData), an attacker who reclaims the memory with a fake vtable from Frame B’s JavaScript could potentially achieve Remote Code Execution (RCE) within the renderer sandbox.
Suggested Fix
WebRtcAudioDeviceImpl should explicitly clear the renderer’s source_ pointer before it is destroyed. Since Terminate() and the ADM’s destructor run on the Signaling Thread, while the renderer’s source_ pointer is protected by WebRtcAudioRenderer::lock_, the ADM could acquire the renderer’s lock and set source_ = nullptr during termination. Alternatively, the architecture could be refactored to use thread-safe weak pointers (base::WeakPtr if bound to the correct sequence) or explicit cross-thread teardown signals to guarantee the renderer stops pulling data before the ADM is freed.
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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.