Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactObject lifecycle issue in WebRTC
DescriptionObject lifecycle issue in WebRTC
ComponentWebRTC
Bug ClassLogic Error
Tracker523692228
Fix commitff81618204d4 (chromium/src) +143/-120
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
modified
WebRtcAudioDeviceImplReleaseTest
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
modified
WebRtcAudioDeviceImplReleaseTest
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
modified
if
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
From ff81618204d47311fe2fe21f5069317793b0f140 Mon Sep 17 00:00:00 2001
From: Johannes Kron <kron@chromium.org>
Date: Mon, 15 Jun 2026 06:37:20 -0700
Subject: [PATCH] Clear renderer_ in WebRtcAudioDeviceImpl::Terminate

To prevent WebRtcAudioDeviceImpl from retaining a stale strong reference
to WebRtcAudioRenderer after termination, use std::move(renderer_) when
populating renderer_to_disconnect. This successfully breaks the circular
dependency between the ADM and renderer, resolving a per-iframe WebRTC
memory leak.

Additionally, add a new unit test TerminateReleasesRenderer to verify
this behavior, and refactor the existing teardown unit tests to share
a common helper class.

Fixed: 523692228
Change-Id: Ie0d050657c84bf71420b95391272193de6f3b7fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7942489
Reviewed-by: Guido Urdaneta <guidou@chromium.org>
Commit-Queue: Johannes Kron <kron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1646766}
---

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 ce0fc50..e21f8ed 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
@@ -220,7 +220,7 @@
   scoped_refptr<blink::WebRtcAudioRenderer> renderer_to_disconnect;
   {
     base::AutoLock auto_lock(lock_);
-    renderer_to_disconnect = renderer_;
+    renderer_to_disconnect = std::move(renderer_);
     capturers_.clear();
   }
 
diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
index b56bfda..3d43a5f3 100644
--- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
+++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
@@ -7,6 +7,7 @@
 #include <memory>
 
 #include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
 #include "base/threading/thread.h"
 #include "base/time/time.h"
@@ -196,138 +197,160 @@
 base::PlatformThreadId TestWebRtcAudioRenderer::destructor_thread_id_ =
     base::kInvalidThreadId;
 
-TEST_F(WebRtcAudioDeviceImplTest, ReleaseRendererSoonWithShutDownQueue) {
-  // 1. Setup Blink environment. We need a real WebView and WebLocalFrame
-  // to get a frame-associated task runner for the renderer.
+class WebRtcAudioDeviceImplReleaseTest : public WebRtcAudioDeviceImplTest {
+ public:
+  WebRtcAudioDeviceImplReleaseTest() {
+    agent_group_scheduler_ =
+        std::make_unique<blink::scheduler::WebAgentGroupScheduler>(
+            ThreadScheduler::Current()
+                ->ToMainThreadScheduler()
+                ->CreateAgentGroupScheduler());
+
+    web_view_ = blink::WebView::Create(
+        /*client=*/nullptr,
+        /*is_hidden=*/false,
+        /*prerender_param=*/nullptr,
+        /*fenced_frame_mode=*/std::nullopt,
+        /*compositing_enabled=*/false,
+        /*widgets_never_composited=*/false,
+        /*opener=*/nullptr, mojo::NullAssociatedReceiver(),
+        *agent_group_scheduler_,
+        /*session_storage_namespace_id=*/std::string(),
+        /*page_base_background_color=*/std::nullopt,
+        /*browsing_context_group_token=*/base::UnguessableToken::Create(),
+        /*color_provider_colors=*/nullptr,
+        /*history_index=*/-1,
+        /*history_length=*/0);
+
+    web_local_frame_ = blink::WebLocalFrame::CreateMainFrame(
+        web_view_, &web_local_frame_client_, nullptr, mojo::NullRemote(),
+        LocalFrameToken(), DocumentToken(),
+        /*policy_container=*/nullptr);
+
+    MediaStreamComponentVector dummy_components;
+    stream_descriptor_ = MakeGarbageCollected<MediaStreamDescriptor>(
+        "new stream", dummy_components, dummy_components);
+
+    TestWebRtcAudioRenderer::destructor_thread_id_ = base::kInvalidThreadId;
+
+    renderer_ = base::MakeRefCounted<TestWebRtcAudioRenderer>(
+        scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_,
+        *web_local_frame_, base::UnguessableToken::Create(), "",
+        base::RepeatingCallback<void()>(), run_loop_.QuitClosure());
+
+    test_audio_device_ = new webrtc::RefCountedObject<WebRtcAudioDeviceImpl>();
+    EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get()));
+
+    worker_thread_ = std::make_unique<base::Thread>("WebRTC_Worker");
+    worker_thread_->Start();
+
+    base::RunLoop init_loop;
+    worker_thread_->task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(
+                       [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
+                         static_cast<webrtc::AudioDeviceModule*>(ADM)->Init();
+                         loop->Quit();
+                       },
+                       base::Unretained(test_audio_device_.get()),
+                       base::Unretained(&init_loop)));
+    init_loop.Run();
+  }
+
+  ~WebRtcAudioDeviceImplReleaseTest() override {
+    test_audio_device_ = nullptr;
+    web_local_frame_ = nullptr;
+    if (web_view_) {
+      web_view_.ExtractAsDangling()->Close();
+    }
+    blink::WebHeap::CollectAllGarbageForTesting();
+    worker_thread_->Stop();
+  }
+
+  void TerminateADMOnWorkerThread() {
+    base::RunLoop terminate_loop;
+    worker_thread_->task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
+              static_cast<webrtc::AudioDeviceModule*>(ADM)->Terminate();
+              loop->Quit();
+            },
+            base::Unretained(test_audio_device_.get()),
+            base::Unretained(&terminate_loop)));
+    terminate_loop.Run();
+  }
+
+  void StartStopUnderlyingRenderer() {
+    MediaStreamAudioRenderer* underlying_renderer =
+        static_cast<MediaStreamAudioRenderer*>(renderer_.get());
+    underlying_renderer->Start();
+    underlying_renderer->Stop();
+  }
+
+  void DetachFrame() { web_local_frame_->Detach(); }
+  void ReleaseAudioDevice() { test_audio_device_ = nullptr; }
+  void ReleaseRenderer() { renderer_ = nullptr; }
+  void RunLoop() { run_loop_.Run(); }
+
+ private:
   std::unique_ptr<blink::scheduler::WebAgentGroupScheduler>
