CVE-2026-10947
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forpc/jsep_transport_collection.cc |
modified |
Files Changed
pc/jsep_transport_collection.cc
Patch
From 52de8e0bd243a300837334db34260e7dd911ab3f Mon Sep 17 00:00:00 2001 From: Tommi <[email protected]> Date: Wed, 22 Apr 2026 05:02:28 +0200 Subject: [PATCH] Fix short-circuit evaluation in RollbackTransports This prevents Use-After-Free by ensuring all map_change_callback_ invocations occur during rollback, even if one fails. Bug: chromium:504597736 Change-Id: If5dd44bcdec27d06af18fc74c526264ab08ae9d1 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/465880 Commit-Queue: Danil Chapovalov <[email protected]> Auto-Submit: Tomas Gunnarsson <[email protected]> Reviewed-by: Danil Chapovalov <[email protected]> Cr-Commit-Position: refs/heads/main@{#47513} --- diff --git a/pc/jsep_transport_collection.cc b/pc/jsep_transport_collection.cc index 6d8dbf0..3c469ff 100644 --- a/pc/jsep_transport_collection.cc +++ b/pc/jsep_transport_collection.cc @@ -269,14 +269,16 @@ // First, remove any new mid->transport mappings. for (const auto& kv : mid_to_transport_) { if (stable_mid_to_transport_.count(kv.first) == 0) { - ret = ret && map_change_callback_(kv.first, nullptr); + bool success = map_change_callback_(kv.first, nullptr); + ret = ret && success; } } // Next, restore old mappings. for (const auto& kv : stable_mid_to_transport_) { auto it = mid_to_transport_.find(kv.first); if (it == mid_to_transport_.end() || it->second != kv.second) { - ret = ret && map_change_callback_(kv.first, kv.second); + bool success = map_change_callback_(kv.first, kv.second); + ret = ret && success; } } mid_to_transport_ = stable_mid_to_transport_;
Original Bug Report
Potential UAF in JsepTransportCollection::RollbackTransports due to short-circuit evaluation
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in WebRTC’s JsepTransportCollection::RollbackTransports uses short-circuiting (&&) when invoking callbacks. This allows an attacker to skip transport updates for certain transceivers during rollback by intentionally causing prior callbacks to fail (e.g., via SSRC conflicts). The bypassed transceivers retain dangling raw pointers to pending transports that are unconditionally freed at the end of the rollback process.
Affected files:
third_party/webrtc/pc/jsep_transport_collection.ccthird_party/webrtc/pc/channel.hthird_party/webrtc/pc/channel.cc
Estimated timestamp from git blame: 2025-04-07
Root Cause Analysis
A potential Use-After-Free (UAF) exists in WebRTC’s transport rollback mechanism. In third_party/webrtc/pc/jsep_transport_collection.cc, the RollbackTransports function restores BaseChannel instances to their stable RtpTransport assignments. The iteration looks like this:
bool ret = true;
// ...
// Next, restore old mappings.
for (const auto& kv : stable_mid_to_transport_) {
auto it = mid_to_transport_.find(kv.first);
if (it == mid_to_transport_.end() || it->second != kv.second) {
ret = ret && map_change_callback_(kv.first, kv.second); // Potential short-circuit
}
}
mid_to_transport_ = stable_mid_to_transport_;
// ...
DestroyUnusedTransports(); // Unconditional destruction
The boolean variable ret tracks the success of the map_change_callback_ (which eventually calls BaseChannel::SetRtpTransport). If any invocation returns false, ret becomes false. Because C++ uses short-circuit evaluation for the logical AND (&&) operator, the right-hand side map_change_callback_ is not executed for any subsequent elements in the loop.
Despite this failure, the function proceeds to unconditionally reset mid_to_transport_ and calls DestroyUnusedTransports(). This destroys any RtpTransport instances created for the pending state.
If a transceiver’s callback is skipped due to the short-circuit, its BaseChannel is never instructed to disconnect from its pending transport. The channel’s RtpTransportInternal* rtp_transport_ field (third_party/webrtc/pc/channel.h) remains pointing to the pending transport. Once DestroyUnusedTransports() frees that transport, rtp_transport_ becomes a dangling raw pointer. Note that rtp_transport_ is a raw C++ pointer, meaning it is not protected by MiraclePtr/base::raw_ptr.
Potential Exploitation Scenario
We do not yet have a working proof of concept, but the following steps suggest how an attacker could exploit this logic flaw from malicious JavaScript:
- Stable State: Establish an
RTCPeerConnectionwith three transceivers. Assign them MIDs"A","B", and"E"to control the alphabetical iteration order of the internalflat_map. Bundle all three transceivers onto a singleJsepTransport. - Malicious Offer: Apply a remote SDP offer (
setRemoteDescription) that:- Splits the BUNDLE groups:
"A"on one transport, and"B"and"E"on a second transport. - Injects the exact same
a=ssrcvalue for both"A"and"B".
- Splits the BUNDLE groups:
- Bypassing Validation: WebRTC’s
ApplyRemoteDescriptionlacks global SSRC duplicate validation. It relies on theRtpDemuxerto detect conflicts. Because"A"and"B"are placed on different BUNDLE transports (and thus different demuxers), no conflict is detected, and the pending offer is applied successfully. - Rollback: Initiate a rollback (
setLocalDescription({type: 'rollback'})). - Triggering the Conflict:
RollbackTransportsiterates in alphabetical order ("A", then"B", then"E")."A"successfully restores to the stable demuxer, registering its malicious SSRC."B"attempts to restore to the same stable demuxer.RtpDemuxer::AddSinkdetects that"B"’s SSRC conflicts with the SSRC just registered by"A". The callback fails and returnsfalse.
- Short-Circuit and UAF: Due to
retbecomingfalse, themap_change_callback_for"E"is skipped entirely due to short-circuiting."E"’s channel retains its raw pointer to its pending transport.DestroyUnusedTransports()then frees the pending transport. - Remote Code Execution: The attacker can reclaim the freed
RtpTransportInternalobject using JS heap-spraying techniques. Any subsequent operation on transceiver"E"(e.g., closing the PeerConnection) will dereference the dangling pointer. BecauseRtpTransportInternalis polymorphic, calling a virtual method (likeUnregisterRtpDemuxerSink) allows the attacker to hijack the instruction pointer and achieve RCE in the renderer process.
Suggested Fix
Ensure that the short-circuiting behavior is removed so that all transceivers are processed during a rollback, even if previous ones fail. For example, use the bitwise AND operator (&) or a temporary variable to force evaluation:
bool success = map_change_callback_(kv.first, kv.second);
ret = ret && success;
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
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.