CVE-2026-11054
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc |
modified |
Files Changed
third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc
Patch
From cdb9f6eb73f5f00d35087d50778e785baef0b44a Mon Sep 17 00:00:00 2001
From: eshr <eshr@google.com>
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 <guidou@chromium.org>
Commit-Queue: Evan Shrubsole <eshr@google.com>
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))
Original Bug Report
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.ccthird_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.
- An attacker hosts a page with an
<iframe>. - Inside the iframe, JavaScript creates a WebRTC connection and triggers
peerConnection.createOffer()(orcreateAnswer()). - Immediately after triggering the offer, the attacker’s main page removes the iframe from the DOM (
iframe.remove()). - The WebRTC thread finishes generating the offer and attempts to return it to the main thread.
- 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
- When
pc.createOffer()is called, Blink creates aCreateSessionDescriptionRequest(rtc_peer_connection_handler.cc:962). - This object holds a
cppgc::Persistent<RTCSessionDescriptionRequest> webkit_request_to keep the JS request object alive across the async boundary. - A raw pointer to this ref-counted request is passed to the native WebRTC layer, which wraps it in a
scoped_refptrand passes it to the WebRTC signaling thread. - When the iframe is removed, its
FrameSchedulerImplis destroyed, shutting down the frame’sTaskQueueand invalidating itsTaskRunner(main_thread_). - Once the WebRTC signaling thread finishes the offer, it calls
CreateSessionDescriptionRequest::OnSuccessUniquePtr. - Because it is on a background thread, it attempts to post a task back using
PostCrossThreadTask(rtc_peer_connection_handler.cc:164). - The Bug:
PostCrossThreadTaskdirectly usesbase::SequencedTaskRunner::PostTask. Because themain_thread_task runner is shut down,PostTaskreturnsfalse. Consequently, thebase::OnceCallbackand its capturedscoped_refptrare destroyed immediately on the WebRTC signaling thread. - This drops the final reference to the
CreateSessionDescriptionRequest, invoking its destructor (and the~Persistentdestructor) on the signaling thread. PersistentRegionBase::FreeNodeexecutes on the WebRTC thread. In Release builds, theCPPGC_DCHECK(IsCreationThread())is compiled out, so the method writes to the main thread’sfree_list_head_without taking any locks.- Concurrently, the main thread may be allocating new
Persistenthandles. The unsynchronized writes corrupt the singly-linked free-list, allowing two livePersistenthandles to share the samePersistentNode. 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:
- Use
CrossThreadPersistent: Change the type ofwebkit_request_fromcppgc::Persistenttocppgc::CrossThreadPersistent.CrossThreadPersistentRegionuses a process-global lock (PersistentRegionLock), which safely handles node allocation and destruction across thread boundaries. - 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::BindPostTaskusesbase::OnTaskRunnerDeleterto 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.