-      agent_group_scheduler =
-          std::make_unique<blink::scheduler::WebAgentGroupScheduler>(
-              ThreadScheduler::Current()
-                  ->ToMainThreadScheduler()
-                  ->CreateAgentGroupScheduler());
-
-  WebView* web_view = blink::WebView::Create(
-      /*client=*/nullptr,
-      /*is_hidden=*/false,
-      /*prerender_param=*/nullptr,
-      /*fenced_frame_mode=*/std::nullopt,
-      /*compositing_enabled=*/false,
-      /*widgets_never_composited=*/false,
-      /*opener=*/nullptr, mojo::NullAssociatedReceiver(),
-      *agent_group_scheduler,
-      /*session_storage_namespace_id=*/std::string(),
-      /*page_base_background_color=*/std::nullopt,
-      /*browsing_context_group_token=*/base::UnguessableToken::Create(),
-      /*color_provider_colors=*/nullptr,
-      /*history_index=*/-1,
-      /*history_length=*/0);
-
-  WebLocalFrameClient web_local_frame_client;
-  WebLocalFrame* web_local_frame = blink::WebLocalFrame::CreateMainFrame(
-      web_view, &web_local_frame_client, nullptr, mojo::NullRemote(),
-      LocalFrameToken(), DocumentToken(),
-      /*policy_container=*/nullptr);
-
-  // 2. Setup Mock Platform. WebRtcAudioRenderer::Initialize calls
-  // Platform::Current()->NewAudioRendererSink, so we must mock it to return
-  // a valid mock sink.
+      agent_group_scheduler_;
+  WebLocalFrameClient web_local_frame_client_;
+  raw_ptr<WebView> web_view_;
+  raw_ptr<WebLocalFrame> web_local_frame_;
   ScopedTestingPlatformSupport<AudioDeviceFactoryTestingPlatformSupport>
-      platform_support;
+      platform_support_;
+  Persistent<MediaStreamDescriptor> stream_descriptor_;
+  base::RunLoop run_loop_;
+  scoped_refptr<TestWebRtcAudioRenderer> renderer_;
+  scoped_refptr<WebRtcAudioDeviceImpl> test_audio_device_;
+  std::unique_ptr<base::Thread> worker_thread_;
+};
 
