Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Bug ClassUAF
Tracker513789382
Fix commit5fbcbfa9f94f (chromium/src) +62/-30
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • remoting/host/basic_desktop_environment.cc
  • remoting/host/basic_desktop_environment.h
  • remoting/host/delegating_desktop_display_info_monitor.cc
  • remoting/host/delegating_desktop_display_info_monitor.h
  • remoting/host/delegating_desktop_display_info_monitor_unittest.cc
  • remoting/host/desktop_display_info_monitor.h
  • remoting/host/linux/gnome_desktop_display_info_monitor.cc
  • remoting/host/linux/gnome_desktop_display_info_monitor.h
  • remoting/host/linux/pipewire_mouse_cursor_capturer.cc
From 5fbcbfa9f94fcc0912a7e9d0abeec3bbae84c21a Mon Sep 17 00:00:00 2001
From: Yuwei Huang <yuweih@chromium.org>
Date: Wed, 20 May 2026 18:15:54 -0700
Subject: [PATCH] Fix lifetime issue in DesktopDisplayInfoMonitor observers

DesktopDisplayInfoMonitor::AddCallback now returns a
CallbackListSubscription. This allows observers to unregister their
callbacks and prevents potential use-after-free issues when observers
are destroyed before the monitor.

TAG=agy
CONV=abed1b42-36a6-4cc1-92e4-41fe6b47ff73

Bug: 513789382
Change-Id: I19a0949af15534c1f2d9d340517ff98171fde167
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7865733
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
Reviewed-by: Joe Downing <joedow@chromium.org>
Auto-Submit: Yuwei Huang <yuweih@chromium.org>
Commit-Queue: Joe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633957}
---

diff --git a/remoting/host/basic_desktop_environment.cc b/remoting/host/basic_desktop_environment.cc
index c952afd..a4bcc28 100644
--- a/remoting/host/basic_desktop_environment.cc
+++ b/remoting/host/basic_desktop_environment.cc
@@ -108,7 +108,8 @@
           video_layout_callback.Run(info->GetVideoLayoutProto());
         },
         display_info_monitor_.get(), std::move(video_layout_callback));
-    display_info_monitor_->AddCallback(std::move(callback));
+    display_info_subscription_ =
+        display_info_monitor_->AddCallback(std::move(callback));
   }
   return display_info_monitor_.get();
 }
diff --git a/remoting/host/basic_desktop_environment.h b/remoting/host/basic_desktop_environment.h
index 08696af3..73e668d 100644
--- a/remoting/host/basic_desktop_environment.h
+++ b/remoting/host/basic_desktop_environment.h
@@ -9,6 +9,7 @@
 #include <memory>
 #include <string>
 
+#include "base/callback_list.h"
 #include "base/functional/callback_forward.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
@@ -111,6 +112,7 @@
   base::WeakPtr<ClientSessionControl> client_session_control_;
 
   std::unique_ptr<DesktopDisplayInfoMonitor> display_info_monitor_;
+  base::CallbackListSubscription display_info_subscription_;
 
   DesktopEnvironmentOptions options_;
 };
diff --git a/remoting/host/delegating_desktop_display_info_monitor.cc b/remoting/host/delegating_desktop_display_info_monitor.cc
index 814b3b4..66934a33 100644
--- a/remoting/host/delegating_desktop_display_info_monitor.cc
+++ b/remoting/host/delegating_desktop_display_info_monitor.cc
@@ -29,7 +29,7 @@
   }
 
   started_ = true;
