Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker501424047
Fix commit8fc4e8102781 (chromium/src) +24/-25
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
ExecutionContext
third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h
modified
GetStatsCallbackWrapper
third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc
modified

Files Changed

  • third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h
  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
  • third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_stats.h
From 8fc4e8102781e4fdf8bbc4a370474f38d06b1b94 Mon Sep 17 00:00:00 2001
From: Tony Herre <[email protected]>
Date: Mon, 13 Apr 2026 03:49:46 -0700
Subject: [PATCH] Use WrapCrossThreadPersistent for callbacks in RTCPeerConnection

Switch to WrapCrossThreadPersistent for generateCertificate and getStats
callbacks to ensure thread safety.

Bug: 501424047
Change-Id: I2379ed85cc1dba90810e891b16387e4667b766ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747791
Reviewed-by: Palak Agarwal <[email protected]>
Commit-Queue: Palak Agarwal <[email protected]>
Auto-Submit: Tony Herre <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1613612}
---

diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h b/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h
index d486bb4..833bd25 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h
@@ -18,8 +18,8 @@
 
 class ExecutionContext;
 
-using RTCCertificateCallback =
-    base::OnceCallback<void(webrtc::scoped_refptr<webrtc::RTCCertificate>)>;
+using RTCCertificateCallback = CrossThreadOnceFunction<void(
+    webrtc::scoped_refptr<webrtc::RTCCertificate>)>;
 
 // Chromium's WebRTCCertificateGenerator implementation; uses the
 // PeerConnectionIdentityStore/SSLIdentity::Generate to generate the identity,
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
index c5ba331b..2ef7005 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
@@ -123,6 +123,7 @@
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
 #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/instrumentation/instance_counters.h"
@@ -1472,8 +1473,8 @@
 
   // Helper closure callback for RTCPeerConnection::generateCertificate.
   auto completion_callback =
-      BindOnce(RTCPeerConnection::GenerateCertificateCompleted,
-               WrapPersistent(resolver));
+      CrossThreadBindOnce(RTCPeerConnection::GenerateCertificateCompleted,
+                          WrapCrossThreadPersistent(resolver));
 
   // Generate certificate. The |certificateObserver| will resolve the promise
   // asynchronously upon completion. The observer will manage its own
@@ -1766,8 +1767,9 @@
       // while leaving the associated promise pending as specified.
       resolver->Detach();
     } else {
-      peer_handler_->GetStats(BindOnce(WebRTCStatsReportCallbackResolver,
-                                       WrapPersistent(resolver)));
+      peer_handler_->GetStats(
+          CrossThreadBindOnce(WebRTCStatsReportCallbackResolver,
+                              WrapCrossThreadPersistent(resolver)));
     }
     return promise;
   }
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
index 76a04bcf..188095b 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
@@ -233,19 +233,14 @@
   PeerConnectionTracker::Action action_;
 };
 
-using RTCStatsReportCallbackInternal =
-    CrossThreadOnceFunction<void(std::unique_ptr<RTCStatsReportPlatform>)>;
-
 void GetRTCStatsOnSignalingThread(
     const scoped_refptr<base::SingleThreadTaskRunner>& main_thread,
     webrtc::scoped_refptr<webrtc::PeerConnectionInterface>
         native_peer_connection,
-    RTCStatsReportCallbackInternal callback) {
+    RTCStatsReportCallback callback) {
   TRACE_EVENT0("webrtc", "GetRTCStatsOnSignalingThread");
   native_peer_connection->GetStats(
-      CreateRTCStatsCollectorCallback(
-          main_thread, ConvertToBaseOnceCallback(std::move(callback)))
-          .get());
+      CreateRTCStatsCollectorCallback(main_thread, std::move(callback)).get());
 }
 
 std::set<RTCPeerConnectionHandler*>* GetPeerConnectionHandlers() {
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc
index 567d85dd..98f8d99 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc
@@ -38,6 +38,7 @@
 #include "third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_stats_report.h"
 #include "third_party/blink/renderer/modules/peerconnection/web_rtc_stats_report_callback_resolver.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h"
 #include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h"
@@ -161,8 +162,8 @@
   auto* resolver =
       MakeGarbageCollected<ScriptPromiseResolver<RTCStatsReport>>(script_state);
   auto promise = resolver->Promise();
-  receiver_->GetStats(
-      BindOnce(WebRTCStatsReportCallbackResolver, WrapPersistent(resolver)));
+  receiver_->GetStats(CrossThreadBindOnce(WebRTCStatsReportCallbackResolver,
+                                          WrapCrossThreadPersistent(resolver)));
   return promise;
 }
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
index 6d7519e..916880179 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
@@ -47,6 +47,7 @@
 #include "third_party/blink/renderer/modules/peerconnection/rtc_stats_report.h"
 #include "third_party/blink/renderer/modules/peerconnection/web_rtc_stats_report_callback_resolver.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/heap/persistent.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_dtmf_sender_handler.h"
