Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Chromoting
DescriptionMemory corruption in Chromoting
ComponentChromoting
Sub-componentLinux
Bug ClassMemory Corruption
Tracker562236876
Fix commitbe8d13d1376f (chromium/src) +208/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
remoting/host/linux/remote_display_session_manager.cc
modified
RemoteDisplaySessionManagerTest
remoting/host/linux/remote_display_session_manager_unittest.cc
modified
TEST_F
remoting/host/linux/remote_display_session_manager_unittest.cc
modified

Files Changed

  • remoting/host/linux/BUILD.gn
  • remoting/host/linux/remote_display_session_manager_unittest.cc
  • remoting/host/linux/remote_display_session_manager.h
  • remoting/host/linux/remote_display_session_manager.cc
From be8d13d1376f1419a3d4a99a8907266c71fc2b67 Mon Sep 17 00:00:00 2001
From: Yuwei Huang <yuweih@chromium.org>
Date: Wed, 16 Sep 2026 18:53:04 -0700
Subject: [PATCH] [linux] Ignore session info for removed remote displays

In RemoteDisplaySessionManager, queries to logind for session info
are performed asynchronously. It is possible for a remote display
or an individual display session to be removed by GDM before the
session info query completes (for example, if the session is
terminated while the query is in flight).

This change checks whether the remote display and the specific
display session still exist before updating session info, preventing
stale lookups or resurrecting removed sessions. It also ensures that
pending startup queries are unblocked if a display session is removed.

Bug: 562236876
Change-Id: Ib3ca1311c43db8a929c0f1876d8108f1a073e898
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8415701
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
Reviewed-by: Lambros Lambrou <lambroslambrou@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699415}
---
diff --git a/remoting/host/linux/remote_display_session_manager.cc b/remoting/host/linux/remote_display_session_manager.cc
index db234d3..2e7f27ca 100644
--- a/remoting/host/linux/remote_display_session_manager.cc
+++ b/remoting/host/linux/remote_display_session_manager.cc
@@ -16,6 +16,7 @@
 #include "base/base_paths.h"
 #include "base/files/file_path.h"
 #include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/json/json_reader.h"
 #include "base/location.h"
 #include "base/logging.h"
@@ -204,17 +205,26 @@
 void RemoteDisplaySessionManager::HandleSessionInfoQueriesBlockingStartup() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  if (start_state_ == StartState::STARTED) {
+  if (start_state_ != StartState::STARTING) {
     return;
   }
 
-  DCHECK_EQ(start_state_, StartState::STARTING);
   if (session_info_queries_blocking_startup_.empty()) {
     start_state_ = StartState::STARTED;
-    std::move(init_callback_).Run(base::ok());
+    if (init_callback_) {
+      std::move(init_callback_).Run(base::ok());
+    }
   }
 }
 
+void RemoteDisplaySessionManager::FinalizeSessionInfoQuery(
+    const gvariant::ObjectPath& display_path) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  session_info_queries_blocking_startup_.erase(display_path);
+  HandleSessionInfoQueriesBlockingStartup();
+}
+
 void RemoteDisplaySessionManager::OnCreateDbusConnectionResult(
     base::expected<GDBusConnectionRef, Loggable> result) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -296,11 +306,17 @@
   }
   HOST_LOG << "GDM remote display session removed: " << display_path.value()
            << ", remote id: " << remote_id.value();
+
+  base::ScopedClosureRunner finalize_query(
+      base::BindOnce(&RemoteDisplaySessionManager::FinalizeSessionInfoQuery,
+                     weak_ptr_factory_.GetWeakPtr(), display_path));
+
   auto it = remote_displays_.find(display_name);
   if (it == remote_displays_.end()) {
     LOG(WARNING) << "Cannot find remote display with name: " << display_name;
     return;
   }
+
   RemoteDisplayInfo& display_info = it->second;
   display_info.sessions.erase(display_path);
   if (!display_info.sessions.empty()) {
@@ -457,19 +473,32 @@
     base::expected<LoginSessionManager::SessionInfo, Loggable> result) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
+  base::ScopedClosureRunner finalize_query(
+      base::BindOnce(&RemoteDisplaySessionManager::FinalizeSessionInfoQuery,
+                     weak_ptr_factory_.GetWeakPtr(), display_path));
+
   if (!result.has_value()) {
     LOG(ERROR) << "Failed to get session info for " << display_name << ": "
                << result.error();
-    session_info_queries_blocking_startup_.erase(display_path);
-    HandleSessionInfoQueriesBlockingStartup();
     return;
   }
   DCHECK(result->is_remote);
 
   auto remote_display_it = remote_displays_.find(display_name);
-  DCHECK(remote_display_it != remote_displays_.end());
+  if (remote_display_it == remote_displays_.end()) {
+    LOG(WARNING) << "Cannot find remote display with name: " << display_name;
+    return;
+  }
+
   auto& remote_display_info = remote_display_it->second;
