Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker523712556
Fix commit18a41228c0a0 (chromium/src) +61/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
modified

Files Changed

  • third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
  • third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.h
  • third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
From 18a41228c0a061c0a08d01c22e943e4862223f1e Mon Sep 17 00:00:00 2001
From: Erik Språng <sprang@chromium.org>
Date: Thu, 25 Jun 2026 04:27:23 -0700
Subject: [PATCH] Add better handling of RtcVideoDecoderAdapter init timeout.

This CL introduced as new private `OnInitializeDone()` callback method
and uses the `weak_decoder_this_` instead of unreatined
`video_decoder_.get()`.

Bug: 523712556
Change-Id: Icc2f8272b948dbc4fa65d5175dbcf60cea033462
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7992498
Reviewed-by: Guido Urdaneta <guidou@chromium.org>
Commit-Queue: Erik Språng <sprang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1652310}
---

diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
index 2a387ac..0fe5b9a 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
@@ -62,6 +62,8 @@
 // Maximum number of buffers that we will queue in |pending_buffers_|.
 constexpr int32_t kMaxPendingBuffers = 8;
 
+std::optional<base::TimeDelta> g_init_timeout_for_testing;
+
 // Maximum number of timestamps that will be maintained in |decode_timestamps_|.
 // Really only needs to be a bit larger than the maximum reorder distance (which
 // is presumably 0 for WebRTC), but being larger doesn't hurt much.
@@ -234,6 +236,9 @@
   void DecodePendingBuffers();
   void OnDecodeDone(media::DecoderStatus status);
   void OnOutput(scoped_refptr<media::VideoFrame> frame);
+  void OnInitializeDone(CrossThreadOnceFunction<void(bool)> init_cb,
+                        media::VideoDecoderType* decoder_type,
+                        media::DecoderStatus status);
 
   const raw_ptr<media::GpuVideoAcceleratorFactories> gpu_factories_;
   const scoped_refptr<WebRtcVideoFrameAdapter::SharedResources>
@@ -291,22 +296,28 @@
   media::VideoDecoder::OutputCB output_cb =
       ConvertToBaseRepeatingCallback(CrossThreadBindRepeating(
           &RTCVideoDecoderAdapter::Impl::OnOutput, weak_decoder_this_));
+  // Safe to use CrossThreadUnretained(decoder_type) because `decoder_type`
+  // points to the `RTCVideoDecoderAdapter::decoder_type_` member variable,
+  // which is guaranteed to outlive `Impl` since the adapter's destructor
+  // blocks synchronously on the media thread while destroying `Impl` in
+  // `Release()`.
   video_decoder_->Initialize(
-      config, /*low_delay=*/true,
-      /*cdm_context=*/nullptr,
-      base::BindOnce(
-          [](base::OnceCallback<void(bool)> cb,
-             media::VideoDecoderType* decoder_type,
-             media::VideoDecoder* video_decoder, media::DecoderStatus status) {
-            *decoder_type = video_decoder->GetDecoderType();
-            std::move(cb).Run(status.is_ok());
-          },
-          ConvertToBaseOnceCallback(std::move(init_cb)),
-          CrossThreadUnretained(decoder_type),
-          CrossThreadUnretained(video_decoder_.get())),
+      config, /*low_delay=*/true, /*cdm_context=*/nullptr,
+      ConvertToBaseOnceCallback(CrossThreadBindOnce(
+          &RTCVideoDecoderAdapter::Impl::OnInitializeDone, weak_decoder_this_,
+          std::move(init_cb), CrossThreadUnretained(decoder_type))),
       output_cb, base::DoNothing());
 }
 