@@ -882,8 +883,8 @@
   auto* resolver =
       MakeGarbageCollected<ScriptPromiseResolver<RTCStatsReport>>(script_state);
   auto promise = resolver->Promise();
-  sender_->GetStats(
-      BindOnce(WebRTCStatsReportCallbackResolver, WrapPersistent(resolver)));
+  sender_->GetStats(CrossThreadBindOnce(WebRTCStatsReportCallbackResolver,
+                                        WrapCrossThreadPersistent(resolver)));
   return promise;
 }
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
index 3ed0885..7d10622 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
@@ -362,14 +362,11 @@
     std::move(callback).Run(result);
   }
 
-  using RTCStatsReportCallbackInternal =
-      CrossThreadOnceFunction<void(std::unique_ptr<RTCStatsReportPlatform>)>;
-
-  void GetStatsOnSignalingThread(RTCStatsReportCallbackInternal callback) {
+  void GetStatsOnSignalingThread(RTCStatsReportCallback callback) {
     native_peer_connection_->GetStats(
         webrtc::scoped_refptr<webrtc::RtpSenderInterface>(webrtc_sender_.get()),
-        CreateRTCStatsCollectorCallback(
-            main_task_runner_, ConvertToBaseOnceCallback(std::move(callback))));
+        CreateRTCStatsCollectorCallback(main_task_runner_,
+                                        std::move(callback)));
   }
 
   void SetParametersOnSignalingThread(
diff --git a/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc b/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc
index a90e06e..6d32a06 100644
--- a/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc
+++ b/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc
@@ -7,6 +7,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 
 namespace blink {
 
@@ -16,7 +17,8 @@
 
 RTCStatsReportCallback
 TestWebRTCStatsReportObtainer::GetStatsCallbackWrapper() {
-  return base::BindOnce(&TestWebRTCStatsReportObtainer::OnStatsDelivered, this);
+  return CrossThreadBindOnce(&TestWebRTCStatsReportObtainer::OnStatsDelivered,
+                             base::RetainedRef(this));
 }
 
 RTCStatsReportPlatform* TestWebRTCStatsReportObtainer::report() const {
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h
index 7e86745..1438da20 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h
@@ -11,6 +11,7 @@
 #include "third_party/blink/public/platform/web_string.h"
 #include "third_party/blink/renderer/platform/allow_discouraged_type.h"
 #include "third_party/blink/renderer/platform/platform_export.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 #include "third_party/blink/renderer/platform/wtf/hash_map.h"
 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
 #include "third_party/webrtc/api/scoped_refptr.h"
@@ -56,7 +57,7 @@
 };
 
 using RTCStatsReportCallback =
-    base::OnceCallback<void(std::unique_ptr<RTCStatsReportPlatform>)>;
Loading diff…

Original Bug Report

reported by [email protected]

Off-thread cppgc::Persistent destruction in RTCPeerConnection leads to memory corruption

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 Chrome Security team.

Overview: A race condition during asynchronous WebRTC certificate generation allows a thread-unsafe cppgc::Persistent handle to be destroyed on a worker thread if the originating iframe is removed. This off-thread destruction modifies the Oilpan PersistentRegion free-list without locking, causing memory corruption and potential Use-After-Free (UAF).

Affected files:

  • third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
  • v8/include/cppgc/internal/persistent-node.h
  • v8/include/cppgc/persistent.h

Estimated timestamp from git blame: 2025-11-26

Summary

There is a potential race condition and thread-safety violation in RTCCertificateGenerator that allows a main-thread cppgc::Persistent handle to be destroyed on a WebRTC worker thread. Because cppgc::PersistentNode freeing is lockless (assuming execution on the creation thread), this off-thread destruction corrupts the PersistentRegion free-list, leading to memory corruption, aliased handles, and potential Remote Code Execution (RCE) in the renderer process.

Vulnerability Details

The potential vulnerability occurs in the following sequence:

  1. Trigger: An attacker calls RTCPeerConnection.generateCertificate() from within an iframe, passing a large RSA modulus (e.g., 4096 bits) to intentionally delay the asynchronous generation process.
  2. Promise Binding: On the main thread, RTCPeerConnection::generateCertificate creates a ScriptPromiseResolver and wraps it in a thread-local cppgc::Persistent handle (WrapPersistent(resolver)). This handle is bound to the completion callback using WTF::BindOnce.
  3. Cross-Thread Handoff: The resulting base::OnceCallback is passed to the WebRTC worker thread via CrossThreadBindOnce and PostCrossThreadTask in RTCCertificateGeneratorRequest::GenerateCertificateAsync.
  4. Assertion Bypass: The cross-thread static assertions (CheckGCedTypeRestrictions, etc.) verify that a base::OnceCallback is safe to pass across threads, but they do not recursively inspect the callback’s BindState. The thread-unsafe cppgc::Persistent handle is therefore successfully passed to the worker thread.
  5. Race Condition Window: The worker thread performs synchronous RSA key generation, blocking for several seconds.
  6. Frame Detachment: During this window, the attacker’s script removes the iframe from the DOM. This detaches the frame, shuts down its ExecutionContext, and eventually shuts down its detached task queues (including blink::TaskType::kInternalMedia).
  7. Task Posting Failure: When the worker thread finishes generating the key, it attempts to post the result back to the main thread via PostCrossThreadTask using the now-destroyed kInternalMedia task runner.
  8. Off-Thread Destruction: Because the task queue is shut down, PostTask fails. PostCrossThreadTask takes ownership of the callback and immediately destroys it on the current thread (the worker thread).
  9. Lockless Modification: The callback’s destruction invokes ~BasicPersistent(), which calls PersistentRegionBase::FreeNode(). In release builds, thread-safety DCHECKs are compiled out. FreeNode() updates the main-thread PersistentRegion free-list without any locking.

Exploitation and Impact

This lockless modification races with concurrent allocations or frees on the main thread. A “lost update” interleaving can cause a live, in-use PersistentNode to be erroneously linked back into the free-list.

When the main thread subsequently allocates a new Persistent handle, it pops the corrupted node. Two distinct Persistent handles now alias the exact same PersistentNode memory. The new handle overwrites the owner_ and trace_ fields of the node.

During Garbage Collection, the original Persistent handle is completely skipped because the node no longer points to it. The GC will collect the underlying object, resulting in a Use-After-Free (UAF) when the original handle is later accessed by Blink C++. Additionally, overlapping nodes can lead to type confusion.

Note: cppgc::PersistentNode uses raw pointers in a union, which explicitly cannot be protected by MiraclePtr (BackupRefPtr).

Suggested Fix

Ensure that cppgc::Persistent handles are not captured by value in callbacks that might be destroyed off-thread. One potential fix is to bind a CrossThreadPersistent handle instead of a regular Persistent handle, or ensure the callback is wrapped in a mechanism that guarantees destruction on the original sequence (e.g., using base::ScopedClosureRunner bound to the main thread, although care must be taken if the target queue is shut down).

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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