High watch 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
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker504612429
Fix commitebdeef338b47 (chromium/src) +12/-15
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-21 (public git)
Disclosed2026-05-05
Patch gap14 days — fix public in source before the advisory

Background

`RTCVideoEncoder` / `RTCVideoEncoder::Impl`
Blink’s hardware-accelerated WebRTC video encoder; the outer object lives on the WebRTC worker sequence while Impl runs on the GPU/media sequence.
`SimulcastToSvcConverter`
a helper that rewrites simulcast encoding requests into SVC layers, held per-encoder as the Impl member simulcast_to_svc_converter_.
Sequence checkers
DCHECK_CALLED_ON_VALID_SEQUENCE guards 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.

Key insight
The converter was the one encoder field set out-of-band on the wrong sequence after init instead of being passed in with everything else; moving it into CreateAndInitializeVEA’s parameter list makes its write happen on the Impl sequence, eliminating the data race (and the UAF it produced).

Attack Path

  1. Start a racing encode session A page uses WebRTC with simulcast such that a SimulcastToSvcConverter is created and encoder initialization is posted to the GPU/media sequence.
  2. Concurrent member write On the WebRTC sequence the old code calls impl_->SetSimulcastToSvcConverter(...) while CreateAndInitializeVEA runs on the Impl sequence, so both sequences touch simulcast_to_svc_converter_ without synchronization.
  3. Lose the race The unsynchronized write/read interleaves with initialization teardown or reassignment, leaving a stale/destroyed converter referenced on the encoder sequence.
  4. 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

A web page that establishes a WebRTC session using hardware encoding with simulcast can drive the timing-dependent data race in the renderer/GPU interaction, with no special privileges beyond initiating media. The advisory rates this high severity and describes it as a use after free in WebRTC; it does not report in-the-wild exploitation. Escalation of the UAF into a controllable read/write or code execution is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
modified
Impl
third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h
modified

Files Changed

  • third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_video_encoder.h

Audit Directions

  • Post-init out-of-band setters
    Audit 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 access
    Review members guarded by one SEQUENCE_CHECKER that are also written from another sequence; confirm every write is posted to the owning sequence.
  • `CrossThreadBindOnce` argument completeness
    Check that all state a cross-thread task needs is captured in its bound arguments rather than mutated afterward on the posting thread.
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);
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. Read: The GPU task runner reads it inside RTCVideoEncoder::Impl::EncodeOneFrame and makes virtual method calls on its owned objects.
  2. Write: The WebRTC encoder thread move-assigns it directly inside RTCVideoEncoder::Impl::SetSimulcastToSvcConverter during InitEncode.

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.

  1. A page creates an RTCPeerConnection using VP9 on Windows with a hardware encoder supporting SVC scalability.
  2. 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.
  3. The page triggers a same-layer-count reconfiguration (e.g., track.applyConstraints()) that allows the RTCVideoEncoder::Impl instance to be reused.
  4. During the drain/flush sequence, the race between the InputBufferReleased task and the WebRTC thread’s SetSimulcastToSvcConverter call 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.

View on issue tracker