Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Bug ClassUAF
Tracker501722605
Fix commit2fb4251778d9 (chromium/src) +120/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
if
remoting/protocol/BUILD.gn
modified
if
remoting/protocol/webrtc_video_renderer_adapter.cc
modified
WebrtcVideoRendererAdapterTest
remoting/protocol/webrtc_video_renderer_adapter_unittest.cc
modified
TEST_F
remoting/protocol/webrtc_video_renderer_adapter_unittest.cc
modified

Files Changed

  • remoting/protocol/BUILD.gn
  • remoting/protocol/webrtc_video_renderer_adapter.cc
  • remoting/protocol/webrtc_video_renderer_adapter.h
  • remoting/protocol/webrtc_video_renderer_adapter_unittest.cc
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • remoting/protocol/webrtc_connection_to_host.cc
  • remoting/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.

  1. Establish a WebRTC connection to the victim (e.g., via the ChromeOS Boca Spotlight feature).
  2. Send an SDP offer defining a MediaStream (Stream S) with a single video track (T0). The victim creates a WebrtcVideoRendererAdapter and registers it on T0.
  3. Send an SDP renegotiation offer that modifies Stream S so it no longer contains T0 at index 0 (e.g., replace it with a new track T1). WebRTC mutates Stream S in-place.
  4. 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 causes WebrtcConnectionToHost::GetOrCreateVideoAdapter to overwrite the existing unique_ptr<WebrtcVideoRendererAdapter>, immediately destroying the old adapter.
  5. The old adapter’s destructor runs, queries Stream S, finds track T1, and calls RemoveSink on T1. The original track T0 still holds a dangling pointer to the freed adapter.
  6. Send RTP video frames for track T0. The client decodes the frame and calls the virtual OnFrame method 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.

View on issue tracker