Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker504620824
Fix commit458a75ec3e37 (src) +15/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-19

Changed Functions

FunctionChangeNotes
TEST_F
media/engine/simulcast_encoder_adapter_unittest.cc
modified
for
media/engine/simulcast_encoder_adapter_unittest.cc
modified

Files Changed

  • media/engine/simulcast_encoder_adapter.cc
  • media/engine/simulcast_encoder_adapter_unittest.cc
From 458a75ec3e375c6a858dba2092eed8eae36439d7 Mon Sep 17 00:00:00 2001
From: Erik Språng <sprang@webrtc.org>
Date: Tue, 21 Apr 2026 13:50:05 +0200
Subject: [PATCH] Call Release in SimulcastEncoderAdapter destructor if inited.

This check should be superflous once
https://webrtc-review.git.corp.google.com/c/src/+/465582 has landed,
but just to be safe make sure we always call Release() in the destructor
if it hasn't already been called.

Bug: chromium:504620824
Change-Id: I70323b1a71fd9830f0d7af62ebeced05dd833d54
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/465600
Auto-Submit: Erik Språng <sprang@webrtc.org>
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Sergey Silkin <ssilkin@webrtc.org>
Commit-Queue: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47496}
---

diff --git a/media/engine/simulcast_encoder_adapter.cc b/media/engine/simulcast_encoder_adapter.cc
index 06aebdc..2f61bdd 100644
--- a/media/engine/simulcast_encoder_adapter.cc
+++ b/media/engine/simulcast_encoder_adapter.cc
@@ -342,7 +342,9 @@
 
 SimulcastEncoderAdapter::~SimulcastEncoderAdapter() {
   RTC_DCHECK_RUN_ON(&encoder_queue_checker_);
-  RTC_DCHECK(!Initialized());
+  if (Initialized()) {
+    Release();
+  }
   DestroyStoredEncoders();
 }
 
diff --git a/media/engine/simulcast_encoder_adapter_unittest.cc b/media/engine/simulcast_encoder_adapter_unittest.cc
index 5c10aee..ff843b7 100644
--- a/media/engine/simulcast_encoder_adapter_unittest.cc
+++ b/media/engine/simulcast_encoder_adapter_unittest.cc
@@ -790,6 +790,18 @@
   EXPECT_EQ(0, adapter_->Release());
 }
 
+TEST_F(TestSimulcastEncoderAdapterFake, DestructorCallsReleaseIfInitialized) {
+  SetupCodec();
+  std::vector<MockVideoEncoder*> encoders = helper_->factory()->encoders();
+  ASSERT_EQ(3u, encoders.size());
+
+  for (auto* encoder : encoders) {
+    EXPECT_CALL(*encoder, ReleaseMock()).WillOnce(testing::Return(0));
+  }
+
+  adapter_.reset();
+}
+
 TEST_F(TestSimulcastEncoderAdapterFake, Reinit) {
   SetupCodec();
   EXPECT_EQ(0, adapter_->Release());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/engine/simulcast_encoder_adapter_unittest.cc b/media/engine/simulcast_encoder_adapter_unittest.cc
index 5c10aee..ff843b7 100644
--- a/media/engine/simulcast_encoder_adapter_unittest.cc
+++ b/media/engine/simulcast_encoder_adapter_unittest.cc
@@ -790,6 +790,18 @@
   EXPECT_EQ(0, adapter_->Release());
 }
 
