CVE-2026-7347
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/protocol/BUILD.gn |
modified | |
ifremoting/protocol/webrtc_video_renderer_adapter.cc |
modified | |
WebrtcVideoRendererAdapterTestremoting/protocol/webrtc_video_renderer_adapter_unittest.cc |
modified | |
TEST_Fremoting/protocol/webrtc_video_renderer_adapter_unittest.cc |
modified |
Files Changed
remoting/protocol/BUILD.gnremoting/protocol/webrtc_video_renderer_adapter.ccremoting/protocol/webrtc_video_renderer_adapter.hremoting/protocol/webrtc_video_renderer_adapter_unittest.cc
Patch
From 2fb4251778d90fe19c011a207c009a39db2e9fa1 Mon Sep 17 00:00:00 2001 From: Joe Downing <[email protected]> Date: Mon, 13 Apr 2026 14:21:25 -0700 Subject: [PATCH] [remoting] Fix Use-after-Free in WebrtcVideoRendererAdapter WebrtcVideoRendererAdapter registered itself as a sink on the first track of a MediaStream but was re-querying the stream for its first track during destruction. If the tracks were changed via SDP renegotiation, it could unregister from the wrong track, leaving a dangling pointer in the original track. This CL fixes the issue by caching the track the adapter is registered on and using that cached track for unregistration in the destructor and in SetMediaStream. Bug: 501722605 Change-Id: I5adf5b2acdc1c60095892f549ac65aa9d74e7cdc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7757703 Reviewed-by: Yuwei Huang <[email protected]> Commit-Queue: Joe Downing <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613974} --- diff --git a/remoting/protocol/BUILD.gn b/remoting/protocol/BUILD.gn index 4daab165..1868e194 100644 --- a/remoting/protocol/BUILD.gn +++ b/remoting/protocol/BUILD.gn @@ -417,6 +417,7 @@ "ssl_hmac_channel_authenticator_unittest.cc", "validating_authenticator_unittest.cc", "webrtc_event_log_data_unittest.cc", + "webrtc_video_renderer_adapter_unittest.cc", ] if (enable_remoting_host) { diff --git a/remoting/protocol/webrtc_video_renderer_adapter.cc b/remoting/protocol/webrtc_video_renderer_adapter.cc index 768c8ba0..23ba1a4 100644 --- a/remoting/protocol/webrtc_video_renderer_adapter.cc +++ b/remoting/protocol/webrtc_video_renderer_adapter.cc @@ -71,31 +71,30 @@ // Needed for ConnectionTest unittests which set up a fake connection without // starting any video. This video adapter is instantiated when the incoming // video-stats data channel is created. - if (!media_stream_) { - return; + if (video_track_) { + video_track_->RemoveSink(this); } - - webrtc::VideoTrackVector video_tracks = media_stream_->GetVideoTracks(); - DCHECK(!video_tracks.empty()); - video_tracks[0]->RemoveSink(this); } void WebrtcVideoRendererAdapter::SetMediaStream( webrtc::scoped_refptr<webrtc::MediaStreamInterface> media_stream) { DCHECK_EQ(media_stream->id(), label()); - media_stream_ = std::move(media_stream); + if (video_track_) { + video_track_->RemoveSink(this); + } - webrtc::VideoTrackVector video_tracks = media_stream_->GetVideoTracks(); + webrtc::VideoTrackVector video_tracks = media_stream->GetVideoTracks(); // Caller must verify that the media stream contains video tracks. - DCHECK(!video_tracks.empty()); + CHECK(!video_tracks.empty()); if (video_tracks.size() > 1U) { LOG(WARNING) << "Received media stream with multiple video tracks."; } - video_tracks[0]->AddOrUpdateSink(this, webrtc::VideoSinkWants()); + video_track_ = video_tracks[0]; + video_track_->AddOrUpdateSink(this, webrtc::VideoSinkWants()); } void WebrtcVideoRendererAdapter::SetVideoStatsChannel( diff --git a/remoting/protocol/webrtc_video_renderer_adapter.h b/remoting/protocol/webrtc_video_renderer_adapter.h index a659c4c..f90f09a 100644 --- a/remoting/protocol/webrtc_video_renderer_adapter.h +++ b/remoting/protocol/webrtc_video_renderer_adapter.h @@ -77,7 +77,7 @@ std::string label_; - webrtc::scoped_refptr<webrtc::MediaStreamInterface> media_stream_; + webrtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_; raw_ptr<VideoRenderer> video_renderer_; std::unique_ptr<ClientVideoStatsDispatcher> video_stats_dispatcher_; diff --git a/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc b/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc new file mode 100644 index 0000000..76427f8a --- /dev/null +++ b/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc @@ -0,0 +1,109 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/protocol/webrtc_video_renderer_adapter.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/memory/scoped_refptr.h" +#include "base/test/task_environment.h" +#include "remoting/protocol/fake_video_renderer.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/api/make_ref_counted.h" +#include "third_party/webrtc/api/media_stream_interface.h" +#include "third_party/webrtc/api/test/mock_media_stream_interface.h" +#include "third_party/webrtc/api/test/mock_video_track.h" + +using testing::_; +using testing::Return; + +namespace remoting::protocol { + +class WebrtcVideoRendererAdapterTest : public testing::Test { + public: + WebrtcVideoRendererAdapterTest() + : task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {} + + protected: + base::test::TaskEnvironment task_environment_; + FakeVideoRenderer video_renderer_; +}; + +TEST_F(WebrtcVideoRendererAdapterTest, DanglingSinkAfterTrackSwap) { + std::string label = "test_stream"; + auto adapter = + std::make_unique<WebrtcVideoRendererAdapter>(label, &video_renderer_); + + auto track0 = webrtc::MockVideoTrack::Create(); + auto track1 = webrtc::MockVideoTrack::Create(); + auto stream = webrtc::make_ref_counted<webrtc::MockMediaStream>(); + + EXPECT_CALL(*stream, id()).WillRepeatedly(Return(label)); + + // Initial tracks: [track0] + EXPECT_CALL(*stream, GetVideoTracks()) + .WillRepeatedly(Return( + std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{ + track0})); + + // Expect registration on track0 + EXPECT_CALL(*track0, AddOrUpdateSink(adapter.get(), _)).Times(1); + + adapter->SetMediaStream(stream); + + // Swap tracks: [track1] + EXPECT_CALL(*stream, GetVideoTracks()) + .WillRepeatedly(Return( + std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{ + track1})); + + // On destruction, the code should unregister from the ORIGINAL track + // (track0). + EXPECT_CALL(*track1, RemoveSink(adapter.get())).Times(0); + EXPECT_CALL(*track0, RemoveSink(adapter.get())).Times(1); + + adapter.reset(); +} + +TEST_F(WebrtcVideoRendererAdapterTest, DanglingSinkAfterSetMediaStreamReplace) { + std::string label = "test_stream"; + auto adapter = + std::make_unique<WebrtcVideoRendererAdapter>(label, &video_renderer_); + + auto track0 = webrtc::MockVideoTrack::Create(); + auto stream0 = webrtc::make_ref_counted<webrtc::MockMediaStream>(); + EXPECT_CALL(*stream0, id()).WillRepeatedly(Return(label)); + EXPECT_CALL(*stream0, GetVideoTracks()) + .WillRepeatedly(Return( + std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{ + track0})); + + auto track1 = webrtc::MockVideoTrack::Create(); + auto stream1 = webrtc::make_ref_counted<webrtc::MockMediaStream>(); + EXPECT_CALL(*stream1, id()).WillRepeatedly(Return(label)); + EXPECT_CALL(*stream1, GetVideoTracks()) + .WillRepeatedly(Return( + std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{ + track1})); + + // First SetMediaStream + EXPECT_CALL(*track0, AddOrUpdateSink(adapter.get(), _)).Times(1); + adapter->SetMediaStream(stream0); + + // Second SetMediaStream. The code should unregister from track0 before + // registering on track1. + EXPECT_CALL(*track0, RemoveSink(adapter.get())).Times(1); + EXPECT_CALL(*track1, AddOrUpdateSink(adapter.get(), _)).Times(1);
Regression Test / PoC
diff --git a/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc b/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc
new file mode 100644
index 0000000..76427f8a
--- /dev/null
+++ b/remoting/protocol/webrtc_video_renderer_adapter_unittest.cc
@@ -0,0 +1,109 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/protocol/webrtc_video_renderer_adapter.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/memory/scoped_refptr.h"
+#include "base/test/task_environment.h"
+#include "remoting/protocol/fake_video_renderer.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/webrtc/api/make_ref_counted.h"
+#include "third_party/webrtc/api/media_stream_interface.h"
+#include "third_party/webrtc/api/test/mock_media_stream_interface.h"
+#include "third_party/webrtc/api/test/mock_video_track.h"
+
+using testing::_;
+using testing::Return;
+
+namespace remoting::protocol {
+
+class WebrtcVideoRendererAdapterTest : public testing::Test {
+ public:
+ WebrtcVideoRendererAdapterTest()
+ : task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {}
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ FakeVideoRenderer video_renderer_;
+};
+
+TEST_F(WebrtcVideoRendererAdapterTest, DanglingSinkAfterTrackSwap) {
+ std::string label = "test_stream";
+ auto adapter =
+ std::make_unique<WebrtcVideoRendererAdapter>(label, &video_renderer_);
+
+ auto track0 = webrtc::MockVideoTrack::Create();
+ auto track1 = webrtc::MockVideoTrack::Create();
+ auto stream = webrtc::make_ref_counted<webrtc::MockMediaStream>();
+
+ EXPECT_CALL(*stream, id()).WillRepeatedly(Return(label));
+
+ // Initial tracks: [track0]
+ EXPECT_CALL(*stream, GetVideoTracks())
+ .WillRepeatedly(Return(
+ std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{
+ track0}));
+
+ // Expect registration on track0
+ EXPECT_CALL(*track0, AddOrUpdateSink(adapter.get(), _)).Times(1);
+
+ adapter->SetMediaStream(stream);
+
+ // Swap tracks: [track1]
+ EXPECT_CALL(*stream, GetVideoTracks())
+ .WillRepeatedly(Return(
+ std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{
+ track1}));
+
+ // On destruction, the code should unregister from the ORIGINAL track
+ // (track0).
+ EXPECT_CALL(*track1, RemoveSink(adapter.get())).Times(0);
+ EXPECT_CALL(*track0, RemoveSink(adapter.get())).Times(1);
+
+ adapter.reset();
+}
+
+TEST_F(WebrtcVideoRendererAdapterTest, DanglingSinkAfterSetMediaStreamReplace) {
+ std::string label = "test_stream";
+ auto adapter =
+ std::make_unique<WebrtcVideoRendererAdapter>(label, &video_renderer_);
+
+ auto track0 = webrtc::MockVideoTrack::Create();
+ auto stream0 = webrtc::make_ref_counted<webrtc::MockMediaStream>();
+ EXPECT_CALL(*stream0, id()).WillRepeatedly(Return(label));
+ EXPECT_CALL(*stream0, GetVideoTracks())
+ .WillRepeatedly(Return(
+ std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{
+ track0}));
+
+ auto track1 = webrtc::MockVideoTrack::Create();
+ auto stream1 = webrtc::make_ref_counted<webrtc::MockMediaStream>();
+ EXPECT_CALL(*stream1, id()).WillRepeatedly(Return(label));
+ EXPECT_CALL(*stream1, GetVideoTracks())
+ .WillRepeatedly(Return(
+ std::vector<webrtc::scoped_refptr<webrtc::VideoTrackInterface>>{
+ track1}));
+
+ // First SetMediaStream
+ EXPECT_CALL(*track0, AddOrUpdateSink(adapter.get(), _)).Times(1);
+ adapter->SetMediaStream(stream0);
+
+ // Second SetMediaStream. The code should unregister from track0 before
+ // registering on track1.
+ EXPECT_CALL(*track0, RemoveSink(adapter.get())).Times(1);
+ EXPECT_CALL(*track1, AddOrUpdateSink(adapter.get(), _)).Times(1);
+ adapter->SetMediaStream(stream1);
+
+ // On destruction, it will unregister from track1.
+ EXPECT_CALL(*track1, RemoveSink(adapter.get())).Times(1);
+ adapter.reset();
+}
+
+} // namespace remoting::protocol
Original Bug Report
Potential Use-After-Free in WebrtcVideoRendererAdapter via mutable MediaStream tracks
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.
Overview: A potential Use-After-Free (UAF) exists in the ChromeOS Boca remoting client. The WebrtcVideoRendererAdapter registers as a video sink on its stream’s first track but re-queries the stream’s track list during destruction, failing to unregister correctly if the tracks were changed via SDP renegotiation. This leaves an unprotected dangling pointer in the original WebRTC video track, which can be triggered by incoming video frames.
Affected files:
remoting/protocol/webrtc_video_renderer_adapter.ccremoting/protocol/webrtc_connection_to_host.ccremoting/protocol/webrtc_video_renderer_adapter.h
Estimated timestamp from git blame: 2025-04-14
Summary
A potential Use-After-Free (UAF) vulnerability exists in remoting::protocol::WebrtcVideoRendererAdapter. The class registers itself as a video sink on the first track of a WebRTC MediaStream. However, because MediaStream objects are mutable in-place via SDP renegotiation, the tracks can change before the adapter is destroyed. During destruction, the adapter re-queries the stream for its first track, meaning it may unregister from the new track rather than the original track it subscribed to. This leaves a dangling pointer in the original track, which results in a virtual method call on freed memory when a new video frame is received.
Technical Details
When a stream is added, WebrtcConnectionToHost creates a WebrtcVideoRendererAdapter and calls its SetMediaStream method. This method registers the adapter as a sink on the first video track of the stream:
void WebrtcVideoRendererAdapter::SetMediaStream(
webrtc::scoped_refptr<webrtc::MediaStreamInterface> media_stream) {
media_stream_ = std::move(media_stream);
webrtc::VideoTrackVector video_tracks = media_stream_->GetVideoTracks();
// ...
video_tracks[0]->AddOrUpdateSink(this, webrtc::VideoSinkWants());
}
The destructor later attempts to unregister the sink by again querying the first track of media_stream_:
WebrtcVideoRendererAdapter::~WebrtcVideoRendererAdapter() {
// ...
webrtc::VideoTrackVector video_tracks = media_stream_->GetVideoTracks();
DCHECK(!video_tracks.empty());
video_tracks[0]->RemoveSink(this);
}
If a remote host sends a new SDP offer that changes the tracks associated with the MediaStream (e.g., using the a=msid attribute to replace track T0 with track T1), WebRTC mutates the MediaStream object in-place. OnAddStream is not fired again since the stream ID remains the same, so WebrtcVideoRendererAdapter is never notified. When the adapter is eventually destroyed, media_stream_->GetVideoTracks()[0] returns T1. The destructor calls RemoveSink on T1, leaving the adapter still registered as a sink on T0.
The VideoBroadcaster in third_party/webrtc/media/base/video_source_base.h stores these sinks inside a SinkPair struct using a standard C++ raw pointer (VideoSinkInterface<VideoFrame>* sink;). Because it is not a base::raw_ptr, PartitionAlloc’s BackupRefPtr (MiraclePtr) mitigation does not protect it. When T0 receives a frame, it iterates its sinks and invokes sink->OnFrame(frame), triggering a virtual call on freed memory.
Potential Exploitation Steps
Note: These are suggested steps for an attacker, as our tooling agent does not currently have the ability to run dynamic code to provide a working proof-of-concept.
- Establish a WebRTC connection to the victim (e.g., via the ChromeOS Boca Spotlight feature).
- Send an SDP offer defining a
MediaStream(StreamS) with a single video track (T0). The victim creates aWebrtcVideoRendererAdapterand registers it onT0. - Send an SDP renegotiation offer that modifies Stream
Sso it no longer containsT0at index 0 (e.g., replace it with a new trackT1). WebRTC mutates StreamSin-place. - Trigger the destruction of the adapter. This can be done by opening an SCTP data channel with a new video label (e.g.,
video_stats:S2). This causesWebrtcConnectionToHost::GetOrCreateVideoAdapterto overwrite the existingunique_ptr<WebrtcVideoRendererAdapter>, immediately destroying the old adapter. - The old adapter’s destructor runs, queries Stream
S, finds trackT1, and callsRemoveSinkonT1. The original trackT0still holds a dangling pointer to the freed adapter. - Send RTP video frames for track
T0. The client decodes the frame and calls the virtualOnFramemethod on the dangling pointer, granting control over the execution flow.
Impact
In ChromeOS, the Remoting client used by the Boca Spotlight feature runs on a dedicated IO thread within the browser process (Boca Spotlight IO). Exploitation of this UAF provides a primitive for Remote Code Execution (RCE) directly within the unsandboxed browser process.
Suggested Fix
Instead of dynamically querying the MediaStream for its tracks during destruction, the WebrtcVideoRendererAdapter should explicitly store a scoped_refptr<webrtc::VideoTrackInterface> (or safely handle tracking) to the exact track it successfully registered with in SetMediaStream. The destructor should then call RemoveSink on that specific track instance.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.