-  MediaStreamComponentVector dummy_components;
-  Persistent<MediaStreamDescriptor> stream_descriptor =
-      MakeGarbageCollected<MediaStreamDescriptor>(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
index b56bfda..3d43a5f3 100644
--- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
+++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
@@ -7,6 +7,7 @@
 #include <memory>
 
 #include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
 #include "base/threading/thread.h"
 #include "base/time/time.h"
@@ -196,138 +197,160 @@
 base::PlatformThreadId TestWebRtcAudioRenderer::destructor_thread_id_ =
     base::kInvalidThreadId;
 
-TEST_F(WebRtcAudioDeviceImplTest, ReleaseRendererSoonWithShutDownQueue) {
-  // 1. Setup Blink environment. We need a real WebView and WebLocalFrame
-  // to get a frame-associated task runner for the renderer.
+class WebRtcAudioDeviceImplReleaseTest : public WebRtcAudioDeviceImplTest {
+ public:
+  WebRtcAudioDeviceImplReleaseTest() {
+    agent_group_scheduler_ =
+        std::make_unique<blink::scheduler::WebAgentGroupScheduler>(
+            ThreadScheduler::Current()
+                ->ToMainThreadScheduler()
+                ->CreateAgentGroupScheduler());
+
+    web_view_ = blink::WebView::Create(
+        /*client=*/nullptr,
+        /*is_hidden=*/false,
+        /*prerender_param=*/nullptr,
+        /*fenced_frame_mode=*/std::nullopt,
+        /*compositing_enabled=*/false,
+        /*widgets_never_composited=*/false,
+        /*opener=*/nullptr, mojo::NullAssociatedReceiver(),
+        *agent_group_scheduler_,
+        /*session_storage_namespace_id=*/std::string(),
+        /*page_base_background_color=*/std::nullopt,
+        /*browsing_context_group_token=*/base::UnguessableToken::Create(),
+        /*color_provider_colors=*/nullptr,
+        /*history_index=*/-1,
+        /*history_length=*/0);
+
+    web_local_frame_ = blink::WebLocalFrame::CreateMainFrame(
+        web_view_, &web_local_frame_client_, nullptr, mojo::NullRemote(),
+        LocalFrameToken(), DocumentToken(),
+        /*policy_container=*/nullptr);
+
+    MediaStreamComponentVector dummy_components;
+    stream_descriptor_ = MakeGarbageCollected<MediaStreamDescriptor>(
+        "new stream", dummy_components, dummy_components);
+
+    TestWebRtcAudioRenderer::destructor_thread_id_ = base::kInvalidThreadId;
+
+    renderer_ = base::MakeRefCounted<TestWebRtcAudioRenderer>(
+        scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_,
+        *web_local_frame_, base::UnguessableToken::Create(), "",
+        base::RepeatingCallback<void()>(), run_loop_.QuitClosure());
+
+    test_audio_device_ = new webrtc::RefCountedObject<WebRtcAudioDeviceImpl>();
+    EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get()));
+
+    worker_thread_ = std::make_unique<base::Thread>("WebRTC_Worker");
+    worker_thread_->Start();
+
+    base::RunLoop init_loop;
+    worker_thread_->task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(
+                       [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
+                         static_cast<webrtc::AudioDeviceModule*>(ADM)->Init();
+                         loop->Quit();
+                       },
+                       base::Unretained(test_audio_device_.get()),
+                       base::Unretained(&init_loop)));
+    init_loop.Run();
+  }
+
+  ~WebRtcAudioDeviceImplReleaseTest() override {
+    test_audio_device_ = nullptr;
+    web_local_frame_ = nullptr;
+    if (web_view_) {
+      web_view_.ExtractAsDangling()->Close();
+    }
+    blink::WebHeap::CollectAllGarbageForTesting();
+    worker_thread_->Stop();
+  }
+
+  void TerminateADMOnWorkerThread() {
+    base::RunLoop terminate_loop;
+    worker_thread_->task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
+              static_cast<webrtc::AudioDeviceModule*>(ADM)->Terminate();
+              loop->Quit();
+            },
+            base::Unretained(test_audio_device_.get()),
+            base::Unretained(&terminate_loop)));
+    terminate_loop.Run();
+  }
+
+  void StartStopUnderlyingRenderer() {
+    MediaStreamAudioRenderer* underlying_renderer =
+        static_cast<MediaStreamAudioRenderer*>(renderer_.get());
+    underlying_renderer->Start();
+    underlying_renderer->Stop();
+  }
+
+  void DetachFrame() { web_local_frame_->Detach(); }
+  void ReleaseAudioDevice() { test_audio_device_ = nullptr; }
+  void ReleaseRenderer() { renderer_ = nullptr; }
+  void RunLoop() { run_loop_.Run(); }
+
+ private:
   std::unique_ptr<blink::scheduler::WebAgentGroupScheduler>