+void RTCVideoDecoderAdapter::Impl::OnInitializeDone(
+    CrossThreadOnceFunction<void(bool)> init_cb,
+    media::VideoDecoderType* decoder_type,
+    media::DecoderStatus status) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
+  *decoder_type = video_decoder_->GetDecoderType();
+  std::move(init_cb).Run(status.is_ok());
+}
+
 void RTCVideoDecoderAdapter::Impl::Decode(
     scoped_refptr<media::DecoderBuffer> buffer,
     base::WaitableEvent* waiter,
@@ -628,8 +639,8 @@
                               weak_impl_, config, std::move(init_cb),
                               start_time,
                               CrossThreadUnretained(&decoder_type_)))) {
-    // TODO(crbug.com/1076817) Remove if a root cause is found.
-    if (!async_init_waiter_->TimedWait(base::Seconds(10))) {
+    if (!async_init_waiter_->TimedWait(
+            g_init_timeout_for_testing.value_or(base::Seconds(10)))) {
       RecordInitializationLatency(base::TimeTicks::Now() - start_time);
       return false;
     }
@@ -946,4 +957,9 @@
   g_num_decoders_--;
 }
 
+void RTCVideoDecoderAdapter::SetInitializeSyncTimeoutForTesting(
+    std::optional<base::TimeDelta> timeout) {
+  g_init_timeout_for_testing = timeout;
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.h b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.h
index f2ddb988..d4c522e 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.h
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.h
@@ -91,6 +91,8 @@
   static int GetCurrentDecoderCountForTesting();
   static void IncrementCurrentDecoderCountForTesting();
   static void DecrementCurrentDecoderCountForTesting();
+  static void SetInitializeSyncTimeoutForTesting(
+      std::optional<base::TimeDelta> timeout);
 
   static std::atomic<int> g_num_decoders_;
 
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
index 5bc83ddd..eb209540 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
@@ -21,6 +21,7 @@
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
+#include "base/threading/platform_thread.h"
 #include "base/threading/thread.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -889,4 +890,32 @@
   media_thread_.FlushForTesting();
 }
 
+TEST_F(RTCVideoDecoderAdapterTest, InitializeSyncTimeoutRace) {
+  RTCVideoDecoderAdapter::SetInitializeSyncTimeoutForTesting(
+      base::Milliseconds(1));
+
+  base::WaitableEvent initialize_called;
+  media::VideoDecoder::InitCB saved_init_cb;
+
+  EXPECT_CALL(*video_decoder_, Initialize_)
+      .WillOnce(testing::WithArg<3>([&](media::VideoDecoder::InitCB& init_cb) {
+        saved_init_cb = std::move(init_cb);
+        initialize_called.Signal();
+      }));
+
+  ASSERT_FALSE(RTCVideoDecoderAdapterWrapper::Create(
+      &gpu_factories_,
+      webrtc::SdpVideoFormat(
+          webrtc::CodecTypeToPayloadString(webrtc::kVideoCodecVP9)),
+      true));
+
+  initialize_called.Wait();
+  media_thread_.task_runner()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(saved_init_cb),
+                                media::DecoderStatus::Codes::kOk));
+  media_thread_.FlushForTesting();
+
+  RTCVideoDecoderAdapter::SetInitializeSyncTimeoutForTesting(std::nullopt);
+}
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
index 5bc83ddd..eb209540 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
@@ -21,6 +21,7 @@
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
+#include "base/threading/platform_thread.h"
 #include "base/threading/thread.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -889,4 +890,32 @@
   media_thread_.FlushForTesting();
 }
 
+TEST_F(RTCVideoDecoderAdapterTest, InitializeSyncTimeoutRace) {
+  RTCVideoDecoderAdapter::SetInitializeSyncTimeoutForTesting(
+      base::Milliseconds(1));
+
+  base::WaitableEvent initialize_called;
+  media::VideoDecoder::InitCB saved_init_cb;
+
+  EXPECT_CALL(*video_decoder_, Initialize_)
+      .WillOnce(testing::WithArg<3>([&](media::VideoDecoder::InitCB& init_cb) {
+        saved_init_cb = std::move(init_cb);
+        initialize_called.Signal();
+      }));
+
+  ASSERT_FALSE(RTCVideoDecoderAdapterWrapper::Create(
+      &gpu_factories_,
+      webrtc::SdpVideoFormat(
+          webrtc::CodecTypeToPayloadString(webrtc::kVideoCodecVP9)),
+      true));
+
+  initialize_called.Wait();
+  media_thread_.task_runner()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(saved_init_cb),
+                                media::DecoderStatus::Codes::kOk));
+  media_thread_.FlushForTesting();
+
+  RTCVideoDecoderAdapter::SetInitializeSyncTimeoutForTesting(std::nullopt);
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in RTCVideoDecoderAdapter::InitializeSync via Timeout Race Condition

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A race condition exists in RTCVideoDecoderAdapter where initialization can time out, causing the adapter to be destroyed while initialization tasks remain pending. Because WTF::CrossThreadUnretained is used to capture raw pointers, subsequent asynchronous failure callbacks bypass MiraclePtr and access dangling pointers. This results in UAF virtual method calls and memory corruption, potentially leading to renderer Remote Code Execution (RCE).

Affected files:

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

Estimated timestamp from git blame: 2025-11-20

Summary

