CVE-2026-10947
Overview
Background
- `RollbackTransports`
- the JSEP transport-collection operation that undoes an in-progress negotiation by removing newly-added mid→transport mappings and restoring the previously-stable ones.
- `map_change_callback_`
- a callback invoked for each mid mapping change (
(mid, transport)) that propagates the change to the rest of the system and returns aboolsuccess indicator. - Short-circuit evaluation
- the C++ rule that in
a && b,bis not evaluated whenais alreadyfalse, so a folded-inret && map_change_callback_(...)skips the callback oncerethas becomefalse. - `stable_mid_to_transport_` / `mid_to_transport_`
- the saved stable mapping set and the current (possibly newly-modified) mapping set that rollback reconciles.
Root Cause Analysis
In the rollback loops, each per-mapping callback was invoked as ret = ret && map_change_callback_(...). Once any callback returned false, ret became false, and C++ short-circuit evaluation caused every subsequent map_change_callback_(...) to be skipped entirely.
As a result, some mid mappings that should have been torn down (set to nullptr) or restored during rollback were never actually reverted, leaving stale mid→transport associations pointing at transports that rollback intended to remove, which yields a use-after-free when those dangling mappings are later used.
The fix evaluates the callback into a separate bool success first and only then folds it in with ret = ret && success, so the callback is always executed for every mapping regardless of prior failures. This restores the invariant that rollback fully reconciles all mappings while still reporting overall failure through ret.
&& short-circuit suppress the side-effecting map_change_callback_ calls after the first failure; the fix forces every callback to run by computing its result into success before combining it into ret.Attack Path
- Drive a negotiation then roll it back
The application performs an SDP negotiation that adds new mid→transport mappings and then triggers a rollback (e.g.
setLocalDescription/setRemoteDescriptionwith typerollback), enteringRollbackTransports. - Induce an early callback failure
Arrange for one
map_change_callback_(...)invocation to returnfalseearly in the loop, settingrettofalse. - Subsequent reversions are skipped
Under the old code, short-circuit evaluation skips all following
map_change_callback_(...)calls, so later mid mappings are neither cleared nor restored and continue referencing transports that rollback was supposed to detach. - Stale mapping used after free When a leftover mid→transport mapping is later dereferenced while the associated transport has been freed as part of the incomplete rollback, a use-after-free occurs (the specific dangling transport object and later dereference site are not shown in the diff) (inferred).
Impact Assessment
PeerConnection can leave the transport collection in an inconsistent state, producing a use-after-free in the process running WebRTC (the renderer for browser WebRTC). The advisory classifies this as a use-after-free in WebRTC and does not state exploited-in-the-wild status; escalation from the UAF to a controllable read/write primitive or code execution is not demonstrated by the patch (inferred). The precondition is triggering a rollback in which at least one mapping-change callback fails.Changed Functions
| Function | Change | Notes |
|---|---|---|
forpc/jsep_transport_collection.cc |
modified |
Files Changed
pc/jsep_transport_collection.cc
Audit Directions
- Side effects inside `&&`/`||`Search for boolean accumulators that fold a side-effecting call directly into
x = x && call(...)orx = x || call(...), where short-circuiting can silently skip required calls after the first failure. - Partial-failure cleanup loopsReview rollback/teardown/restore loops that must run to completion for every element even when one step fails, and confirm no error flag prunes later iterations.
- Callback-driven state reconciliationAudit map/collection reconciliation where a callback both reports success and performs the actual state change, ensuring failure reporting is decoupled from executing the change.
Patch
From 52de8e0bd243a300837334db34260e7dd911ab3f Mon Sep 17 00:00:00 2001
From: Tommi <tommi@webrtc.org>
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 <danilchap@webrtc.org>
Auto-Submit: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Danil Chapovalov <danilchap@webrtc.org>
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.