-      agent_group_scheduler =
-          std::make_unique<blink::scheduler::WebAgentGroupScheduler>(
-              ThreadScheduler::Current()
-                  ->ToMainThreadScheduler()
-                  ->CreateAgentGroupScheduler());
-
-  WebView* web_view = blink::WebView::Create(
-      /*client=*/nullptr,
-      /*is_hidden=*/false,
-      /*prerender_param=*/nullptr,
-      /*fenced_frame_mode=*/std::nullopt,
-      /*compositing_enabled=*/false,
-      /*widgets_never_composited=*/false,
-      /*opener=*/nullptr, mojo::NullAssociatedReceiver(),
-      *agent_group_scheduler,
-      /*session_storage_namespace_id=*/std::string(),
-      /*page_base_background_color=*/std::nullopt,
-      /*browsing_context_group_token=*/base::UnguessableToken::Create(),
-      /*color_provider_colors=*/nullptr,
-      /*history_index=*/-1,
-      /*history_length=*/0);
-
-  WebLocalFrameClient web_local_frame_client;
-  WebLocalFrame* web_local_frame = blink::WebLocalFrame::CreateMainFrame(
-      web_view, &web_local_frame_client, nullptr, mojo::NullRemote(),
-      LocalFrameToken(), DocumentToken(),
-      /*policy_container=*/nullptr);
-
-  // 2. Setup Mock Platform. WebRtcAudioRenderer::Initialize calls
-  // Platform::Current()->NewAudioRendererSink, so we must mock it to return
-  // a valid mock sink.
+      agent_group_scheduler_;
+  WebLocalFrameClient web_local_frame_client_;
+  raw_ptr<WebView> web_view_;
+  raw_ptr<WebLocalFrame> web_local_frame_;
   ScopedTestingPlatformSupport<AudioDeviceFactoryTestingPlatformSupport>
-      platform_support;
+      platform_support_;
+  Persistent<MediaStreamDescriptor> stream_descriptor_;
+  base::RunLoop run_loop_;
+  scoped_refptr<TestWebRtcAudioRenderer> renderer_;
+  scoped_refptr<WebRtcAudioDeviceImpl> test_audio_device_;
+  std::unique_ptr<base::Thread> worker_thread_;
+};
 