-  underlying_->AddCallback(base::BindRepeating(
+  underlying_subscription_ = underlying_->AddCallback(base::BindRepeating(
       &DelegatingDesktopDisplayInfoMonitor::OnUnderlyingDisplayInfoChanged,
       base::Unretained(this)));
   if (underlying_->IsStarted()) {
@@ -60,10 +60,10 @@
   return underlying_->GetLatestDisplayInfo();
 }
 
-void DelegatingDesktopDisplayInfoMonitor::AddCallback(
+base::CallbackListSubscription DelegatingDesktopDisplayInfoMonitor::AddCallback(
     base::RepeatingClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  callbacks_.AddUnsafe(std::move(callback));
+  return callbacks_.Add(std::move(callback));
 }
 
 void DelegatingDesktopDisplayInfoMonitor::OnUnderlyingDisplayInfoChanged() {
diff --git a/remoting/host/delegating_desktop_display_info_monitor.h b/remoting/host/delegating_desktop_display_info_monitor.h
index 7421295..f131b035 100644
--- a/remoting/host/delegating_desktop_display_info_monitor.h
+++ b/remoting/host/delegating_desktop_display_info_monitor.h
@@ -30,7 +30,8 @@
   void Start() override;
   bool IsStarted() const override;
   const DesktopDisplayInfo* GetLatestDisplayInfo() const override;
-  void AddCallback(base::RepeatingClosure callback) override;
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override;
 
  private:
   void OnUnderlyingDisplayInfoChanged();
@@ -41,6 +42,8 @@
       GUARDED_BY_CONTEXT(sequence_checker_);
   bool started_ GUARDED_BY_CONTEXT(sequence_checker_) = false;
   base::RepeatingClosureList callbacks_ GUARDED_BY_CONTEXT(sequence_checker_);
+  base::CallbackListSubscription underlying_subscription_
+      GUARDED_BY_CONTEXT(sequence_checker_);
   base::WeakPtrFactory<DelegatingDesktopDisplayInfoMonitor> weak_ptr_factory_{
       this};
 };
diff --git a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
index 7f4bb682..8e13a5e8 100644
--- a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
+++ b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
@@ -35,8 +35,9 @@
     return info ? &info.value() : nullptr;
   }
 
-  void AddCallback(base::RepeatingClosure callback) override {
-    callbacks.AddUnsafe(std::move(callback));
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    return callbacks.Add(std::move(callback));
   }
 
   void NotifyChange() { callbacks.Notify(); }
@@ -74,10 +75,11 @@
   FakeDesktopDisplayInfoMonitor underlying_monitor_;
   DelegatingDesktopDisplayInfoMonitor monitor_{
       underlying_monitor_.GetWeakPtr()};
+  base::CallbackListSubscription subscription_;
 };
 
 void DelegatingDesktopDisplayInfoMonitorTest::AddCallback() {
-  monitor_.AddCallback(base::BindRepeating(
+  subscription_ = monitor_.AddCallback(base::BindRepeating(
       &DelegatingDesktopDisplayInfoMonitorTest::OnDisplayInfoChanged,
       base::Unretained(this)));
 }
diff --git a/remoting/host/desktop_display_info_monitor.h b/remoting/host/desktop_display_info_monitor.h
index d29f2bfe..485adaa 100644
--- a/remoting/host/desktop_display_info_monitor.h
+++ b/remoting/host/desktop_display_info_monitor.h
@@ -5,6 +5,7 @@
 #ifndef REMOTING_HOST_DESKTOP_DISPLAY_INFO_MONITOR_H_
 #define REMOTING_HOST_DESKTOP_DISPLAY_INFO_MONITOR_H_
 
+#include "base/callback_list.h"
 #include "base/functional/callback.h"
 #include "remoting/host/desktop_display_info.h"
 
@@ -30,10 +31,10 @@
 
   // Adds a callback to be notified of display-info changes or the first
   // available display info after Start() is called. Callbacks added after
-  // calling Start() will NOT be called until it changes. Implementations do not
-  // return a base::CallbackListSubscription, so |callback| must either outlive
-  // this object, or be bound to a suitable WeakPtr.
-  virtual void AddCallback(base::RepeatingClosure callback) = 0;
+  // calling Start() will NOT be called until it changes. The returned
+  // subscription must be kept alive to keep the callback registered.
+  virtual base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) = 0;
 };
 
 }  // namespace remoting
diff --git a/remoting/host/linux/gnome_desktop_display_info_monitor.cc b/remoting/host/linux/gnome_desktop_display_info_monitor.cc
index ad310e8..2673f67 100644
--- a/remoting/host/linux/gnome_desktop_display_info_monitor.cc
+++ b/remoting/host/linux/gnome_desktop_display_info_monitor.cc
@@ -46,11 +46,11 @@
   return desktop_display_info_ ? &desktop_display_info_.value() : nullptr;
 }
 
