Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker503502607
Fix commit723e87b3942e (chromium/src) +91/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
WebRtcAudioRendererTrackSourceTest
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
WebRtcAudioRendererTrackSourceTest
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
if
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
modified
if
third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc
modified

Files Changed

  • third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc
From 723e87b3942ed64372fe54014c790745c16f4f73 Mon Sep 17 00:00:00 2001
From: Johannes Kron <kron@chromium.org>
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 <guidou@chromium.org>
Commit-Queue: Johannes Kron <kron@chromium.org>
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>(&params));
+
+  // 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();
Loading diff…

Regression Test / PoC

shipped with the fix
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>(&params));
+
+  // 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() {
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • third_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:

  1. Setup: Create a webpage with two iframes (Frame A and Frame B). In Frame A, establish an RTCPeerConnection and receive a remote audio MediaStreamTrack.
  2. Transfer: Pass this MediaStream from Frame A to Frame B via postMessage and play it in an <audio> element in Frame B.
  3. Destruction: Remove Frame A from the DOM. This causes Frame A’s ExecutionContext to be destroyed, and its PeerConnectionDependencyFactory is marked for Garbage Collection.
  4. Pre-Finalizer Execution: During GC sweeping on the main thread, CleanupPeerConnectionFactory runs. It posts a task to the WebRTC Signaling Thread to release the PeerConnectionFactory.
  5. Asynchronous Teardown: Concurrently, closing the RTCPeerConnection posts 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.
  6. The Race Condition: On the Signaling Thread, the PeerConnectionFactory and its ConnectionContext are destroyed. This drops the last references to the WebRtcAudioDeviceImpl.
  7. Dangling Pointer: WebRtcAudioDeviceImpl::Terminate() is called, setting initialized_ = false, but failing to clear the renderer’s source_ pointer. The object is then destroyed, but the WebRtcAudioRenderer survives because Frame B’s WebMediaPlayerMS still holds a scoped_refptr to it (as the Main Thread hasn’t processed the track-ended event yet).
  8. UAF Execution: The hardware audio thread fires its 10ms callback, executing WebRtcAudioRenderer::Render(), which calls source_->RenderData(...). Since source_ is now a dangling raw_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.

View on issue tracker