CVE-2026-11053
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/peerconnection/rtc_stats.cc |
modified |
Files Changed
third_party/blink/renderer/platform/peerconnection/rtc_stats.ccthird_party/blink/renderer/platform/peerconnection/rtc_stats.h
Patch
From 360165e410fa95e28c1a9a612acf2b38d794d8e3 Mon Sep 17 00:00:00 2001 From: Philip Eliasson <[email protected]> Date: Wed, 08 Apr 2026 06:21:35 -0700 Subject: [PATCH] Fire the getStats callback with an empty report if the actual stats report has not been delivered before shutdown. Bug: chromium:498841456 Change-Id: I17d93f335fb7c01f1ded21f00c33afdc6ab5c3f8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7734937 Commit-Queue: Philip Eliasson <[email protected]> Reviewed-by: Henrik Boström <[email protected]> Cr-Commit-Position: refs/heads/main@{#1611433} --- diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc index 5cb09d6..11103f8 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc +++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc @@ -67,7 +67,9 @@ : main_thread_(std::move(main_thread)), callback_(std::move(callback)) {} RTCStatsCollectorCallbackImpl::~RTCStatsCollectorCallbackImpl() { - DCHECK(!callback_); + if (callback_) { + OnStatsDelivered(webrtc::RTCStatsReport::Create(webrtc::Timestamp::Zero())); + } } void RTCStatsCollectorCallbackImpl::OnStatsDelivered( @@ -75,18 +77,12 @@ PostCrossThreadTask( *main_thread_.get(), FROM_HERE, CrossThreadBindOnce( - &RTCStatsCollectorCallbackImpl::OnStatsDeliveredOnMainThread, - webrtc::scoped_refptr<RTCStatsCollectorCallbackImpl>(this), report)); -} - -void RTCStatsCollectorCallbackImpl::OnStatsDeliveredOnMainThread( - webrtc::scoped_refptr<const webrtc::RTCStatsReport> report) { - DCHECK(main_thread_->BelongsToCurrentThread()); - DCHECK(report); - DCHECK(callback_); - // Make sure the callback is destroyed in the main thread as well. - std::move(callback_).Run(std::make_unique<RTCStatsReportPlatform>( - base::WrapRefCounted(report.get()))); + [](RTCStatsReportCallback callback, + webrtc::scoped_refptr<const webrtc::RTCStatsReport> report) { + std::move(callback).Run(std::make_unique<RTCStatsReportPlatform>( + base::WrapRefCounted(report.get()))); + }, + std::move(callback_), report)); } } // namespace blink diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h index 44fdc58..7e86745 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h +++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h @@ -81,9 +81,6 @@ RTCStatsReportCallback callback); ~RTCStatsCollectorCallbackImpl() override; - void OnStatsDeliveredOnMainThread( - webrtc::scoped_refptr<const webrtc::RTCStatsReport> report); - const scoped_refptr<base::SingleThreadTaskRunner> main_thread_; RTCStatsReportCallback callback_; };
Original Bug Report
Cross-thread destruction of cppgc::Persistent in WebRTC getStats
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 without the security team.
Overview: A race condition in WebRTC stats collection allows a cppgc::Persistent handle to be improperly destroyed on the signaling thread if the main thread’s task runner shuts down. This non-atomic destruction corrupts the cppgc freelist on weakly ordered architectures like ARM. An attacker could potentially exploit this to achieve arbitrary memory writes and Remote Code Execution.
Affected files:
third_party/blink/renderer/platform/peerconnection/rtc_stats.ccthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.ccthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
Estimated timestamp from git blame: 2025-08-26
Summary
A potential cross-thread destruction vulnerability exists in Blink’s WebRTC implementation. When RTCPeerConnection.getStats() is called, a cppgc::Persistent handle is created and passed to the WebRTC signaling thread via a callback. If the main thread’s task runner is shut down (e.g., by iframe detachment) before the operation completes, the callback fails to post back to the main thread and is destroyed synchronously on the signaling thread. This violates cppgc thread-safety guarantees and can corrupt the PersistentRegion freelist, potentially leading to Remote Code Execution (RCE).
Technical Details
- Handle Creation:
RTCPeerConnection::getStatscreates aScriptPromiseResolverand binds it into a callback usingWrapPersistent(resolver). This allocates aPersistentNodeon the main thread’sPersistentRegion. - Task Posting: This callback is sent to the WebRTC signaling thread inside an
RTCStatsCollectorCallbackImplwrapper. - Task Runner Shutdown: If the owning iframe is detached, its
ExecutionContextis destroyed, shutting down the frame’s task queues. - Cross-Thread Destruction: When the signaling thread finishes gathering stats, it calls
PostCrossThreadTaskto return the result. Because the task runner is shut down,PostTaskreturnsfalse. The task payload, including thePersistenthandle, is synchronously destroyed on the signaling thread. - Freelist Corruption:
PersistentNode::FreeNodeexecutes on the signaling thread. In release builds, API checks are disabled, bypassing the thread-creationDCHECK.FreeNodeupdatesfree_list_head_and overwrites the node’s internalunion { void* owner_; PersistentNode* next_; }.
On weakly-ordered CPU architectures (like ARM), concurrent Persistent allocations on the main thread can observe this update out-of-order. The main thread may read the new free_list_head_ but read the stale owner_ pointer (which points into the PartitionAlloc heap) instead of the new next_ pointer. This memory race results in the main thread’s cppgc freelist pointing into arbitrary PartitionAlloc memory.
Potential Exploitation Steps
Note: These are suggested steps based on static analysis; our tooling does not currently have the capability to execute code to provide a working proof-of-concept.
- Setup: An attacker creates an
iframeand initializes anRTCPeerConnection. - Trigger Stats: The attacker calls
getStats()to begin asynchronous processing on the signaling thread. - Detach Frame: The attacker immediately removes the
iframefrom the DOM, invalidating its task runner. - Heap Spray: The attacker sprays the PartitionAlloc heap to control the memory surrounding the callback’s bind state.
- Trigger Race: The attacker initiates concurrent
cppgcallocations from the main thread (e.g., via a surviving frame). If the main thread reads the torn freelist state, it will route the freelist into attacker-controlled PartitionAlloc memory. - Achieve RCE: Subsequent allocations from the corrupted freelist allow the attacker to control the
next_pointer (an arbitrary write primitive) or trigger a type confusion during the next Garbage Collection cycle whenTraceAsRootis called on the attacker-controlled payload.
Suggested Fix
Ensure that cross-thread tasks properly handle destruction when PostTask fails. The promise resolver should be bound using WrapCrossThreadPersistent instead of WrapPersistent when passed to WebRTCStatsReportCallbackResolver, as it is explicitly intended to travel across threads. Alternatively, RTCStatsCollectorCallbackImpl must be redesigned so that if posting back to the main thread fails, the callback payload is safely discarded on the main thread (or explicitly leaked) rather than destroyed on the signaling thread.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.