-  auto& session = remote_display_info.sessions[display_path];
+  auto session_it = remote_display_info.sessions.find(display_path);
+  if (session_it == remote_display_info.sessions.end()) {
+    LOG(WARNING) << "Cannot find remote display session with display path: "
+                 << display_path.value();
+    return;
+  }
+
+  auto& session = session_it->second;
   session.session_info = std::move(*result);
   auto user_info_expected = GetPasswdUserInfo(session.session_info->username);
   if (user_info_expected.has_value()) {
@@ -481,9 +510,6 @@
   if (start_state_ == StartState::STARTED) {
     delegate_->OnRemoteDisplayChanged(display_name, remote_display_info);
   }
-
-  session_info_queries_blocking_startup_.erase(display_path);
-  HandleSessionInfoQueriesBlockingStartup();
 }
 
 }  // namespace remoting
diff --git a/remoting/host/linux/remote_display_session_manager.h b/remoting/host/linux/remote_display_session_manager.h
index 826c82c2..3243575 100644
--- a/remoting/host/linux/remote_display_session_manager.h
+++ b/remoting/host/linux/remote_display_session_manager.h
@@ -128,6 +128,8 @@
   }
 
  private:
+  friend class RemoteDisplaySessionManagerTest;
+
   enum class StartState {
     NOT_STARTED,
     STARTING,
@@ -141,6 +143,7 @@
   // If `start_state_` is `STARTING` and there are no more session info queries
   // blocking startup, then transition to `STARTED` and run the init callback.
   void HandleSessionInfoQueriesBlockingStartup();
+  void FinalizeSessionInfoQuery(const gvariant::ObjectPath& display_path);
 
   void OnCreateDbusConnectionResult(
       base::expected<GDBusConnectionRef, Loggable> result);
diff --git a/remoting/host/linux/remote_display_session_manager_unittest.cc b/remoting/host/linux/remote_display_session_manager_unittest.cc
new file mode 100644
index 0000000..eb71875
--- /dev/null
+++ b/remoting/host/linux/remote_display_session_manager_unittest.cc
@@ -0,0 +1,168 @@
+// 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/host/linux/remote_display_session_manager.h"
+
+#include <string>
+
+#include "base/no_destructor.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "base/thread_annotations.h"
+#include "base/types/expected.h"
+#include "remoting/host/linux/gvariant_ref.h"
+#include "remoting/host/linux/login_session_manager.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+namespace {
+
+constexpr char kDisplayName[] = "test_display";
+
+const gvariant::ObjectPath& DisplayPath1() {
+  static const base::NoDestructor<gvariant::ObjectPath> path(
+      *gvariant::ObjectPath::TryFrom("/org/gnome/DisplayManager/Displays/_1"));
+  return *path;
+}
+
+const gvariant::ObjectPath& DisplayPath2() {
+  static const base::NoDestructor<gvariant::ObjectPath> path(
+      *gvariant::ObjectPath::TryFrom("/org/gnome/DisplayManager/Displays/_2"));
+  return *path;
+}
+
+const gvariant::ObjectPath& RemoteId() {
+  static const base::NoDestructor<gvariant::ObjectPath> id(
+      *gvariant::ObjectPath::TryFrom(
+          "/com/google/ChromeRemoteDesktop/RemoteDisplays/test_display"));
+  return *id;
+}
+
+LoginSessionManager::SessionInfo CreateFakeSessionInfo(
+    const std::string& session_id,
+    const gvariant::ObjectPath& object_path) {
+  LoginSessionManager::SessionInfo info;
+  info.is_remote = true;
+  info.session_id = session_id;
+  info.session_class = "user";
+  info.session_type = "wayland";
+  info.username = "nobody";
+  info.uid = 65534;
+  info.object_path = object_path;
+  return info;
+}
+
+}  // namespace
+
+class RemoteDisplaySessionManagerTest : public testing::Test {
+ protected:
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::MainThreadType::UI};
+  RemoteDisplaySessionManager manager_;
+
+  void AddRemoteDisplay(std::string_view display_name,
+                        RemoteDisplaySessionManager::RemoteDisplayInfo info)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.remote_displays_[std::string(display_name)] = std::move(info);
+  }
+
+  void AddStartupBlockingQuery(const gvariant::ObjectPath& display_path)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.session_info_queries_blocking_startup_.insert(display_path);
+  }
+
+  bool HasStartupBlockingQuery(const gvariant::ObjectPath& display_path) const
+      NO_THREAD_SAFETY_ANALYSIS {
+    return manager_.session_info_queries_blocking_startup_.contains(
+        display_path);
+  }
+
+  void SetStarting(RemoteDisplaySessionManager::Callback init_callback)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.start_state_ = RemoteDisplaySessionManager::StartState::STARTING;
+    manager_.init_callback_ = std::move(init_callback);
+  }
+
+  void OnSessionInfoReady(
+      const std::string& display_name,
+      const gvariant::ObjectPath& display_path,
+      base::expected<LoginSessionManager::SessionInfo, Loggable> result) {
+    manager_.OnSessionInfoReady(display_name, display_path, std::move(result));
+  }
+
+  void OnRemoteDisplayRemoved(const gvariant::ObjectPath& display_path,
+                              const gvariant::ObjectPath& remote_id) {
+    manager_.OnRemoteDisplayRemoved(display_path, remote_id);
+  }
+};
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnSessionInfoReady_RemoteDisplayRemoved) {
+  AddStartupBlockingQuery(DisplayPath1());
+
+  // remote_displays_ does not contain kDisplayName.
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  // Should safely return without crash or inserting a phantom display.
+  EXPECT_TRUE(manager_.remote_displays().empty());
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnSessionInfoReady_DisplaySessionRemoved) {
+  // Setup remote display with only DisplayPath2.
+  RemoteDisplaySessionManager::RemoteDisplayInfo display_info;
+  display_info.sessions[DisplayPath2()] =
+      RemoteDisplaySessionManager::RemoteDisplaySession();
+  AddRemoteDisplay(kDisplayName, std::move(display_info));
+  AddStartupBlockingQuery(DisplayPath1());
+
+  // OnSessionInfoReady arrives for DisplayPath1 (which was removed).
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  // Should not resurrect DisplayPath1 into sessions.
+  const auto* info = manager_.GetRemoteDisplayInfo(kDisplayName);
+  ASSERT_NE(info, nullptr);
+  EXPECT_EQ(info->sessions.size(), 1u);
+  EXPECT_TRUE(info->sessions.contains(DisplayPath2()));
+  EXPECT_FALSE(info->sessions.contains(DisplayPath1()));
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnRemoteDisplayRemoved_UnblocksStartup) {
+  base::test::TestFuture<base::expected<void, Loggable>> init_future;
+  SetStarting(init_future.GetCallback());
+  AddStartupBlockingQuery(DisplayPath1());
+
+  OnRemoteDisplayRemoved(DisplayPath1(), RemoteId());
+
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+  EXPECT_TRUE(init_future.IsReady());
+  EXPECT_TRUE(init_future.Get().has_value());
+}
+
+TEST_F(RemoteDisplaySessionManagerTest, OnSessionInfoReady_PopulatesSession) {
+  RemoteDisplaySessionManager::RemoteDisplayInfo display_info;
+  display_info.sessions[DisplayPath1()] =
+      RemoteDisplaySessionManager::RemoteDisplaySession();
+  AddRemoteDisplay(kDisplayName, std::move(display_info));
+  AddStartupBlockingQuery(DisplayPath1());
+
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  const auto* info = manager_.GetRemoteDisplayInfo(kDisplayName);
+  ASSERT_NE(info, nullptr);
+  auto it = info->sessions.find(DisplayPath1());
+  ASSERT_NE(it, info->sessions.end());
+  ASSERT_TRUE(it->second.session_info.has_value());
+  EXPECT_EQ(it->second.session_info->session_id, "c1");
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+}  // namespace remoting
diff --git a/remoting/host/linux/BUILD.gn b/remoting/host/linux/BUILD.gn
index 8cc8f6a..3372c41 100644
--- a/remoting/host/linux/BUILD.gn
+++ b/remoting/host/linux/BUILD.gn
@@ -449,6 +449,7 @@
     "peer_connection_bpf_policy_linux_unittest.cc",
     "pipewire_audio_capturer_unittest.cc",
     "pipewire_mouse_cursor_capturer_unittest.cc",