-void GnomeDesktopDisplayInfoMonitor::AddCallback(
+base::CallbackListSubscription GnomeDesktopDisplayInfoMonitor::AddCallback(
     base::RepeatingClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  callback_list_.AddUnsafe(std::move(callback));
+  return callback_list_.Add(std::move(callback));
 }
 
 void GnomeDesktopDisplayInfoMonitor::OnGnomeDisplayConfigReceived(
diff --git a/remoting/host/linux/gnome_desktop_display_info_monitor.h b/remoting/host/linux/gnome_desktop_display_info_monitor.h
index 09097f0..af03780 100644
--- a/remoting/host/linux/gnome_desktop_display_info_monitor.h
+++ b/remoting/host/linux/gnome_desktop_display_info_monitor.h
@@ -30,7 +30,8 @@
   void Start() override;
   bool IsStarted() const override;
   const DesktopDisplayInfo* GetLatestDisplayInfo() const override;
-  void AddCallback(base::RepeatingClosure callback) override;
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override;
 
  private:
   void OnGnomeDisplayConfigReceived(const GnomeDisplayConfig& config);
diff --git a/remoting/host/linux/pipewire_mouse_cursor_capturer.cc b/remoting/host/linux/pipewire_mouse_cursor_capturer.cc
index 77f9886..fd4c1fe4 100644
--- a/remoting/host/linux/pipewire_mouse_cursor_capturer.cc
+++ b/remoting/host/linux/pipewire_mouse_cursor_capturer.cc
@@ -34,8 +34,9 @@
     base::WeakPtr<CaptureStreamManager> stream_manager)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
index 7f4bb682..8e13a5e8 100644
--- a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
+++ b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc
@@ -35,8 +35,9 @@
     return info ? &info.value() : nullptr;
   }
 
-  void AddCallback(base::RepeatingClosure callback) override {
-    callbacks.AddUnsafe(std::move(callback));
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    return callbacks.Add(std::move(callback));
   }
 
   void NotifyChange() { callbacks.Notify(); }
@@ -74,10 +75,11 @@
   FakeDesktopDisplayInfoMonitor underlying_monitor_;
   DelegatingDesktopDisplayInfoMonitor monitor_{
       underlying_monitor_.GetWeakPtr()};
+  base::CallbackListSubscription subscription_;
 };
 
 void DelegatingDesktopDisplayInfoMonitorTest::AddCallback() {
-  monitor_.AddCallback(base::BindRepeating(
+  subscription_ = monitor_.AddCallback(base::BindRepeating(
       &DelegatingDesktopDisplayInfoMonitorTest::OnDisplayInfoChanged,
       base::Unretained(this)));
 }
diff --git a/remoting/host/linux/pipewire_mouse_cursor_capturer_unittest.cc b/remoting/host/linux/pipewire_mouse_cursor_capturer_unittest.cc
index 554218c8..6a43138 100644
--- a/remoting/host/linux/pipewire_mouse_cursor_capturer_unittest.cc
+++ b/remoting/host/linux/pipewire_mouse_cursor_capturer_unittest.cc
@@ -53,8 +53,9 @@
     return info_.get();
   }
 
-  void AddCallback(base::RepeatingClosure callback) override {
-    callbacks_.AddUnsafe(std::move(callback));
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    return callbacks_.Add(std::move(callback));
   }
 
   void SetDisplayInfo(std::unique_ptr<DesktopDisplayInfo> info) {
diff --git a/remoting/host/resizing_host_observer_unittest.cc b/remoting/host/resizing_host_observer_unittest.cc
index 5b2d283..877ee93 100644
--- a/remoting/host/resizing_host_observer_unittest.cc
+++ b/remoting/host/resizing_host_observer_unittest.cc
@@ -136,8 +136,9 @@
     return info ? &info.value() : nullptr;
   }
 
-  void AddCallback(base::RepeatingClosure callback) override {
-    callbacks.AddUnsafe(std::move(callback));
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    return callbacks.Add(std::move(callback));
   }
 
   std::optional<DesktopDisplayInfo> info;
diff --git a/remoting/host/webrtc_mouse_cursor_monitor_adaptor_unittest.cc b/remoting/host/webrtc_mouse_cursor_monitor_adaptor_unittest.cc
index 773b4908..e0629bbc 100644
--- a/remoting/host/webrtc_mouse_cursor_monitor_adaptor_unittest.cc
+++ b/remoting/host/webrtc_mouse_cursor_monitor_adaptor_unittest.cc
@@ -8,6 +8,7 @@
 #include <optional>
 #include <utility>
 
+#include "base/callback_list.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/notreached.h"
@@ -69,7 +70,10 @@
     return info ? &info.value() : nullptr;
   }
 
-  void AddCallback(base::RepeatingClosure callback) override { NOTREACHED(); }
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    NOTREACHED();
+  }
 
   bool started = false;
   std::optional<DesktopDisplayInfo> info;
