High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Sub-componentBlink>WebRTC>PeerConnection
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Mechanism matches diff. Minor: diff shows mid_to_transport_ is reset to stable_mid_to_transport_ after the loops, so stale state lies with the skipped callback recipients, not the collection itself; how a page makes a callback fail is not shown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker504597736
Fix commit52de8e0bd243 (src) +4/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-20 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-06-02
Patch gap41 days — fix public in source before the advisory

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 a bool success indicator.
Short-circuit evaluation
the C++ rule that in a && b, b is not evaluated when a is already false, so a folded-in ret && map_change_callback_(...) skips the callback once ret has become false.
`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.

Key insight
The core mistake was letting && 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

  1. 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/setRemoteDescription with type rollback), entering RollbackTransports.
  2. Induce an early callback failure Arrange for one map_change_callback_(...) invocation to return false early in the loop, setting ret to false.
  3. 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.
  4. 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

An attacker able to control SDP negotiation and rollback on a 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

FunctionChangeNotes
for
pc/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(...) or x = x || call(...), where short-circuiting can silently skip required calls after the first failure.
  • Partial-failure cleanup loops
    Review 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 reconciliation
    Audit map/collection reconciliation where a callback both reports success and performs the actual state change, ensuring failure reporting is decoupled from executing the change.
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_;
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_party/webrtc/pc/channel.h
  • third_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:

  1. Stable State: Establish an RTCPeerConnection with three transceivers. Assign them MIDs "A", "B", and "E" to control the alphabetical iteration order of the internal flat_map. Bundle all three transceivers onto a single JsepTransport.
  2. 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=ssrc value for both "A" and "B".
  3. Bypassing Validation: WebRTC’s ApplyRemoteDescription lacks global SSRC duplicate validation. It relies on the RtpDemuxer to 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.
  4. Rollback: Initiate a rollback (setLocalDescription({type: 'rollback'})).
  5. Triggering the Conflict: RollbackTransports iterates 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::AddSink detects that "B"’s SSRC conflicts with the SSRC just registered by "A". The callback fails and returns false.
  6. Short-Circuit and UAF: Due to ret becoming false, the map_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.
  7. Remote Code Execution: The attacker can reclaim the freed RtpTransportInternal object using JS heap-spraying techniques. Any subsequent operation on transceiver "E" (e.g., closing the PeerConnection) will dereference the dangling pointer. Because RtpTransportInternal is polymorphic, calling a virtual method (like UnregisterRtpDemuxerSink) 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.

View on issue tracker