CVE-2026-7928
Overview
Background
- `RTCVideoEncoder` / `RTCVideoEncoder::Impl`
- Blink’s hardware-accelerated WebRTC video encoder; the outer object lives on the WebRTC worker sequence while
Implruns on the GPU/media sequence. - `SimulcastToSvcConverter`
- a helper that rewrites simulcast encoding requests into SVC layers, held per-encoder as the
Implmembersimulcast_to_svc_converter_. - Sequence checkers
DCHECK_CALLED_ON_VALID_SEQUENCEguards assert that a method runs on its owning sequence; crossing sequences to touch a member without posting is a data race.- `CrossThreadBindOnce`
- binds a task (here
CreateAndInitializeVEA) to run on another sequence, carrying its arguments across the thread boundary.
Root Cause Analysis
Initialization posted RTCVideoEncoder::Impl::CreateAndInitializeVEA to the GPU/media sequence via CrossThreadBindOnce, but the simulcast_to_svc_converter_ member was set separately afterward through Impl::SetSimulcastToSvcConverter, called from the WebRTC sequence in the caller’s else branch (impl_->SetSimulcastToSvcConverter(...)). Because CreateAndInitializeVEA reads/uses simulcast_to_svc_converter_ on the Impl’s own sequence while the separate setter wrote it from a different sequence, two threads accessed the same member without synchronization — a data race, with a lifetime/ordering hazard that manifests as use-after-free when one sequence tears down or reassigns the converter while the other is using it.
The fix threads simulcast_to_svc_converter as an explicit parameter all the way from InitializeEncoder through CrossThreadBindOnce into CreateAndInitializeVEA, which assigns simulcast_to_svc_converter_ = std::move(...) on its own (sequence-checked) sequence; the standalone SetSimulcastToSvcConverter setter and its cross-sequence call site are deleted, so the member is now only ever written on the correct thread as part of the single initialization path.
CreateAndInitializeVEA’s parameter list makes its write happen on the Impl sequence, eliminating the data race (and the UAF it produced).Attack Path
- Start a racing encode session
A page uses WebRTC with simulcast such that a
SimulcastToSvcConverteris created and encoder initialization is posted to the GPU/media sequence. - Concurrent member write
On the WebRTC sequence the old code calls
impl_->SetSimulcastToSvcConverter(...)whileCreateAndInitializeVEAruns on theImplsequence, so both sequences touchsimulcast_to_svc_converter_without synchronization. - Lose the race The unsynchronized write/read interleaves with initialization teardown or reassignment, leaving a stale/destroyed converter referenced on the encoder sequence.
- Use after free
Subsequent encode work dereferences the raced
simulcast_to_svc_converter_, producing the use-after-free the advisory reports (any further primitive is inferred and not shown by the diff).
Impact Assessment
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
Audit Directions
- Post-init out-of-band settersAudit objects initialized via a posted task where some members are instead set by a separate setter from the originating sequence; fold such state into the initialization task’s parameters.
- Cross-sequence member accessReview members guarded by one
SEQUENCE_CHECKERthat are also written from another sequence; confirm every write is posted to the owning sequence. - `CrossThreadBindOnce` argument completenessCheck that all state a cross-thread task needs is captured in its bound arguments rather than mutated afterward on the posting thread.
Patch
From ebdeef338b474242acdfb5c138bd1e6e1528c033 Mon Sep 17 00:00:00 2001 From: Ilya Nikolaevskiy <[email protected]> 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 <[email protected]> Commit-Queue: Ilya Nikolaevskiy <[email protected]> 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.