diff --git a/remoting/host/win/mouse_cursor_monitor_win_unittest.cc b/remoting/host/win/mouse_cursor_monitor_win_unittest.cc
index 15160bbd..a1c1c51 100644
--- a/remoting/host/win/mouse_cursor_monitor_win_unittest.cc
+++ b/remoting/host/win/mouse_cursor_monitor_win_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/callback_list.h"
 #include "base/memory/raw_ptr.h"
 #include "base/sequence_checker.h"
 #include "base/test/task_environment.h"
@@ -59,7 +60,10 @@
   const DesktopDisplayInfo* GetLatestDisplayInfo() const override {
     return &info_;
   }
-  void AddCallback(base::RepeatingClosure callback) override {}
+  base::CallbackListSubscription AddCallback(
+      base::RepeatingClosure callback) override {
+    return base::CallbackListSubscription();
+  }
 
  private:
   DesktopDisplayInfo info_;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-after-free in remoting_host via DelegatingDesktopDisplayInfoMonitor on Linux Wayland

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 potential Use-after-free vulnerability exists in Chrome Remote Desktop’s host process on Linux Wayland when using non-GNOME compositors. A session-scoped monitor registers a raw pointer callback with a process-lifetime singleton but fails to unregister it upon session termination. This leads to a dangling pointer invocation during subsequent display configuration changes.

Affected files:

  • remoting/host/delegating_desktop_display_info_monitor.cc
  • remoting/host/polling_desktop_display_info_monitor.cc
  • remoting/host/linux/portal_interaction_strategy.cc
  • remoting/host/linux/portal_remote_desktop_session.cc

Estimated timestamp from git blame: 2025-11-03

Summary

A potential Use-after-free (UAF) vulnerability has been identified in the remoting_host process on Linux systems running Wayland with non-GNOME compositors (e.g., KDE Plasma, Sway). The issue arises from a lifetime mismatch where a per-session DelegatingDesktopDisplayInfoMonitor registers a callback with a process-lifetime singleton using base::Unretained(this) and fails to unregister it when the session ends.

Root Cause Analysis

In remoting/host/delegating_desktop_display_info_monitor.cc, the Start() method registers a repeating callback to its own OnUnderlyingDisplayInfoChanged method using base::Unretained(this):

void DelegatingDesktopDisplayInfoMonitor::Start() {
  // ...
  underlying_->AddCallback(base::BindRepeating(
      &DelegatingDesktopDisplayInfoMonitor::OnUnderlyingDisplayInfoChanged,
      base::Unretained(this)));
  // ...
}

The underlying_ monitor’s AddCallback implementation (found in PollingDesktopDisplayInfoMonitor) uses callback_list_.AddUnsafe(), which does not return a subscription handle for unregistration:

void PollingDesktopDisplayInfoMonitor::AddCallback(base::RepeatingClosure callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  callback_list_.AddUnsafe(std::move(callback));
}

On Linux Wayland (non-GNOME), PortalInteractionStrategy creates a DelegatingDesktopDisplayInfoMonitor that wraps a PollingDesktopDisplayInfoMonitor owned by the PortalRemoteDesktopSession singleton. This singleton outlives the per-session monitor.

When a session is destroyed, the DelegatingDesktopDisplayInfoMonitor is freed, but the dangling Unretained pointer remains in the singleton’s callback list. If a subsequent display information change occurs (triggered by a new session or the 100ms polling timer), the singleton invokes the dangling callback, resulting in a UAF.

Potential Impact

An attacker who can authenticate and connect to a Chrome Remote Desktop host on a vulnerable Linux Wayland configuration could potentially exploit this UAF to achieve arbitrary code execution. Since remoting_host is an unsandboxed process typically running with the privileges of the logged-in user, this could lead to full system compromise under that user’s context.

Potential Steps to Reproduce

  1. Use a Linux machine running a Wayland compositor like KDE Plasma or Sway.
  2. Set up and connect to the machine via Chrome Remote Desktop.
  3. Disconnect the CRD session. This destroys the per-session DelegatingDesktopDisplayInfoMonitor while leaving a dangling callback in the PortalRemoteDesktopSession singleton.
  4. Trigger a display resolution change (e.g., by connecting a new session or via local OS settings).
  5. The remoting_host process may crash or exhibit undefined behavior as it attempts to execute the dangling callback.

Suggested Fix

The DelegatingDesktopDisplayInfoMonitor::Start() method should use a base::WeakPtr instead of base::Unretained(this) when binding the callback. Alternatively, the DesktopDisplayInfoMonitor interface should be updated to return a base::CallbackListSubscription from AddCallback, allowing implementations like DelegatingDesktopDisplayInfoMonitor to manage their registration lifetime correctly.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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