Overview

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

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
modified

Files Changed

  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
From cdb9f6eb73f5f00d35087d50778e785baef0b44a Mon Sep 17 00:00:00 2001
From: eshr <[email protected]>
Date: Tue, 07 Apr 2026 06:03:36 -0700
Subject: [PATCH] Use CrossThreadPersistent for RTCSessionDescriptionRequest

Replace cppgc::Persistent with CrossThreadPersistent in CreateSessionDescriptionRequest
and use BindPostTask to ensure callbacks are invoked and destroyed on the main thread.
This prevents heap corruption when an iframe is detached.

Bug: 498845284
Change-Id: I1958d4c1bea327d16894dd1b3770c6dd6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735123
Reviewed-by: Guido Urdaneta <[email protected]>
Commit-Queue: Evan Shrubsole <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610696}
---

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 388c2572..7f512ad9 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
@@ -66,6 +66,7 @@
 #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_void_request.h"
 #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
+#include "third_party/blink/renderer/platform/wtf/bind_post_task.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_base.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_std.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
@@ -161,12 +162,12 @@
   void OnSuccessUniquePtr(
       std::unique_ptr<webrtc::SessionDescriptionInterface> desc) {
     if (!main_thread_->BelongsToCurrentThread()) {
-      PostCrossThreadTask(
-          *main_thread_.get(), FROM_HERE,
+      BindPostTask(
+          main_thread_,
           CrossThreadBindOnce(
               &CreateSessionDescriptionRequest::OnSuccessUniquePtr,
-              webrtc::scoped_refptr<CreateSessionDescriptionRequest>(this),
-              std::move(desc)));
+              webrtc::scoped_refptr<CreateSessionDescriptionRequest>(this)))
+          .Run(std::move(desc));
       return;
     }
 
@@ -194,12 +195,12 @@
   }
   void OnFailure(webrtc::RTCError error) override {
     if (!main_thread_->BelongsToCurrentThread()) {
-      PostCrossThreadTask(
-          *main_thread_.get(), FROM_HERE,
+      BindPostTask(
+          main_thread_,
           CrossThreadBindOnce(
               &CreateSessionDescriptionRequest::OnFailure,
-              webrtc::scoped_refptr<CreateSessionDescriptionRequest>(this),
-              std::move(error)));
+              webrtc::scoped_refptr<CreateSessionDescriptionRequest>(this)))
+          .Run(std::move(error));
       return;
     }
 
@@ -226,7 +227,7 @@
   }
 
   const scoped_refptr<base::SingleThreadTaskRunner> main_thread_;
-  Persistent<RTCSessionDescriptionRequest> webkit_request_;
+  CrossThreadPersistent<RTCSessionDescriptionRequest> webkit_request_;
   const base::WeakPtr<RTCPeerConnectionHandler> handler_;
   const CrossThreadWeakPersistent<PeerConnectionTracker> tracker_;
   PeerConnectionTracker::Action action_;
