CVE-2026-7928
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc |
modified | |
Implthird_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h |
modified |
Files Changed
third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.ccthird_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h
Patch
From ebdeef338b474242acdfb5c138bd1e6e1528c033 Mon Sep 17 00:00:00 2001
From: Ilya Nikolaevskiy <ilnik@chromium.org>
Date: Tue, 21 Apr 2026 05:37:39 -0700
Subject: [PATCH] Pass SimToSvcConverter in RtcVideoEncoder::Impl initalization
This was the only piece which was set separately after the initialization.
This was causing a data race.
Now it's sent in the same path as everyting else and is set on the correct
thread.
Alternative was to pass it in vea_config instead as a new functions parameter,
but this is a RtcVideoEncoder detail, so it was not reasonable to include
it in VEA::config.
Fixed: 504612429
Change-Id: Iacbf82cd844e85c1119f9d20f48e9b96057cc4f5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7780805
Reviewed-by: Guido Urdaneta <guidou@chromium.org>
Commit-Queue: Ilya Nikolaevskiy <ilnik@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618101}
---
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
index 662f22c0..4ee951f 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
@@ -77,7 +77,6 @@
#include "third_party/webrtc/modules/video_coding/codecs/h264/include/h264.h"
#include "third_party/webrtc/modules/video_coding/include/video_error_codes.h"
#include "third_party/webrtc/modules/video_coding/svc/create_scalability_structure.h"
-#include "third_party/webrtc/modules/video_coding/svc/simulcast_to_svc_converter.h"
#include "third_party/webrtc/rtc_base/time_utils.h"
namespace {
@@ -723,6 +722,7 @@
// its own thread, hence the |init_event| argument.
void CreateAndInitializeVEA(
const media::VideoEncodeAccelerator::Config& vea_config,
+ std::optional<webrtc::SimulcastToSvcConverter> simulcast_to_svc_converter,
SignaledValue init_event);
// Enqueue a frame from WebRTC for encoding. This function is called
@@ -768,9 +768,6 @@
void Drain(SignaledValue event);
void DrainCompleted(bool success);
- void SetSimulcastToSvcConverter(std::optional<webrtc::SimulcastToSvcConverter>
- simulcast_to_svc_converter);
-
private:
enum {
kInputBufferExtraCount = 1, // The number of input buffers allocated, more
@@ -1080,11 +1077,13 @@
void RTCVideoEncoder::Impl::CreateAndInitializeVEA(
const media::VideoEncodeAccelerator::Config& vea_config,
+ std::optional<webrtc::SimulcastToSvcConverter> simulcast_to_svc_converter,
SignaledValue init_event) {
TRACE_EVENT0("webrtc", "RTCVideoEncoder::Impl::CreateAndInitializeVEA");
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ simulcast_to_svc_converter_ = std::move(simulcast_to_svc_converter);
status_ = WEBRTC_VIDEO_CODEC_UNINITIALIZED;
async_init_event_ = ScopedSignaledValue(std::move(init_event));
@@ -1286,11 +1285,6 @@
}
}
-void RTCVideoEncoder::Impl::SetSimulcastToSvcConverter(
- std::optional<webrtc::SimulcastToSvcConverter> simulcast_to_svc_converter) {
- simulcast_to_svc_converter_ = std::move(simulcast_to_svc_converter);
-}
-
void RTCVideoEncoder::Impl::UseOutputBitstreamBuffer(
int32_t bitstream_buffer_id) {
TRACE_EVENT0("webrtc", "RTCVideoEncoder::Impl::UseOutputBitstreamBuffer");
@@ -2607,7 +2601,8 @@
}
int32_t RTCVideoEncoder::InitializeEncoder(
- const media::VideoEncodeAccelerator::Config& vea_config) {
+ const media::VideoEncodeAccelerator::Config& vea_config,
+ std::optional<webrtc::SimulcastToSvcConverter> simulcast_to_svc_converter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(webrtc_sequence_checker_);
TRACE_EVENT1("webrtc", "RTCVideoEncoder::InitEncode", "config",
vea_config.AsHumanReadableString());
@@ -2627,7 +2622,7 @@
*gpu_task_runner_.get(), FROM_HERE,
CrossThreadBindOnce(
&RTCVideoEncoder::Impl::CreateAndInitializeVEA, weak_impl_,
- vea_config,
+ vea_config, std::move(simulcast_to_svc_converter),
SignaledValue(&initialization_waiter, &initialization_retval)));
// webrtc::VideoEncoder expects this call to be synchronous.
initialization_waiter.Wait();
@@ -2878,12 +2873,11 @@
media::VideoEncodeAccelerator::Config::EncoderType::kNoPreference;
}
- int32_t initialization_ret = InitializeEncoder(vea_config);
+ int32_t initialization_ret =
+ InitializeEncoder(vea_config, std::move(simulcast_to_svc_converter));
if (initialization_ret != WEBRTC_VIDEO_CODEC_OK) {
ReleaseImpl();
CHECK(!impl_);
- } else {
- impl_->SetSimulcastToSvcConverter(std::move(simulcast_to_svc_converter));
}
return initialization_ret;
}
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h
index 6be01325..c089928 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h
@@ -25,6 +25,7 @@
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/webrtc/api/video/video_bitrate_allocation.h"
#include "third_party/webrtc/modules/video_coding/include/video_codec_interface.h"
+#include "third_party/webrtc/modules/video_coding/svc/simulcast_to_svc_converter.h"
#include "ui/gfx/geometry/size.h"
#if BUILDFLAG(RTC_USE_H265)
@@ -95,7 +96,9 @@
class Impl;
int32_t InitializeEncoder(
- const media::VideoEncodeAccelerator::Config& vea_config);
+ const media::VideoEncodeAccelerator::Config& vea_config,
+ std::optional<webrtc::SimulcastToSvcConverter>
+ simulcast_to_svc_converter);
void UpdateEncoderInfo(
media::VideoEncoderInfo encoder_info,
std::vector<webrtc::VideoFrameBuffer::Type> preferred_pixel_formats);
Original Bug Report
Potential UAF via race condition in RTCVideoEncoder::Impl::SetSimulcastToSvcConverter
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 between the WebRTC encoder thread and the GPU task runner in RTCVideoEncoder allows concurrent access to simulcast_to_svc_converter_ without locking. If an attacker triggers an encoder reconfiguration while input frames are queued, a task ordering race can result in a Heap Use-After-Free during a virtual method call. This vulnerability could lead to arbitrary code execution within the renderer process.
Affected files:
third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
Estimated timestamp from git blame: 2024-09-20
Summary
A potential race condition in RTCVideoEncoder::Impl::SetSimulcastToSvcConverter can lead to a heap use-after-free (UAF) in the renderer process. The vulnerability occurs due to unsynchronized cross-thread access to the simulcast_to_svc_converter_ member between the WebRTC encoder thread and the GPU task runner during encoder reconfiguration.
Root Cause Analysis
In third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc, the simulcast_to_svc_converter_ member is accessed by two distinct threads without synchronization:
- Read: The GPU task runner reads it inside
RTCVideoEncoder::Impl::EncodeOneFrameand makes virtual method calls on its owned objects. - Write: The WebRTC encoder thread move-assigns it directly inside
RTCVideoEncoder::Impl::SetSimulcastToSvcConverterduringInitEncode.
A race window opens when an RTCPeerConnection video track is reconfigured (e.g., via applyConstraints()) while the encoder has frames waiting in pending_frames_.
During reconfiguration, the WebRTC thread calls DrainEncoderAndUpdateFrameSize, posting an Impl::Drain task to the GPU task runner and blocking on drain_waiter.Wait(). Impl::Drain issues a Mojo Flush command to the hardware encoder.
As the hardware encoder finishes previous Encode calls, it releases its references to the input media::VideoFrame objects. The destruction observer for these frames (RTCVideoEncoder::Impl::InputBufferReleased) is posted back to the GPU task runner via base::BindPostTaskToCurrentDefault(). Because it is posted, it lands at the end of the GPU task runner’s queue.
When the Flush operation completes, Mojo directly executes the RTCVideoEncoder::Impl::DrainCompleted callback. This means DrainCompleted is executed by the GPU task runner before the asynchronously posted InputBufferReleased task.
DrainCompleted signals drain_waiter, immediately unblocking the WebRTC thread. The WebRTC thread then returns to InitEncode and executes impl_->SetSimulcastToSvcConverter(std::move(...)), destroying the old SimulcastToSvcConverter object.
Concurrently, the GPU task runner finishes DrainCompleted and executes InputBufferReleased. It finds frames in pending_frames_, calls EncodeOneFrame, and attempts to execute virtual methods on the ScalableVideoController objects owned by the SimulcastToSvcConverter that the WebRTC thread is currently destroying.
Impact
This race results in a heap Use-After-Free targeting std::vector<LayerState> elements and the std::unique_ptr<webrtc::ScalableVideoController> objects they contain. Because the UAF targets an object with virtual methods (ScalableVideoController::OnEncodeDone), an attacker capable of manipulating the heap could potentially hijack the virtual call and achieve arbitrary code execution in the renderer process.
This code resides partially in the WebRTC codebase (third_party/webrtc/modules/video_coding/svc/), where std::unique_ptr is used instead of base::raw_ptr. As such, this vulnerability is not mitigated by MiraclePtr (BackupRefPtr).
Potential Trigger Path
Please note: These are theoretical steps; our automated tooling has not verified this with an executable Proof-of-Concept.
- A page creates an
RTCPeerConnectionusing VP9 on Windows with a hardware encoder supporting SVC scalability. - The page provides frames to the encoder faster than it can process them, causing the
pending_frames_queue to fill and hardware input buffers to become exhausted. - The page triggers a same-layer-count reconfiguration (e.g.,
track.applyConstraints()) that allows theRTCVideoEncoder::Implinstance to be reused. - During the drain/flush sequence, the race between the
InputBufferReleasedtask and the WebRTC thread’sSetSimulcastToSvcConvertercall occurs, triggering the UAF.
Suggested Fix
Ensure that all access to simulcast_to_svc_converter_ is synchronized.
One approach is to use the existing lock_ mutex inside RTCVideoEncoder::Impl to protect read/write access to simulcast_to_svc_converter_. Alternatively, ensure that SetSimulcastToSvcConverter is executed strictly on the GPU task runner (via PostCrossThreadTask), just like other state modifications in RTCVideoEncoder::Impl.
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.