CVE-2026-7341
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
VideoMetronomeWorkerthird_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc |
modified | |
ifthird_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc |
modified |
Files Changed
third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc
Patch
From c56abf1fe8e4cb61159d9eebcc6dac6bcd9db886 Mon Sep 17 00:00:00 2001 From: Tony Herre <[email protected]> Date: Tue, 21 Apr 2026 05:45:58 -0700 Subject: [PATCH] Fix cross-thread UAF in RTCEncodedVideoStreamTransformerDelegate The RTCEncodedVideoStreamTransformerDelegate was using base::WeakPtr across multiple threads (worker, signaling, and main), which is not thread-safe and led to use-after-free vulnerabilities. This CL refactors the delegate to use a separate ref-counted worker class, VideoMetronomeWorker. This worker handles metronome tasks using webrtc::scoped_refptr to keep itself alive during cross-thread callbacks, eliminating the need for WeakPtr. The design also avoids circular references by decoupling the metronome state from the delegate's lifecycle and ensures thread safety by relying on sequence affinity for worker state. A new unit test WorkerOutlivesDelegate verifies the fix. Bug: 504586599 Change-Id: I935074a5c4f3efa772a764079ffc337ab9f30128 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7779865 Reviewed-by: Guido Urdaneta <[email protected]> Commit-Queue: Tony Herre <[email protected]> Cr-Commit-Position: refs/heads/main@{#1618115} --- diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc b/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc index a0d07f2..0b5d4735 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc +++ b/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc @@ -19,6 +19,7 @@ #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_base.h" #include "third_party/blink/renderer/platform/wtf/cross_thread_copier_std.h" #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" +#include "third_party/blink/renderer/platform/wtf/thread_safe_ref_counted.h" #include "third_party/webrtc/api/frame_transformer_interface.h" #include "third_party/webrtc/rtc_base/ref_counted_object.h" @@ -36,6 +37,126 @@ // shortcircuiting/setting transforms. const size_t kMaxBufferedFrames = 60; +// This class handles the metronome-related tasks for the transformer delegate. +// It is ref-counted and decoupled from the delegate's lifecycle to avoid +// circular references and ensure cross-thread safety. +class VideoMetronomeWorker : public ThreadSafeRefCounted<VideoMetronomeWorker> { + public: + VideoMetronomeWorker( + std::unique_ptr<Metronome> metronome, + scoped_refptr<RTCEncodedVideoStreamTransformer::Broker> + transformer_broker, + scoped_refptr<base::SingleThreadTaskRunner> source_task_runner) + : source_task_runner_(source_task_runner), + transformer_broker_(std::move(transformer_broker)), + use_metronome_(!!metronome), + metronome_(std::move(metronome)) {} + + void SetSourceTaskRunner( + scoped_refptr<base::SingleThreadTaskRunner> task_runner) { + base::AutoLock locker(source_task_runner_lock_); + source_task_runner_ = std::move(task_runner); + } + + void RegisterTransformedFrameSinkCallback( + webrtc::scoped_refptr<webrtc::TransformedFrameCallback> + send_frame_to_sink_callback, + uint32_t ssrc) { + transformer_broker_->RegisterTransformedFrameSinkCallback( + std::move(send_frame_to_sink_callback), ssrc); + } + + void UnregisterTransformedFrameSinkCallback(uint32_t ssrc) { + transformer_broker_->UnregisterTransformedFrameSinkCallback(ssrc); + } + + void Transform( + std::unique_ptr<webrtc::TransformableVideoFrameInterface> frame) { + if (use_metronome_) { + bool should_schedule_tick = false; + { + base::AutoLock locker(metronome_lock_); + queued_frames_.emplace_back(std::move(frame)); + if (!tick_scheduled_) { + tick_scheduled_ = true; + should_schedule_tick = true; + } + } + + if (should_schedule_tick) { + // Using a lambda here instead of a OnceClosure as + // RequestCallOnNextTick() requires an absl::AnyInvocable. + metronome_->RequestCallOnNextTick( + [worker = scoped_refptr<VideoMetronomeWorker>(this)] { + worker->InvokeQueuedTransforms(); + }); + } + return; + } + + scoped_refptr<base::SingleThreadTaskRunner> task_runner; + { + base::AutoLock locker(source_task_runner_lock_); + task_runner = source_task_runner_; + } + if (task_runner) { + PostCrossThreadTask( + *task_runner, FROM_HERE, + CrossThreadBindOnce(&RTCEncodedVideoStreamTransformer::Broker:: + TransformFrameOnSourceTaskRunner, + transformer_broker_, std::move(frame))); + } + } + + void InvokeQueuedTransforms() { + Vector<std::unique_ptr<webrtc::TransformableVideoFrameInterface>> frames; + { + base::AutoLock locker(metronome_lock_); + tick_scheduled_ = false; + frames = std::move(queued_frames_); + } + + scoped_refptr<base::SingleThreadTaskRunner> task_runner; + { + base::AutoLock locker(source_task_runner_lock_); + task_runner = source_task_runner_; + } + if (!task_runner) { + return; + } + for (std::unique_ptr<webrtc::TransformableVideoFrameInterface>& frame : + frames) { + PostCrossThreadTask( + *task_runner, FROM_HERE, + CrossThreadBindOnce(&RTCEncodedVideoStreamTransformer::Broker:: + TransformFrameOnSourceTaskRunner, + transformer_broker_, std::move(frame))); + } + } + + void Disconnect() { + base::AutoLock locker(source_task_runner_lock_); + source_task_runner_.reset(); + } + + private: + friend class ThreadSafeRefCounted<VideoMetronomeWorker>; + ~VideoMetronomeWorker() = default; + + base::Lock source_task_runner_lock_; + scoped_refptr<base::SingleThreadTaskRunner> source_task_runner_ + GUARDED_BY(source_task_runner_lock_); + + scoped_refptr<RTCEncodedVideoStreamTransformer::Broker> transformer_broker_; + const bool use_metronome_; + const std::unique_ptr<Metronome> metronome_; + + base::Lock metronome_lock_; + bool tick_scheduled_ GUARDED_BY(metronome_lock_) = false; + Vector<std::unique_ptr<webrtc::TransformableVideoFrameInterface>> + queued_frames_ GUARDED_BY(metronome_lock_); +}; + // This delegate class exists to work around the fact that // RTCEncodedVideoStreamTransformer cannot derive from webrtc::RefCountedObject // and post tasks referencing itself as an webrtc::scoped_refptr. Instead, @@ -50,17 +171,18 @@ scoped_refptr<RTCEncodedVideoStreamTransformer::Broker> transformer_broker, std::unique_ptr<Metronome> metronome) - : source_task_runner_(realm_task_runner), - transformer_broker_(std::move(transformer_broker)), - metronome_(std::move(metronome)) { - DCHECK(source_task_runner_->BelongsToCurrentThread()); - DETACH_FROM_SEQUENCE(metronome_sequence_checker_); + : metronome_worker_(base::MakeRefCounted<VideoMetronomeWorker>( + std::move(metronome), + std::move(transformer_broker), + realm_task_runner)) {} + + ~RTCEncodedVideoStreamTransformerDelegate() override { + metronome_worker_->Disconnect(); } void SetSourceTaskRunner( scoped_refptr<base::SingleThreadTaskRunner> task_runner) { - base::AutoLock locker(source_task_runner_lock_); - source_task_runner_ = std::move(task_runner); + metronome_worker_->SetSourceTaskRunner(std::move(task_runner)); } // webrtc::FrameTransformerInterface @@ -68,72 +190,23 @@ webrtc::scoped_refptr<webrtc::TransformedFrameCallback> send_frame_to_sink_callback, uint32_t ssrc) override { - transformer_broker_->RegisterTransformedFrameSinkCallback( + metronome_worker_->RegisterTransformedFrameSinkCallback( std::move(send_frame_to_sink_callback), ssrc);
Regression Test / PoC
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer_test.cc
index 8460eb6..6aa2827 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer_test.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer_test.cc
@@ -287,4 +287,41 @@
CrossThreadUnretained(&mock_transformer_callback_holder_)));
}
+TEST_P(RTCEncodedVideoStreamTransformerTest, WorkerOutlivesDelegate) {
+ if (!GetParam()) {
+ return;
+ }
+
+ MockTransformerCallbackHolder lifecycle_callback_holder;
+ scoped_refptr<base::SingleThreadTaskRunner> main_runner =
+ blink::scheduler::GetSingleThreadTaskRunnerForTesting();
+ auto* mock_metronome = new NiceMock<MockMetronome>();
+ // Using AnyInvocable as that's what the libwebrtc Metronome
+ // interface requires.
+ absl::AnyInvocable<void() &&> metronome_callback;
+ EXPECT_CALL(*mock_metronome, RequestCallOnNextTick)
+ .WillOnce([&](absl::AnyInvocable<void() &&> c) {
+ metronome_callback = std::move(c);
+ });
+
+ auto transformer = std::make_unique<RTCEncodedVideoStreamTransformer>(
+ main_runner, absl::WrapUnique(mock_metronome));
+ transformer->SetTransformerCallback(CrossThreadBindRepeating(
+ &MockTransformerCallbackHolder::OnEncodedFrame,
+ CrossThreadUnretained(&lifecycle_callback_holder)));
+
+ // Send a frame to schedule a tick.
+ transformer->Delegate()->Transform(CreateMockFrame());
+ ASSERT_TRUE(metronome_callback);
+
+ // Destroy the transformer. This should call Disconnect() on the delegate's
+ // worker.
+ transformer.reset();
+
+ // Now fire the metronome tick. It should NOT crash and should NOT call
+ // the callback (since the transformer is gone).
+ EXPECT_CALL(lifecycle_callback_holder, OnEncodedFrame).Times(0);
+ std::move(metronome_callback)();
+}
+
} // namespace blink
Original Bug Report
Renderer UAF in RTCEncodedVideoStreamTransformerDelegate via cross-thread WeakPtr usage
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 potential use-after-free vulnerability exists in RTCEncodedVideoStreamTransformerDelegate due to a Time-of-Check Time-of-Use (TOCTOU) race condition when resolving a base::WeakPtr across different threads. In release builds, bypassed sequence checks allow the object to be destroyed while a callback is simultaneously executing on a different thread. An attacker could potentially reclaim the freed memory to achieve arbitrary code execution via a hijacked virtual function call.
Affected files:
third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc
Estimated timestamp from git blame: 2024-01-09
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in RTCEncodedVideoStreamTransformerDelegate within the Blink renderer. The issue arises from a base::WeakPtr being captured in a callback that executes on the WebRTC worker thread, while the delegate object itself is destroyed on a different thread (typically the WebRTC signaling thread). Because base::WeakPtr sequence affinity checks are disabled in release builds, a Time-of-Check Time-of-Use (TOCTOU) race condition allows the worker thread to successfully resolve the WeakPtr and execute methods on the delegate after its memory has been freed.
Root Cause Analysis
When an encoded video stream is transformed, RTCEncodedVideoStreamTransformerDelegate::Transform() executes on the WebRTC worker thread. If a metronome is active, it schedules a callback for the next tick, capturing a base::WeakPtr to itself:
// third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.cc
metronome_->RequestCallOnNextTick(
[delegate = weak_factory_.GetWeakPtr()] {
if (delegate) {
delegate->InvokeQueuedTransforms();
}
});
The delegate is a thread-safe ref-counted object (webrtc::RefCountedObject) owned jointly by a Blink-managed RTCEncodedVideoStreamTransformer (destroyed on the main thread) and a native WebRTC receiver proxy. During teardown, the final reference is typically released when the native WebRTC receiver proxy completes its destruction on the WebRTC signaling thread.
The vulnerability occurs due to a race condition between the metronome tick executing on the worker thread and the delegate destructor executing on the signaling thread:
- On the worker thread, the lambda evaluates
if (delegate). This reads the atomic validity flag insideinternal::WeakReference::Flag::IsValid(). - Concurrently, the signaling thread begins destroying the delegate. It reaches
base::WeakPtrFactory::~WeakPtrFactory(), which callsInvalidate()to mark the atomic flag as invalid. - In release builds (
DCHECK_IS_ON()is false),Invalidate()skips theDCHECK_CALLED_ON_VALID_SEQUENCEcheck. The cross-thread invalidation succeeds without crashing the process. - If the worker thread reads the atomic flag just before the signaling thread marks it invalid,
if (delegate)passes. The subsequentdelegate->check also passes. - The delegate is fully destroyed on the signaling thread, and the memory is returned to the allocator.
- The worker thread proceeds to call
InvokeQueuedTransforms()on the now-danglingthispointer.
Impact
This vulnerability bypasses MiraclePtr (BackupRefPtr) protections because base::WeakPtr explicitly uses RAW_PTR_EXCLUSION T* ptr_ for its internal pointer to avoid keeping allocations in quarantine.
If an attacker can precisely time the teardown and perform heap spraying to reclaim the freed memory chunk, they can control the execution context of InvokeQueuedTransforms():
- The function begins with
base::AutoLock locker(source_task_runner_lock_);. By filling the reclaimed memory with zeros, the underlying platform mutex will appear unlocked, allowing the lock acquisition to succeed without crashing. - The function iterates over
queued_frames_(aWTF::Vector). An attacker can manipulate the vector’s metadata in the reclaimed memory to point to arbitrary locations. - Inside the loop, it calls
PostCrossThreadTask(*source_task_runner_, ...). If the attacker controls thesource_task_runner_pointer, this leads to a virtual function call (PostTask) on attacker-controlled memory.
This vtable hijack can be leveraged to achieve Arbitrary Code Execution (RCE) within the sandboxed Renderer process.
Potential Reproduction Steps
Note: These are suggested steps to trigger the vulnerability; a working proof-of-concept has not been verified via tooling yet.
- In JavaScript, negotiate a WebRTC connection with a video stream.
- Attach an
RTCRtpScriptTransformto the receiver to force the creation of theRTCEncodedVideoStreamTransformerDelegate. - Send a burst of encoded video frames to ensure the
Transformmethod schedules the metronome callback on the worker thread. - Just before the metronome tick is expected to fire, aggressively tear down the connection (e.g.,
pc.close()and drop references) to trigger destruction of the delegate on the signaling thread. - Concurrently, perform heap spraying (e.g., via a Web Worker) with objects of the exact size of the delegate, filling the payload with zeros for the lock and attacker-controlled addresses for the
source_task_runner_pointer. - If the race is won, the worker thread will execute
InvokeQueuedTransforms()on the attacker’s payload.
Suggested Fix
To prevent this vulnerability, RTCEncodedVideoStreamTransformerDelegate must guarantee that any scheduled callbacks are either safely canceled or executed on valid memory.
One approach is to avoid capturing a WeakPtr bound to a different sequence than where destruction occurs. Instead, the class could implement a tear-down method that synchronously cancels the metronome task or invalidates the WeakPtrFactory on the worker thread before allowing the final reference count to drop and the destructor to run. Alternatively, a separate ref-counted state object could be created exclusively for the metronome callback to hold the queued frames, decoupling the callback’s lifecycle from the delegate’s lifecycle.
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.