-  MediaStreamComponentVector dummy_components;
-  Persistent<MediaStreamDescriptor> stream_descriptor =
-      MakeGarbageCollected<MediaStreamDescriptor>(
-          "new stream", dummy_components, dummy_components);
-
-  base::RunLoop run_loop;
+TEST_F(WebRtcAudioDeviceImplReleaseTest, ReleaseRendererSoonWithShutDownQueue) {
   base::PlatformThreadId main_thread_id = base::PlatformThread::CurrentId();
-  TestWebRtcAudioRenderer::destructor_thread_id_ = base::kInvalidThreadId;
 
-  // 3. Create our test renderer. It will use the frame's task runner (which
-  // is a BlinkSchedulerSingleThreadTaskRunner).
-  scoped_refptr<TestWebRtcAudioRenderer> renderer =
-      base::MakeRefCounted<TestWebRtcAudioRenderer>(
-          scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor,
-          *web_local_frame, base::UnguessableToken::Create(), "",
-          base::RepeatingCallback<void()>(), run_loop.QuitClosure());
+  // Detach the frame to shut down its task queues.
+  DetachFrame();
 
-  // 4. Create the ADM and associate the renderer with it.
-  scoped_refptr<WebRtcAudioDeviceImpl> test_audio_device =
-      new webrtc::RefCountedObject<WebRtcAudioDeviceImpl>();
+  // Terminate() bounces the renderer reference to the main thread. With the
+  // frame queue shut down, ReleaseSoon must successfully fall back to the
+  // default main thread task runner to ensure safe destruction.
+  TerminateADMOnWorkerThread();
 
-  EXPECT_TRUE(test_audio_device->SetAudioRenderer(renderer.get()));
+  // Destroy the ADM to simulate full teardown.
+  ReleaseAudioDevice();
 
-  // 5. Start a worker thread. In production, Terminate() is called on the
-  // WebRTC worker thread. To avoid thread checker failures in Terminate()
-  // (signaling_thread_checker_), we must also initialize (Init()) the ADM on
-  // this worker thread so the checker binds to it.
-  base::Thread worker_thread("WebRTC_Worker");
-  worker_thread.Start();
+  // Transition renderer state to kUninitialized to satisfy destructor DCHECKs.
+  StartStopUnderlyingRenderer();
 
-  base::RunLoop init_loop;
-  worker_thread.task_runner()->PostTask(
-      FROM_HERE, base::BindOnce(
-                     [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
-                       static_cast<webrtc::AudioDeviceModule*>(ADM)->Init();
-                       loop->Quit();
-                     },
-                     base::Unretained(test_audio_device.get()),
-                     base::Unretained(&init_loop)));
-  init_loop.Run();
+  // Release the local reference so ReleaseSoon holds the sole remaining ref.
+  ReleaseRenderer();
 
-  // 6. Detach the frame. This shuts down the frame's task queues (including
-  // the one used by the renderer). Subsequent posts to this queue will fail.
-  web_local_frame->Detach();
+  // Execute the posted destruction task.
+  RunLoop();
 
-  // 7. Call Terminate() on the worker thread. This will attempt to disconnect
-  // the renderer and bounce the final reference back to the main thread.
-  // Because the frame queue is shut down, PostCrossThreadTask would fail and
-  // destroy the renderer inline on the worker thread. ReleaseSoon should
-  // fallback to the thread-level task runner and post it successfully.
-  base::RunLoop terminate_loop;
-  worker_thread.task_runner()->PostTask(
-      FROM_HERE,
-      base::BindOnce(
-          [](WebRtcAudioDeviceImpl* ADM, base::RunLoop* loop) {
-            static_cast<webrtc::AudioDeviceModule*>(ADM)->Terminate();
-            loop->Quit();
-          },
-          base::Unretained(test_audio_device.get()),
-          base::Unretained(&terminate_loop)));
-  terminate_loop.Run();
-
-  // 8. Release the ADM reference on the main thread. This drops the ADM's
-  // reference to the renderer.
-  test_audio_device = nullptr;
-
-  // 9. Start and Stop the renderer to transition its state to kUninitialized.
-  // This is necessary because Initialize() set it to kPaused, and the
-  // destructor DCHECKs that it is kUninitialized. We must do this after
-  // Terminate() has disconnected the source (setting it to null), so that
-  // Stop() doesn't try to call RemoveAudioRenderer on the ADM which is already
-  // being destroyed. We still hold a local reference 'renderer' so we can do
-  // this.
-  MediaStreamAudioRenderer* underlying_renderer =
-      static_cast<MediaStreamAudioRenderer*>(renderer.get());
-  underlying_renderer->Start();
-  underlying_renderer->Stop();
-
-  // Now release our local reference. The only remaining reference to the
-  // renderer should now be held by the task posted to the main thread via
-  // ReleaseSoon's fallback mechanism.
-  renderer = nullptr;
-
-  // 10. Run the main thread message loop. This will execute the posted delete
-  // task.
-  run_loop.Run();
-
-  // 11. Verify that the renderer was actually destructed on the main thread.
   EXPECT_EQ(TestWebRtcAudioRenderer::destructor_thread_id_, main_thread_id);
+}
 
-  // Clean up.
-  web_view->Close();
-  blink::WebHeap::CollectAllGarbageForTesting();
-  worker_thread.Stop();
+TEST_F(WebRtcAudioDeviceImplReleaseTest, TerminateReleasesRenderer) {
+  base::PlatformThreadId main_thread_id = base::PlatformThread::CurrentId();
+
+  // Terminate() must move renderer_ out of the ADM and post it via ReleaseSoon.
+  TerminateADMOnWorkerThread();
+
+  // Transition renderer state to kUninitialized to satisfy destructor DCHECKs.
+  StartStopUnderlyingRenderer();
+
+  // Release the local reference. The ADM is intentionally kept alive.
+  ReleaseRenderer();
+
+  // Run the loop. If Terminate() correctly cleared renderer_ via std::move,
+  // ReleaseSoon will drop the final reference and invoke the destructor.
+  // Otherwise, the ADM would retain a reference and this loop would hang.
+  RunLoop();
+
+  EXPECT_EQ(TestWebRtcAudioRenderer::destructor_thread_id_, main_thread_id);
 }
 
 }  // namespace blink
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF/Heap Corruption via wrong-thread destruction of WebRtcAudioRenderer

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in WebRtcAudioDeviceImpl::Terminate() fails to clear its strong reference to WebRtcAudioRenderer. This bypasses intended cleanup paths, causing the renderer to be destroyed on the WebRTC signaling thread, which corrupts Oilpan heap structures and potentially leads to RCE.