@@ -601,7 +602,7 @@
   void OnIceCandidate(const IceCandidate* candidate) override {
     DCHECK(native_peer_connection_);
     std::string sdp = candidate->ToString();
-   DCHECK(!sdp.empty());
+    DCHECK(!sdp.empty());
     // The generated candidate may have been added to the pending or current
     // local description, take a snapshot and surface them to the main thread.
     // Remote descriptions are also surfaced because
@@ -653,8 +654,9 @@
 
   void OnDataChannelImpl(webrtc::scoped_refptr<DataChannelInterface> channel) {
     DCHECK(main_thread_->BelongsToCurrentThread());
-    if (handler_)
+    if (handler_) {
       handler_->OnDataChannel(channel);
+    }
   }
 
   void OnIceCandidateImpl(const String& sdp,
@@ -949,8 +951,9 @@
   DCHECK(task_runner_->RunsTasksInCurrentSequence());
   TRACE_EVENT0("webrtc", "RTCPeerConnectionHandler::createOffer");
 
-  if (peer_connection_tracker_)
+  if (peer_connection_tracker_) {
     peer_connection_tracker_->TrackCreateOffer(this, options);
+  }
 
   webrtc::PeerConnectionInterface::RTCOfferAnswerOptions webrtc_options;
   if (options) {
@@ -1016,8 +1019,9 @@
   native_peer_connection_->CreateAnswer(description_request.get(),
                                         webrtc_options);
 
-  if (peer_connection_tracker_)
+  if (peer_connection_tracker_) {
     peer_connection_tracker_->TrackCreateAnswer(this, options);
+  }
 }
 
 bool IsOfferOrAnswer(const webrtc::SessionDescriptionInterface* native_desc) {
@@ -1030,8 +1034,9 @@
   DCHECK(task_runner_->RunsTasksInCurrentSequence());
   TRACE_EVENT0("webrtc", "RTCPeerConnectionHandler::setLocalDescription");
 
-  if (peer_connection_tracker_)
+  if (peer_connection_tracker_) {
     peer_connection_tracker_->TrackSetSessionDescriptionImplicit(this);
+  }
 
   scoped_refptr<WebRtcSetDescriptionObserverImpl> content_observer =
       base::MakeRefCounted<WebRtcSetDescriptionObserverImpl>(
@@ -1236,8 +1241,9 @@
   new_configuration.always_negotiate_data_channels =
       blink_config.always_negotiate_data_channels;
 
-  if (peer_connection_tracker_)
+  if (peer_connection_tracker_) {
     peer_connection_tracker_->TrackSetConfiguration(this, new_configuration);
+  }
 
   webrtc::RTCError webrtc_error =
       native_peer_connection_->SetConfiguration(new_configuration);
@@ -1293,10 +1299,11 @@
               std::move(current_remote_description));
         }
         // Resolve promise.
-        if (result.ok())
+        if (result.ok()) {
           request->RequestSucceeded();
-        else
+        } else {
           request->RequestFailed(result);
+        }
       };
 
   native_peer_connection_->AddIceCandidate(
@@ -1458,8 +1465,9 @@
       init);
   std::vector<webrtc::scoped_refptr<webrtc::RtpTransceiverInterface>>
       transceivers;
-  if (error_or_transceiver->ok())
+  if (error_or_transceiver->ok()) {
     transceivers.push_back(error_or_transceiver->value());
+  }
   transceiver_state_surfacer->Initialize(native_peer_connection_,
                                          track_adapter_map_, transceivers);
 }
@@ -1519,8 +1527,9 @@
       native_peer_connection_->AddTransceiver(media_type, init);
   std::vector<webrtc::scoped_refptr<webrtc::RtpTransceiverInterface>>
       transceivers;
-  if (error_or_transceiver->ok())
+  if (error_or_transceiver->ok()) {
     transceivers.push_back(error_or_transceiver->value());
+  }
   transceiver_state_surfacer->Initialize(native_peer_connection_,
                                          track_adapter_map_, transceivers);
 }
@@ -1622,8 +1631,9 @@
   DCHECK(task_runner_->RunsTasksInCurrentSequence());
   TRACE_EVENT0("webrtc", "RTCPeerConnectionHandler::RemoveTrack");
   auto it = FindSender(web_sender->Id());
-  if (it == rtp_senders_.end())
+  if (it == rtp_senders_.end()) {
     return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER);
+  }
   const auto& sender = *it;
   auto webrtc_sender = sender->state().webrtc_sender();
 
