CVE-2026-15121
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc |
modified |
Files Changed
third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.ccthird_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.hthird_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc
Patch
From 18a41228c0a061c0a08d01c22e943e4862223f1e Mon Sep 17 00:00:00 2001 From: Erik Språng <[email protected]> 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 <[email protected]> Commit-Queue: Erik Språng <[email protected]> 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
Regression Test / PoC
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
Original Bug Report
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.ccthird_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:
- Task A Enqueued:
InitializeSyncpostsImpl::Initialize(Task A) to the media thread and blocks viaTimedWait(base::Seconds(10)). - Timeout: If the media thread is heavily backlogged, Task A does not execute within 10 seconds.
InitializeSyncreturnsfalse. - Destruction & Task B Enqueued: The caller (e.g.,
RTCVideoDecoderAdapter::Create) destroys the adapter. The destructor callsRelease(), which postsimpl.reset()(Task B) to the media thread queue and waits for it. The queue is now[Task A, Task B]. - Task A Executes & Enqueues Task C: The media thread eventually processes Task A. It instantiates a
media::VideoDecoderand calls itsInitialize()method, passing a lambda callback that bindsCrossThreadUnretained(video_decoder_.get()). If initialization fails synchronously (e.g., bad config), decoders likeMediaCodecVideoDecoderusebase::BindPostTaskToCurrentDefaultto post the completion callback asynchronously. This appends Task C to the queue, resulting in[Task B, Task C]. - Task B Executes: The media thread runs Task B (
impl.reset()). This destroys theImplobject, which in turn frees themedia::VideoDecoder. Task B signalsRelease()to complete, allowing theRTCVideoDecoderAdapterto be freed. - 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 freedVideoDecoderobject. - Further UAFs: The lambda continues to write to the dangling
decoder_type_pointer. Finally,FinishWaitcallswaiter->Signal()on the danglingasync_init_waiter_pointer.WaitableEvent::Signal()loops over an internal list of waiters and calls the pure virtual methodFire(), 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.
- A malicious website initiates a WebRTC video stream.
- The attacker uses intensive JavaScript operations to intentionally spam and backlog the renderer’s media task runner.
- This forces the 10-second
TimedWaitinInitializeSyncto expire while initialization tasks are still pending. - 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.
- 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.