Affected files:

  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.h

Estimated timestamp from git blame: Unknown (Google3 checkout)

Root Cause

In third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc, the Terminate() method executes on the WebRTC signaling thread to safely disconnect the audio renderer. It copies the renderer_ member variable to a local renderer_to_disconnect variable for a safe main-thread bounce using ReleaseSoon().

However, the implementation fails to clear the renderer_ member variable itself (missing renderer_ = nullptr;).

int32_t WebRtcAudioDeviceImpl::Terminate() {
  // ...
  scoped_refptr<blink::WebRtcAudioRenderer> renderer_to_disconnect;
  {
    base::AutoLock auto_lock(lock_);
    renderer_to_disconnect = renderer_;
    // MISSING: renderer_ = nullptr;
    capturers_.clear();
  }
  if (renderer_to_disconnect) {
    renderer_to_disconnect->DisconnectSource();
    // ... ReleaseSoon ...
  }
}

The Flawed State Machine

Terminate() calls renderer_to_disconnect->DisconnectSource(), which sets the renderer’s internal source_ pointer to nullptr.

Later, when the audio player is stopped on the main thread (e.g., due to iframe detachment), WebRtcAudioRenderer::Stop() is invoked. Stop() is designed to deregister the renderer from the device by calling source_->RemoveAudioRenderer(this). However, because source_ was prematurely set to nullptr during Terminate(), this check fails, and RemoveAudioRenderer() is skipped.

Consequently, the intended mechanism to clear WebRtcAudioDeviceImpl::renderer_ is bypassed, leaving the Audio Device Module (ADM) holding a dangling, strong scoped_refptr to the renderer.

Wrong-Thread Destruction

During garbage collection, PeerConnectionDependencyFactory posts a task to destroy the native webrtc::PeerConnectionFactory on the WebRTC signaling thread. The destruction of this native factory drops the final reference to WebRtcAudioDeviceImpl.

As WebRtcAudioDeviceImpl is destroyed on the signaling thread, its member variables are destroyed, dropping the final, uncleared reference to the WebRtcAudioRenderer. This forces ~WebRtcAudioRenderer() to execute synchronously on the signaling thread.

Impact

WebRtcAudioRenderer contains Blink Oilpan garbage collection handles, specifically WeakPersistent<LocalFrame> source_frame_; and Persistent<MediaStreamDescriptor> media_stream_descriptor_;.

Oilpan Persistent handles have strict thread affinity. When destroyed, they call PersistentRegion::FreeNode(). Executing this on the signaling thread instead of the creation thread bypasses debug checks and modifies the free_list_head_ of the main thread’s Oilpan heap without locking. This concurrent modification creates a data race that reliably corrupts the heap free list, yielding a primitive that can likely be leveraged for Remote Code Execution (RCE) in the sandboxed renderer process.

Potential Trigger Steps

Note: These are suggested steps based on code analysis; we do not currently have a working proof of concept.

An attacker could potentially trigger this issue via the following sequence:

  1. Establish a WebRTC connection and receive an audio track.
  2. Assign the MediaStream to an <audio> tag inside an iframe.
  3. Close the WebRTC connection (pc.close()). This invokes Terminate() on the signaling thread, nulling the renderer’s source_ but leaving the ADM’s renderer_ populated.
  4. Remove the iframe from the DOM. This invokes Stop() on the main thread, which drops legitimate references but skips RemoveAudioRenderer.
  5. Wait for garbage collection. The ADM is eventually destroyed on the signaling thread, triggering the wrong-thread destruction of the renderer and corrupting the Oilpan heap.

Suggested Fix

Modify WebRtcAudioDeviceImpl::Terminate() to ensure the renderer_ member is cleared:

  {
    base::AutoLock auto_lock(lock_);
    renderer_to_disconnect = std::move(renderer_); // Or explicitly: renderer_ = nullptr;
    capturers_.clear();
  }

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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