Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactVULNERABILITY in WebRTC
DescriptionVULNERABILITY in WebRTC
ComponentWebRTC
Bug ClassLogic Error
Tracker498841456
Fix commit360165e410fa (chromium/src) +9/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/peerconnection/rtc_stats.cc
modified

Files Changed

  • third_party/blink/renderer/platform/peerconnection/rtc_stats.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_stats.h
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_;
 };
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
  • third_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

  1. Handle Creation: RTCPeerConnection::getStats creates a ScriptPromiseResolver and binds it into a callback using WrapPersistent(resolver). This allocates a PersistentNode on the main thread’s PersistentRegion.
  2. Task Posting: This callback is sent to the WebRTC signaling thread inside an RTCStatsCollectorCallbackImpl wrapper.
  3. Task Runner Shutdown: If the owning iframe is detached, its ExecutionContext is destroyed, shutting down the frame’s task queues.
  4. Cross-Thread Destruction: When the signaling thread finishes gathering stats, it calls PostCrossThreadTask to return the result. Because the task runner is shut down, PostTask returns false. The task payload, including the Persistent handle, is synchronously destroyed on the signaling thread.
  5. Freelist Corruption: PersistentNode::FreeNode executes on the signaling thread. In release builds, API checks are disabled, bypassing the thread-creation DCHECK. FreeNode updates free_list_head_ and overwrites the node’s internal union { 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.

  1. Setup: An attacker creates an iframe and initializes an RTCPeerConnection.
  2. Trigger Stats: The attacker calls getStats() to begin asynchronous processing on the signaling thread.
  3. Detach Frame: The attacker immediately removes the iframe from the DOM, invalidating its task runner.
  4. Heap Spray: The attacker sprays the PartitionAlloc heap to control the memory surrounding the callback’s bind state.
  5. Trigger Race: The attacker initiates concurrent cppgc allocations 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.
  6. 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 when TraceAsRoot is 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.

View on issue tracker