+TEST_F(TestSimulcastEncoderAdapterFake, DestructorCallsReleaseIfInitialized) {
+  SetupCodec();
+  std::vector<MockVideoEncoder*> encoders = helper_->factory()->encoders();
+  ASSERT_EQ(3u, encoders.size());
+
+  for (auto* encoder : encoders) {
+    EXPECT_CALL(*encoder, ReleaseMock()).WillOnce(testing::Return(0));
+  }
+
+  adapter_.reset();
+}
+
 TEST_F(TestSimulcastEncoderAdapterFake, Reinit) {
   SetupCodec();
   EXPECT_EQ(0, adapter_->Release());
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in SimulcastEncoderAdapter during encoder reconfiguration

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 race condition in SimulcastEncoderAdapter can lead to a Use-After-Free (UAF) on the renderer process heap. When VideoStreamEncoder reconfigures the encoder, it resets the encoder without properly releasing it, causing internal data structures to be destroyed while background threads can still access them.

Affected files:

  • third_party/webrtc/media/engine/simulcast_encoder_adapter.h
  • third_party/webrtc/media/engine/simulcast_encoder_adapter.cc
  • third_party/webrtc/video/video_stream_encoder.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc

Estimated timestamp from git blame: 2026-02-09

Summary

A potential Use-After-Free (UAF) vulnerability exists in SimulcastEncoderAdapter within the WebRTC component of the renderer process. The issue arises from an incorrect member destruction sequence when VideoStreamEncoder replaces an active encoder without first calling Release(). This race condition allows background media/GPU threads to perform UAF reads and writes to a destroyed std::deque on the renderer process heap.

Root Cause Analysis

In third_party/webrtc/video/video_stream_encoder.cc, the ReconfigureEncoder() method handles format changes (e.g., codec changes or simulcast layer adjustments). When pending_encoder_creation_ is true, it calls encoder_.reset() to destroy the existing encoder instance (line 1070) before creating a new one. Critically, it does not call ReleaseEncoder() prior to resetting the unique pointer.

This synchronous destruction invokes SimulcastEncoderAdapter::~SimulcastEncoderAdapter(). While the user-defined destructor contains an RTC_DCHECK(!Initialized()) (which would catch the missing Release() call in Debug builds), this check is compiled out in production Release builds. After the destructor body finishes, C++ begins automatically destroying the class members in the reverse order of their declaration.

In third_party/webrtc/media/engine/simulcast_encoder_adapter.h, the members are declared as follows:

std::vector<StreamContext> stream_contexts_;
EncodedImageCallback* encoded_complete_callback_;
mutable Mutex pending_frames_mutex_;
std::deque<PendingFrame> pending_frames_;

Because pending_frames_ and pending_frames_mutex_ are declared after stream_contexts_, they are destroyed first. The std::deque destructor executes, freeing the chunks of memory that stored the pending frames.

However, the synchronization barrier that stops concurrent callbacks from the background media/GPU thread resides in the destructor of StreamContext (elements of stream_contexts_), which calls Release() on the underlying encoder and unregisters the callback. Since stream_contexts_ is destroyed last, the underlying encoders remain active and continue processing frames while pending_frames_ is being destroyed.

If an underlying encoder finishes a frame concurrently, the media thread fires the OnEncodedImage callback. This callback jumps into the partially destroyed SimulcastEncoderAdapter object, attempts to acquire the destroyed pending_frames_mutex_ (which results in undefined behavior and typically proceeds on POSIX systems), and executes std::find_if on the destroyed pending_frames_ deque.

Potential Attacker Steps

  1. An attacker creates a malicious webpage that establishes an RTCPeerConnection with simulcast video encoding.
  2. The attacker uses JavaScript to trigger an encoder reconfiguration by modifying simulcast layers or changing the video codec via SDP renegotiation.
  3. Concurrently, the attacker uses JavaScript to groom the renderer heap, aiming to reclaim the memory chunks freed when pending_frames_ is destroyed.
  4. When encoder_.reset() is called during reconfiguration, the deque is destroyed and its memory freed, while the background thread races to process an encoded frame.
  5. The background thread executes OnEncodedImage(), which accesses the freed deque memory. If the attacker successfully reclaims the memory, they can control the find_if traversal, causing the code to perform a UAF write (it->expected_layer_index.reset(stream_idx)) and potentially trigger std::deque::erase(), leading to extensive memory corruption.

Note: These are suggested steps; our tooling has not yet executed a working proof-of-concept.

Impact

This vulnerability allows for a UAF write and memory corruption on the renderer heap. Because the corrupted memory belongs to a std::deque’s internal data structures (which rely on raw pointers rather than raw_ptr), MiraclePtr (BackupRefPtr) does not provide mitigation. Successful exploitation could lead to arbitrary code execution within the sandboxed renderer process.

Suggested Fix

There are two primary ways to fix this issue:

  1. In VideoStreamEncoder: Ensure that ReleaseEncoder() is called before encoder_.reset() in ReconfigureEncoder().
  2. In SimulcastEncoderAdapter: Change the declaration order of members in simulcast_encoder_adapter.h so that pending_frames_ and pending_frames_mutex_ are declared before stream_contexts_. Alternatively, explicitly call Release() or explicitly clear stream_contexts_ within ~SimulcastEncoderAdapter().

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