CVE-2026-17884
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc |
modified | |
WebRtcAudioDeviceImplReleaseTestthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc |
modified | |
WebRtcAudioDeviceImplReleaseTestthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc |
modified | |
ifthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
Patch
From ff81618204d47311fe2fe21f5069317793b0f140 Mon Sep 17 00:00:00 2001 From: Johannes Kron <[email protected]> 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 <[email protected]> Commit-Queue: Johannes Kron <[email protected]> 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>(
Regression Test / PoC
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
Original Bug Report
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.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.hthird_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.ccthird_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:
- Establish a WebRTC connection and receive an audio track.
- Assign the
MediaStreamto an<audio>tag inside aniframe. - Close the WebRTC connection (
pc.close()). This invokesTerminate()on the signaling thread, nulling the renderer’ssource_but leaving the ADM’srenderer_populated. - Remove the
iframefrom the DOM. This invokesStop()on the main thread, which drops legitimate references but skipsRemoveAudioRenderer. - 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.