+    "remote_display_session_manager_unittest.cc",
     "systemd_utils_unittest.cc",
     "test_util.cc",
     "test_util.h",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/host/linux/remote_display_session_manager_unittest.cc b/remoting/host/linux/remote_display_session_manager_unittest.cc
new file mode 100644
index 0000000..eb71875
--- /dev/null
+++ b/remoting/host/linux/remote_display_session_manager_unittest.cc
@@ -0,0 +1,168 @@
+// 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/host/linux/remote_display_session_manager.h"
+
+#include <string>
+
+#include "base/no_destructor.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "base/thread_annotations.h"
+#include "base/types/expected.h"
+#include "remoting/host/linux/gvariant_ref.h"
+#include "remoting/host/linux/login_session_manager.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+namespace {
+
+constexpr char kDisplayName[] = "test_display";
+
+const gvariant::ObjectPath& DisplayPath1() {
+  static const base::NoDestructor<gvariant::ObjectPath> path(
+      *gvariant::ObjectPath::TryFrom("/org/gnome/DisplayManager/Displays/_1"));
+  return *path;
+}
+
+const gvariant::ObjectPath& DisplayPath2() {
+  static const base::NoDestructor<gvariant::ObjectPath> path(
+      *gvariant::ObjectPath::TryFrom("/org/gnome/DisplayManager/Displays/_2"));
+  return *path;
+}
+
+const gvariant::ObjectPath& RemoteId() {
+  static const base::NoDestructor<gvariant::ObjectPath> id(
+      *gvariant::ObjectPath::TryFrom(
+          "/com/google/ChromeRemoteDesktop/RemoteDisplays/test_display"));
+  return *id;
+}
+
+LoginSessionManager::SessionInfo CreateFakeSessionInfo(
+    const std::string& session_id,
+    const gvariant::ObjectPath& object_path) {
+  LoginSessionManager::SessionInfo info;
+  info.is_remote = true;
+  info.session_id = session_id;
+  info.session_class = "user";
+  info.session_type = "wayland";
+  info.username = "nobody";
+  info.uid = 65534;
+  info.object_path = object_path;
+  return info;
+}
+
+}  // namespace
+
+class RemoteDisplaySessionManagerTest : public testing::Test {
+ protected:
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::MainThreadType::UI};
+  RemoteDisplaySessionManager manager_;
+
+  void AddRemoteDisplay(std::string_view display_name,
+                        RemoteDisplaySessionManager::RemoteDisplayInfo info)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.remote_displays_[std::string(display_name)] = std::move(info);
+  }
+
+  void AddStartupBlockingQuery(const gvariant::ObjectPath& display_path)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.session_info_queries_blocking_startup_.insert(display_path);
+  }
+
+  bool HasStartupBlockingQuery(const gvariant::ObjectPath& display_path) const
+      NO_THREAD_SAFETY_ANALYSIS {
+    return manager_.session_info_queries_blocking_startup_.contains(
+        display_path);
+  }
+
+  void SetStarting(RemoteDisplaySessionManager::Callback init_callback)
+      NO_THREAD_SAFETY_ANALYSIS {
+    manager_.start_state_ = RemoteDisplaySessionManager::StartState::STARTING;
+    manager_.init_callback_ = std::move(init_callback);
+  }
+
+  void OnSessionInfoReady(
+      const std::string& display_name,
+      const gvariant::ObjectPath& display_path,
+      base::expected<LoginSessionManager::SessionInfo, Loggable> result) {
+    manager_.OnSessionInfoReady(display_name, display_path, std::move(result));
+  }
+
+  void OnRemoteDisplayRemoved(const gvariant::ObjectPath& display_path,
+                              const gvariant::ObjectPath& remote_id) {
+    manager_.OnRemoteDisplayRemoved(display_path, remote_id);
+  }
+};
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnSessionInfoReady_RemoteDisplayRemoved) {
+  AddStartupBlockingQuery(DisplayPath1());
+
+  // remote_displays_ does not contain kDisplayName.
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  // Should safely return without crash or inserting a phantom display.
+  EXPECT_TRUE(manager_.remote_displays().empty());
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnSessionInfoReady_DisplaySessionRemoved) {
+  // Setup remote display with only DisplayPath2.
+  RemoteDisplaySessionManager::RemoteDisplayInfo display_info;
+  display_info.sessions[DisplayPath2()] =
+      RemoteDisplaySessionManager::RemoteDisplaySession();
+  AddRemoteDisplay(kDisplayName, std::move(display_info));
+  AddStartupBlockingQuery(DisplayPath1());
+
+  // OnSessionInfoReady arrives for DisplayPath1 (which was removed).
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  // Should not resurrect DisplayPath1 into sessions.
+  const auto* info = manager_.GetRemoteDisplayInfo(kDisplayName);
+  ASSERT_NE(info, nullptr);
+  EXPECT_EQ(info->sessions.size(), 1u);
+  EXPECT_TRUE(info->sessions.contains(DisplayPath2()));
+  EXPECT_FALSE(info->sessions.contains(DisplayPath1()));
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+TEST_F(RemoteDisplaySessionManagerTest,
+       OnRemoteDisplayRemoved_UnblocksStartup) {
+  base::test::TestFuture<base::expected<void, Loggable>> init_future;
+  SetStarting(init_future.GetCallback());
+  AddStartupBlockingQuery(DisplayPath1());
+
+  OnRemoteDisplayRemoved(DisplayPath1(), RemoteId());
+
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+  EXPECT_TRUE(init_future.IsReady());
+  EXPECT_TRUE(init_future.Get().has_value());
+}
+
+TEST_F(RemoteDisplaySessionManagerTest, OnSessionInfoReady_PopulatesSession) {
+  RemoteDisplaySessionManager::RemoteDisplayInfo display_info;
+  display_info.sessions[DisplayPath1()] =
+      RemoteDisplaySessionManager::RemoteDisplaySession();
+  AddRemoteDisplay(kDisplayName, std::move(display_info));
+  AddStartupBlockingQuery(DisplayPath1());
+
+  auto session_info = CreateFakeSessionInfo("c1", DisplayPath1());
+  OnSessionInfoReady(kDisplayName, DisplayPath1(), std::move(session_info));
+
+  const auto* info = manager_.GetRemoteDisplayInfo(kDisplayName);
+  ASSERT_NE(info, nullptr);
+  auto it = info->sessions.find(DisplayPath1());
+  ASSERT_NE(it, info->sessions.end());
+  ASSERT_TRUE(it->second.session_info.has_value());
+  EXPECT_EQ(it->second.session_info->session_id, "c1");
+  EXPECT_FALSE(HasStartupBlockingQuery(DisplayPath1()));
+}
+
+}  // namespace remoting
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF write in CRD Linux root daemon via unguarded map lookup in OnSessionInfoReady

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: In the Chrome Remote Desktop host for Linux, the root daemon’s RemoteDisplaySessionManager::OnSessionInfoReady guards its remote_displays_.find() lookup with a release-stripped DCHECK only, then dereferences the iterator and writes through it. If the asynchronous two-hop logind session-info query completes after the display entry was erased (GDM InterfacesRemoved racing the logind reply), release builds dereference end() and placement-new a session object through a dangling pointer into a freed heap buffer — a use-after-free write in the root process. The de-privileged _crd_network network process can drive both sides of the race over its Mojo interface to the daemon, making this a network-process→root privilege-escalation vector. An executed proof of concept reproduces the UAF write deterministically against a stock daemon binary.

