High patch-now ~ plausible CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Sub-componentBlink>WebRTC
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Real bug is lifetime: renderer's raw source_ outlives terminated ADM; 'unsynchronized check' framing secondary. Claim 'every access lock-guarded, dangling deref eliminated' unsupported: SwitchOutputDevice checks under lock, releases, later re-dereferences source_ (TOCTOU); diff truncated. Web trigger path unshown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker503502607
Fix commit723e87b3942e (chromium/src) +91/-12
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-17 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-06-02
Patch gap41 days — fix public in source before the advisory

Background

`WebRtcAudioDeviceImpl`
the Blink audio device module (ADM) that, together with WebRtcAudioRenderer, plays out WebRTC audio; the two hold references to each other during normal operation.
`WebRtcAudioRenderer::source_`
the renderer’s pointer to its audio source; before the patch it was accessed on several paths without consistently holding lock_ and without being cleared when the source went away.
`WebRtcAudioDeviceImpl::Terminate`
teardown of the ADM, which stops recording/playout and previously left the renderer’s source_ pointer intact even as the circular reference was torn down.
`SwitchOutputDevice`
a WebRtcAudioRenderer operation (reachable via the renderer proxy) that dereferences source_ to change the audio output device.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Real bug is lifetime: renderer's raw source_ outlives terminated ADM; 'unsynchronized check' framing secondary. Claim 'every access lock-guarded, dangling deref eliminated' unsupported: SwitchOutputDevice checks under lock, releases, later re-dereferences source_ (TOCTOU); diff truncated. Web trigger path unshown.

WebRtcAudioDeviceImpl and WebRtcAudioRenderer formed a circular reference, and when the ADM’s Terminate ran it never told the renderer to drop its source_ pointer. Meanwhile WebRtcAudioRenderer accessed source_ on paths such as SwitchOutputDevice with only an unsynchronized if (!source_) check and a separate lock scope, so after the source was torn down the renderer could dereference a stale source_ — a use-after-free.

The patch breaks the cycle explicitly: Terminate now captures the renderer under lock_, clears capturers_, releases the lock (to avoid a deadlock), and calls the new DisconnectSource(), which sets source_ = nullptr under lock_. It also hardens the renderer to tolerate a null source: SwitchOutputDevice now reads source_ under lock_ into a has_source flag and returns OUTPUT_DEVICE_STATUS_ERROR_INTERNAL when it is null, and the Stop/teardown path guards source_->RemoveAudioRenderer(this) with if (source_). With source_ cleared under the lock on termination and every access lock-guarded and null-checked, the dangling dereference is eliminated.

Key insight
The core mistake was a circular device/renderer reference that was never broken on termination, leaving the renderer’s source_ pointer dangling and dereferenced without lock protection; the fix adds DisconnectSource() (called from Terminate after releasing the lock) to null source_ under lock_ and makes all source_ accesses lock-guarded and null-tolerant.

Attack Path

  1. Set up device and renderer A WebRTC audio session connects a WebRtcAudioDeviceImpl to a WebRtcAudioRenderer, and a shared renderer proxy is created, establishing the mutual references.
  2. Terminate the audio source The ADM is terminated (Terminate), tearing down the source while, pre-patch, the renderer’s source_ pointer is left pointing at the now-invalid source. (inferred)
  3. Access the stale source An operation that touches the source — such as SwitchOutputDevice on the renderer proxy, as the SourceDisconnectedOnDeviceTerminate regression test exercises — dereferences the dangling source_. (inferred)
  4. Use-after-free The renderer reads/calls through the freed source pointer, corrupting or reading reclaimed memory instead of failing gracefully. (inferred)

Impact Assessment

An attacker able to drive WebRTC audio setup and teardown timing in a page can trigger a use-after-free of the audio source in the renderer process when device termination races renderer operations like output-device switching. Successful exploitation could lead to memory corruption and potentially renderer code execution (inferred); the advisory classifies this as a use-after-free in WebRTC and does not report exploitation in the wild.

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

Audit Directions

  • Pattern
    Look for objects that hold mutual (circular) references where one side’s teardown/Terminate does not explicitly notify the other to drop its pointer, leaving a dangling reference.
  • Pattern
    Audit pointers protected by a lock that are nonetheless read or dereferenced outside the lock (e.g. an early if (!ptr) guard separate from the locked scope that actually uses ptr).
  • Pattern
    Review WebRTC audio device/renderer/source lifetimes for accesses to a source pointer that assume it outlives the accessor, and confirm each is null-checked after possible disconnection.
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