A race condition in RTCVideoDecoderAdapter::InitializeSync can lead to a heap use-after-free (UAF) in the renderer process. When initialization of the video decoder takes longer than the hardcoded 10-second timeout, InitializeSync returns false, causing the caller to destroy the RTCVideoDecoderAdapter object. However, initialization tasks previously posted to the media thread may still be pending and hold raw pointers to the adapter’s members and the underlying media::VideoDecoder. When these tasks eventually run, they access freed memory.

Vulnerability Details

In RTCVideoDecoderAdapter::InitializeSync (third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc:607), several members are captured via CrossThreadUnretained and passed to a task on the media thread:

  auto init_cb = CrossThreadBindOnce(
      &FinishWait, CrossThreadUnretained(async_init_waiter_.get()),
      CrossThreadUnretained(&async_init_result_));

  if (PostCrossThreadTask(
          *media_task_runner_.get(), FROM_HERE,
          CrossThreadBindOnce(&RTCVideoDecoderAdapter::Impl::Initialize,
                              weak_impl_, config, std::move(init_cb), start_time,
                              CrossThreadUnretained(&decoder_type_)))) {
    if (!async_init_waiter_->TimedWait(base::Seconds(10))) {
      // ...
      return false; // Timeout occurs here
    }

Because WTF::CrossThreadUnretained is used, these pointers decay to raw C++ pointers, completely bypassing MiraclePtr (BackupRefPtr) protection.

The UAF race condition occurs as follows:

  1. Task A Enqueued: InitializeSync posts Impl::Initialize (Task A) to the media thread and blocks via TimedWait(base::Seconds(10)).
  2. Timeout: If the media thread is heavily backlogged, Task A does not execute within 10 seconds. InitializeSync returns false.
  3. Destruction & Task B Enqueued: The caller (e.g., RTCVideoDecoderAdapter::Create) destroys the adapter. The destructor calls Release(), which posts impl.reset() (Task B) to the media thread queue and waits for it. The queue is now [Task A, Task B].
  4. Task A Executes & Enqueues Task C: The media thread eventually processes Task A. It instantiates a media::VideoDecoder and calls its Initialize() method, passing a lambda callback that binds CrossThreadUnretained(video_decoder_.get()). If initialization fails synchronously (e.g., bad config), decoders like MediaCodecVideoDecoder use base::BindPostTaskToCurrentDefault to post the completion callback asynchronously. This appends Task C to the queue, resulting in [Task B, Task C].
  5. Task B Executes: The media thread runs Task B (impl.reset()). This destroys the Impl object, which in turn frees the media::VideoDecoder. Task B signals Release() to complete, allowing the RTCVideoDecoderAdapter to be freed.
  6. Task C Executes (UAF): The media thread runs Task C, the completion lambda. The lambda executes video_decoder->GetDecoderType(), which is a UAF virtual method call on the freed VideoDecoder object.
  7. Further UAFs: The lambda continues to write to the dangling decoder_type_ pointer. Finally, FinishWait calls waiter->Signal() on the dangling async_init_waiter_ pointer. WaitableEvent::Signal() loops over an internal list of waiters and calls the pure virtual method Fire(), providing a secondary avenue for a UAF virtual method call.

Impact

This is a heap use-after-free vulnerability in the renderer process. An attacker could potentially exploit this race condition to achieve arbitrary Remote Code Execution (RCE) by reallocating the freed media::VideoDecoder or WaitableEvent memory with controlled data before Task C executes. Hijacking the virtual method calls (GetDecoderType() or Fire()) provides direct control over the instruction pointer.

Triggering the issue (Potential Steps)

Note: These are suggested steps; our tooling agent does not yet run code to provide a functional PoC.

  1. A malicious website initiates a WebRTC video stream.
  2. The attacker uses intensive JavaScript operations to intentionally spam and backlog the renderer’s media task runner.
  3. This forces the 10-second TimedWait in InitializeSync to expire while initialization tasks are still pending.
  4. The attacker provides an invalid video configuration to force the synchronous failure path in the underlying decoder, ensuring Task C is appended after Task B.
  5. The attacker uses standard JS heap manipulation techniques to reclaim the freed memory between the execution of Task B and Task C.

Suggested Fix

Remove the use of WTF::CrossThreadUnretained in RTCVideoDecoderAdapter::InitializeSync. Instead of capturing raw pointers to member variables, the completion callback should be bound to a base::WeakPtr of the adapter, or the RTCVideoDecoderAdapter should safely manage the lifetime of its initialization state such that pending callbacks are explicitly cancelled or ignored if the adapter is destroyed.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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