Affected files:

Estimated timestamp from git blame: 2026-02-02

Summary

OnSessionInfoReady is the completion callback of an async two-hop logind D-Bus query (login1.Manager.GetSession then Properties.GetAll, remoting/host/linux/login_session_manager.cc:73-100). The callback is bound to the manager’s WeakPtr, not to the display entry, and nothing cancels in-flight queries when OnRemoteDisplayRemoved erases the entry. Both sibling handlers — OnRemoteDisplayRemoved (remote_display_session_manager.cc:299-303) and OnRemoteDisplayChanged (:328-332) — guard the identical lookup and bail with a warning; only OnSessionInfoReady assumes staleness cannot happen, and its error branch (:460-466) covers only a failed query.

Because remote_displays_ is a base::flat_map (sorted std::vector), the erased-then-looked-up sequence does not fault: find() on the emptied map returns end() == begin(), which points at the destroyed pair left in the vector’s live spare capacity. The destroyed pair’s inner sessions flat_map retains a self-consistent stale rep whose data pointer dangles into a freed heap buffer, so the subsequent sessions[display_path] insert is a placement-new write at the freed address, followed by ~200 bytes of structured SessionInfo/PasswdUserInfo member writes.

The daemon runs as root (remoting/host/installer/linux/debian/chrome-remote-desktop.service:10, --type=daemon, no User=), while the peer holding the mojom::DesktopSessionManager remote is the network process running as the installer-created _crd_network account — a process the daemon explicitly treats as potentially compromised (remoting/host/daemon_process.cc:111-136). That peer can create displays, terminate sessions at a chosen instant, and bias D-Bus timing, so the race is attacker-drivable, silent on failure, and retryable.