@@ -1707,17 +1717,20 @@
 
 void RTCPeerConnectionHandler::CloseClientPeerConnection() {
   DCHECK(task_runner_->RunsTasksInCurrentSequence());
-  if (!is_closed_)
+  if (!is_closed_) {
     client_->ClosePeerConnection();
+  }
 }
 
 void RTCPeerConnectionHandler::OnThermalStateChange(
     mojom::blink::DeviceThermalState thermal_state) {
   DCHECK(task_runner_->RunsTasksInCurrentSequence());
-  if (is_closed_)
+  if (is_closed_) {
     return;
-  if (!base::FeatureList::IsEnabled(kWebRtcThermalResource))
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-thread destruction of cppgc::Persistent in RTCPeerConnectionHandler leads to UAF

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 CreateSessionDescriptionRequest can cause a cppgc::Persistent handle to be destroyed on the WebRTC signaling thread when an iframe is detached. This causes an unsynchronized modification of the main thread’s cppgc free-list, leading to Oilpan heap corruption and a potential Use-After-Free (UAF).

Affected files:

  • third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
  • third_party/blink/renderer/platform/heap/persistent.h

Estimated timestamp from git blame: 2025-10-21

Summary

A potential vulnerability exists in rtc_peer_connection_handler.cc where CreateSessionDescriptionRequest incorrectly manages the lifecycle of a cppgc::Persistent handle. If an iframe is detached while an asynchronous WebRTC operation is pending, the request object can be destructed on the WebRTC signaling thread. cppgc::Persistent is not thread-safe; its cross-thread destruction corrupts the main thread’s Garbage Collection (Oilpan) free-list, which can lead to a highly exploitable Use-After-Free (UAF).

Potential Steps to Trigger

Note: These are suggested steps based on static analysis, as our tooling agent cannot yet run live proofs of concept.

  1. An attacker hosts a page with an <iframe>.
  2. Inside the iframe, JavaScript creates a WebRTC connection and triggers peerConnection.createOffer() (or createAnswer()).
  3. Immediately after triggering the offer, the attacker’s main page removes the iframe from the DOM (iframe.remove()).
  4. The WebRTC thread finishes generating the offer and attempts to return it to the main thread.
  5. The free-list is corrupted. The attacker allocates multiple objects to trigger shared nodes, then forces a garbage collection cycle to trigger a UAF on a live JavaScript/DOM object.

Technical Details

  1. When pc.createOffer() is called, Blink creates a CreateSessionDescriptionRequest (rtc_peer_connection_handler.cc:962).
  2. This object holds a cppgc::Persistent<RTCSessionDescriptionRequest> webkit_request_ to keep the JS request object alive across the async boundary.
  3. A raw pointer to this ref-counted request is passed to the native WebRTC layer, which wraps it in a scoped_refptr and passes it to the WebRTC signaling thread.
  4. When the iframe is removed, its FrameSchedulerImpl is destroyed, shutting down the frame’s TaskQueue and invalidating its TaskRunner (main_thread_).
  5. Once the WebRTC signaling thread finishes the offer, it calls CreateSessionDescriptionRequest::OnSuccessUniquePtr.
  6. Because it is on a background thread, it attempts to post a task back using PostCrossThreadTask (rtc_peer_connection_handler.cc:164).
  7. The Bug: PostCrossThreadTask directly uses base::SequencedTaskRunner::PostTask. Because the main_thread_ task runner is shut down, PostTask returns false. Consequently, the base::OnceCallback and its captured scoped_refptr are destroyed immediately on the WebRTC signaling thread.
  8. This drops the final reference to the CreateSessionDescriptionRequest, invoking its destructor (and the ~Persistent destructor) on the signaling thread.
  9. PersistentRegionBase::FreeNode executes on the WebRTC thread. In Release builds, the CPPGC_DCHECK(IsCreationThread()) is compiled out, so the method writes to the main thread’s free_list_head_ without taking any locks.
  10. Concurrently, the main thread may be allocating new Persistent handles. The unsynchronized writes corrupt the singly-linked free-list, allowing two live Persistent handles to share the same PersistentNode. When one is cleared, the GC will fail to trace the other, leading to a UAF.

Suggested Fix

There are two primary ways to fix this:

  1. Use CrossThreadPersistent: Change the type of webkit_request_ from cppgc::Persistent to cppgc::CrossThreadPersistent. CrossThreadPersistentRegion uses a process-global lock (PersistentRegionLock), which safely handles node allocation and destruction across thread boundaries.
  2. Safe Callback Destruction: Instead of using PostCrossThreadTask, use a mechanism that guarantees the callback and its bound arguments are destroyed on the target thread, even if the task runner is shut down. For instance, blink::BindPostTask uses base::OnTaskRunnerDeleter to ensure safe destruction, preventing the closure from being dropped 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