One environmental assumption remains (see Reproduction): the enabling GDM/logind event ordering was exercised against simulated GNOME services following GDM’s documented behavior, not against a live GNOME stack.


Root Cause Analysis

  1. Async query with no cancellation: remote_display_session_manager.cc:191-202 — QuerySessionInfo binds OnSessionInfoReady to the long-lived manager’s WeakPtr with captured display_name/display_path. Removing the display (:287-312) erases the map entry but leaves the query in flight.
  2. DCHECK-only guard on the completion path: the release build performs no check before dereferencing and mutating:
    // [`remoting/host/linux/remote_display_session_manager.cc:469-476`](https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/remote_display_session_manager.cc;l=469)
    auto remote_display_it = remote_displays_.find(display_name);
    DCHECK(remote_display_it != remote_displays_.end());        // no-op in release
    auto& remote_display_info = remote_display_it->second;      // deref of end()
    auto& session = remote_display_info.sessions[display_path]; // insert through stale rep
    session.session_info = std::move(*result);                  // ~150B structured write
    auto user_info_expected = GetPasswdUserInfo(session.session_info->username);
    if (user_info_expected.has_value()) {
      session.user_info = std::move(user_info_expected.value()); // ~100B more
    
  3. end() points at destroyed-but-unscrubbed residue: remote_displays_ is base::flat_map<std::string, RemoteDisplayInfo> = sorted std::vector<std::pair<...>> (remote_display_session_manager.h:75-76; base/containers/flat_map.h:190-193). Tail vector::erase destroys the element in place with no move-scrub (third_party/libc++/src/include/__vector/vector.h:1094-1103, 773-778), and erase never shrinks capacity — so end() == begin() after the map empties, an in-bounds pointer to the destroyed pair.
  4. The residue is self-consistent, so no hardening fires: under Chromium’s libc++ ABI v2 size-based vector layout, ~RemoteDisplayInfo frees the inner sessions heap buffer but leaves the rep as {__begin_ = dangling, __size_ = 0, __capacity_ = N ≥ 1} (__vector/vector.h:242-256; __vector/layout.h:208-260). _LIBCPP_HARDENING_MODE_EXTENSIVE is enabled but _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR is not, and __wrap_iter dereference is unchecked (__iterator/wrap_iter.h:58), so no assert or trap interrupts the path.
  5. UAF write via operator[]: sessions[display_path] → flat_map::operator[] (base/containers/flat_map.h:317-324) → unsafe_emplace (base/containers/flat_tree.h:1057-1064) → vector::emplace: size() != capacity() (0 ≠ N) selects __emplace_back_assume_capacity, a placement-new of pair<ObjectPath, RemoteDisplaySession> at the dangling __begin_ — a write into the freed PartitionAlloc slot, followed by the SessionInfo/PasswdUserInfo writes at deterministic offsets. None of the involved pointers are raw_ptr<>, so MiraclePtr offers no protection.
  6. Escalation variant (arbitrary free): the insert bumps the stale rep’s size to 1, so a second in-flight query for the same erased display compares display_path against the fake element via ObjectPath::operator<=> (remoting/host/base/gvariant_ref.h:629-658), reading the freed slot’s bytes. If the attacker has reclaimed the slot (every GetDesktopSession copies attacker-length strings into daemon heap, daemon_process.cc:354), the “found” path’s optional/string move-assigns deallocate attacker-forged “long” string buffers — free() of attacker-chosen pointer values in the root process.
  7. Attacker levers from the compromised network process: mojom::DesktopSessionManager (remoting/host/mojom/desktop_session.mojom:321-334) lets it create displays at will (GetDesktopSession with fresh client_id → desktop_session_factory_linux.cc:479-530 → GDM greeter spawn) and terminate sessions at a chosen instant (same client_id, different required_username → TerminateSession → login1.Session.Terminate, desktop_session_factory_linux.cc:290-304, 758-775; daemon_process.cc:176-210). Query replies and GDM removal signals are independently BindPostTask-ed to the daemon main sequence (remoting/host/linux/gdbus_connection_ref.cc:93-95, 143-146), so callback order equals bus-arrival order of two independent senders, and any local user — including _crd_network — can delay logind’s replies by flooding it with cheap serialized calls.

Reproduction

Preconditions: Linux host running the shipped multi-process CRD host (root daemon via the systemd unit) on a GDM version exposing the RemoteDisplay D-Bus API (GNOME 48+); attacker code execution in the _crd_network network process.

  1. From the network process, bind mojom::DesktopSessionManager (rebinding is tolerated, daemon_process.cc:111-136) and call GetDesktopSession(client_id="C", required_username="alice"). The daemon creates a remote display; GDM spawns a greeter; OnRemoteDisplayCreated inserts the map entry and fires the logind query.
  2. From an ordinary system-bus connection, flood org.freedesktop.login1 with serialized calls so its replies to the daemon lag GDM’s signals.
  3. Drive a user login through the CRD pipeline; GDM creates a second RemoteDisplay for the same remote id (gdm_remote_display_manager.h:97-102) and the daemon fires a second session-info query, now stalled.
  4. Immediately call GetDesktopSession(client_id="C", required_username="bob") — the mismatch terminates the session and the info-bearing login sessions; GDM removes the display objects (gdm_remote_display_manager.h:105-107), and OnRemoteDisplayRemoved empties sessions and erases the remote_displays_ entry while the reply is pending.
  5. Optionally spray GetDesktopSession calls with client_id strings sized to reclaim the freed sessions-buffer slot.
  6. The delayed reply dispatches into OnSessionInfoReady for the erased display.
  7. Expected: the callback detects the missing entry and returns early, as OnRemoteDisplayRemoved/OnRemoteDisplayChanged do.

Observed (executed POC, stock ASan daemon binary with dcheck_always_on=false, attacker logic injected only into the network process, deterministic across runs): AddressSanitizer reports heap-use-after-free WRITE of size 8 at offset 0 of the freed 496-byte sessions vector buffer — the pair<ObjectPath, RemoteDisplaySession> placement-new via flat_map::operator[]/vector::emplace from OnSessionInfoReady (remote_display_session_manager.cc:472); the region was freed by OnRemoteDisplayRemoved (remote_displays_.erase) and allocated by OnRemoteDisplayCreated (:278). The POC ran GDM/logind as simulated D-Bus services following GDM’s documented lifecycle, holding the session-info reply until after InterfacesRemoved — the interleaving the logind-flooding step is expected to produce in production; that real GDM/logind exhibit this ordering under flooding is the one remaining (plausible, code-supported) assumption. A guarded early return mirroring the sibling handlers was verified to eliminate the crash under the identical attack choreography.

Severity

medium — The demonstrated capability is a structured use-after-free write (plus a plausible arbitrary-free escalation) in the unsandboxed root CRD daemon, reachable from the deliberately de-privileged _crd_network process: the direct analog of “memory corruption in an unsandboxed process reachable only from a compromised renderer”, normally high. It is demoted one level for extreme trigger complexity: it requires a GNOME 48+/GDM RemoteDisplay host environment, a login/handover cycle through the CRD pipeline, and winning a cross-service D-Bus arrival-order race whose enabling GDM/logind teardown ordering is documented and simulated but not demonstrated against a live GNOME stack. In partial mitigation of that complexity, the race is attacker-biasable, silent on failure, and indefinitely retryable, and the corruption is deterministic once the interleaving occurs — this sits firmly at medium, not low. The DCHECK-only guard is release-stripped; no MiraclePtr, libc++ hardening, or UBSan trap applies on this path.

Suggested Fix Direction

Replace the DCHECK at remote_display_session_manager.cc:470 with the same early-return-with-warning guard used by OnRemoteDisplayRemoved (:299-303) and OnRemoteDisplayChanged (:328-332), including the session_info_queries_blocking_startup_.erase(display_path) / HandleSessionInfoQueriesBlockingStartup() bookkeeping the error branch performs. Cancelling or generation-checking in-flight GetSessionInfo queries on display removal would harden the pattern class.


POC: CRD Linux root daemon UAF write via DCHECK-only flat_map end() deref in RemoteDisplaySessionManager::OnSessionInfoReady

Strategy: code-injection Bug Type: heap-use-after-free (WRITE) in the root CRD daemon process

RemoteDisplaySessionManager::OnSessionInfoReady (remoting/host/linux/remote_display_session_manager.cc:469-479) guards its remote_displays_.find() lookup with a release-stripped DCHECK only. If the entry was erased while the async two-hop logind session-info query was in flight, the callback dereferences end() and inserts into the stale sessions flat_map residue, placement-new’ing a pair<ObjectPath, RemoteDisplaySession> through a dangling pointer into the freed sessions vector buffer — a UAF write inside the (root) daemon.

What this POC runs

  • Victim: completely stock remoting_me2me_host --type=daemon (ASan build, dcheck_always_on=false, so the release-only code path is exercised). No daemon-side code is modified.
  • Attacker: the network process (--type=network, runs as the de-privileged _crd_network account in production). chromium-changes.patch injects the simulated compromised-process logic into HostProcess::ApplyHostConfig (remoting_me2me_host.cc, network-process-only code): it sends two mojom::DesktopSessionManager::GetDesktopSession messages over the real daemon<->network Mojo channel:
    1. client_id="attacker-client", required_username="alice" — daemon creates a GDM remote display and starts async logind queries.
    2. 8s later: same client_id, required_username="bob" — the username mismatch makes the daemon TerminateSession → synchronous delete → TerminateRemoteDisplay → login1.Session.Terminate for the info-bearing greeter session, while the user-session info query is still unanswered.
  • External components (not part of Chromium): the sandbox has no GNOME, so fake_gdm_logind.c implements GDM’s RemoteDisplay D-Bus API and logind on a private “system bus” (dbus-daemon + poc_bus.conf, DBUS_SYSTEM_BUS_ADDRESS). It follows the GDM behavior documented in gdm_remote_display_manager.h:97-107 (greeter display dp1, user-login display dp2 for the same remote id, displays removed when their login session is terminated). The one attacker-relevant liberty is timing: the Properties.GetAll reply for the user session is held until after the displays are removed — the interleaving a compromised network process can force in production by flooding logind with cheap serialized calls so GDM’s InterfacesRemoved signals win the race.
  • Non-root sandbox adaptation: in production the daemon runs as root (systemd unit) and the installer creates the _crd_* accounts. crd_env_shim.c (LD_PRELOAD, daemon only) simulates exactly those two facts: getpwnam_r("_crd_*") returns an entry mapped to the current user, and setgroups() pretends to succeed. Worker processes are exec’d with a cleared environment, so the shim does not propagate into them. Also lab-environment enablement: a private bus replaces the system bus.

Prerequisites

  • ASan build of the Linux multi-process CRD host: autoninja -C out/asan remoting_me2me_host with chromium-changes.patch applied (git apply chromium-changes.patch from the source root). The out dir must have is_asan=true and dcheck_always_on=false.
  • dbus-daemon, gcc, glib/gio dev headers (pkg-config gio-2.0).

Reproduction

cd /chromium_src/src
git apply /poc-artifacts/chromium-changes.patch
autoninja -C out/asan remoting_me2me_host

cd /poc-artifacts        # or any directory containing these files
./run_poc.sh             # ~20s; compiles helpers, starts bus + fake
                         # GDM/logind, runs the stock daemon

The run takes ~15-20 seconds and ends with the daemon aborting on the ASan report (daemon_asan.log).

ASAN_OPTIONS=detect_container_overflow=0 is set by default: libc++ container annotations are an ASan-build-only feature with no release-build equivalent. With annotations enabled, ASan already flags the very first touch of the stale flat_map entry (a container-overflow READ inside the map’s live spare capacity — memory that is plain reachable in a release build; captured in daemon_asan_container_overflow.log, run with DETECT_CO=1 ./run_poc.sh). Disabling the annotation checks reproduces the release-build data flow, which continues into the freed-buffer WRITE — the actual heap-use-after-free.

Expected Result

heap-use-after-free ... WRITE of size 8 in the daemon process: placement-new of pair<ObjectPath, RemoteDisplaySession> (libc++ string copy ctor) via flat_map::operator[] / vector::emplace called from RemoteDisplaySessionManager::OnSessionInfoReady (remote_display_session_manager.cc:472), at offset 0 of the freed 496-byte sessions vector buffer that was:

  • freed by RemoteDisplaySessionManager::OnRemoteDisplayRemoved (remote_displays_.erase(it) destroying RemoteDisplayInfo in place),
  • allocated by RemoteDisplaySessionManager::OnRemoteDisplayCreated (rdsm.cc:278, insertion of the second display session).

Output

From daemon_asan_uaf.log (stock daemon, deterministic — reproduced on every run):

==1487==ERROR: AddressSanitizer: heap-use-after-free on address 0x6e69c1fe7b00 at pc 0x7119ec02f549 bp 0x7fff68533270 sp 0x7fff68533268
WRITE of size 8 at 0x6e69c1fe7b00 thread T0
    #0 ... in std::__Cr::basic_string<...>::__init_copy_ctor_external(char const*, unsigned long) third_party/libc++/src/include/string:2224:18
    #1 ... in std::__Cr::pair<remoting::gvariant::ObjectPath, remoting::RemoteDisplaySessionManager::RemoteDisplaySession>::pair<...>(...)
    #2 ... in std::__Cr::vector<std::__Cr::pair<remoting::gvariant::ObjectPath, remoting::RemoteDisplaySessionManager::RemoteDisplaySession>, ...>::emplace<...>(...) third_party/libc++/src/include/__memory/construct_at.h:37:49
    #3 ... in base::flat_map<remoting::gvariant::ObjectPath, remoting::RemoteDisplaySessionManager::RemoteDisplaySession, ...>::operator[]<...>(...) base/containers/flat_map.h
    #4 ... in remoting::RemoteDisplaySessionManager::OnSessionInfoReady(...) remoting/host/linux/remote_display_session_manager.cc:472:19
    #5..#8 ... LoginSessionManager::OnGetSessionPropertiesResult (async logind reply dispatch)

0x6e69c1fe7b00 is located 0 bytes inside of 496-byte region [0x6e69c1fe7b00,0x6e69c1fe7cf0)
freed by thread T0 here:
    #0 ... in operator delete(void*, unsigned long)
    #1 ... in std::__Cr::__destroy_at<std::__Cr::pair<std::__Cr::basic_string<...>, remoting::RemoteDisplaySessionManager::RemoteDisplayInfo>>(...)
    #2 ... in std::__Cr::vector<std::__Cr::pair<std::__Cr::basic_string<...>, remoting::RemoteDisplaySessionManager::RemoteDisplayInfo>, ...>::erase(...)
    #3 ... in remoting::RemoteDisplaySessionManager::OnRemoteDisplayRemoved(...) base/containers/flat_tree.h:916:16
    #4 ... in remoting::GdmRemoteDisplayManager::OnInterfacesRemoved(...) remoting/host/linux/gdm_remote_display_manager.cc:269:16

previously allocated by thread T0 here:
    #0 ... in operator new(unsigned long)
    #1 ... in std::__Cr::vector<std::__Cr::pair<remoting::gvariant::ObjectPath, remoting::RemoteDisplaySessionManager::RemoteDisplaySession>, ...>::emplace<...>(...)
    #3 ... in remoting::RemoteDisplaySessionManager::OnRemoteDisplayCreated(...) remoting/host/linux/remote_display_session_manager.cc:278:3
    #4 ... in remoting::GdmRemoteDisplayManager::OnInterfacesAddedInternal(...) remoting/host/linux/gdm_remote_display_manager.cc:238:16

SUMMARY: AddressSanitizer: heap-use-after-free third_party/libc++/src/include/string:2224:18 in std::__Cr::basic_string<...>::__init_copy_ctor_external(...)

Attack flow as recorded in the same log / fake_gdm_logind.log:

[POC] step 1: GetDesktopSession(client_id=attacker-client, required_username=alice)
Querying session info for remote display: .../RemoteDisplays/<uuid>, session id: c7   (greeter, answered)
Querying session info for remote display: .../RemoteDisplays/<uuid>, session id: c8   (user, reply held)
[POC] step 2: GetDesktopSession(same client_id, required_username=bob)
Terminating session 0 (error code: SESSION_REJECTED): Required username has changed.
login1 Session.Terminate(/org/freedesktop/login1/session/c7)
GDM remote display session removed: /org/gnome/DisplayManager/Displays/_11
GDM remote display session removed: /org/gnome/DisplayManager/Displays/_12   <- remote_displays_.erase()
releasing held Properties.GetAll(...c8) reply -> OnSessionInfoReady on erased entry -> UAF

Bandaid fix

bandaid-fix.patch replaces the DCHECK with the same guarded early-return already used by the sibling handlers OnRemoteDisplayRemoved (cc:299-303) and OnRemoteDisplayChanged (cc:328-332), plus the blocking-startup bookkeeping the error branch performs.

Verified: with the fix applied (and the identical attack choreography), the daemon logs WARNING: ... Cannot find remote display with name: <uuid> and keeps running — no ASan report (daemon_bandaid_verified.log).

Files

File Purpose
chromium-changes.patch attacker logic injected into the network process (simulated compromised _crd_network process)
bandaid-fix.patch minimal daemon-side fix (verified)
run_poc.sh orchestration: private bus + fake services + stock daemon
fake_gdm_logind.c fake GDM RemoteDisplay API + logind (external, non-Chromium components)
crd_env_shim.c LD_PRELOAD shim simulating root/crd* accounts in the non-root sandbox
poc_bus.conf private “system bus” configuration
daemon_asan_uaf.log main evidence: heap-use-after-free WRITE in the stock daemon
daemon_asan_container_overflow.log same run with ASan container annotations enabled (earlier detection point, ASan-only)
daemon_bandaid_verified.log fix verification run (no crash)
fake_gdm_logind.log fake-service message timeline of the successful run

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