CVE-2026-113258
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/linux/remote_display_session_manager.cc |
modified | |
RemoteDisplaySessionManagerTestremoting/host/linux/remote_display_session_manager_unittest.cc |
modified | |
TEST_Fremoting/host/linux/remote_display_session_manager_unittest.cc |
modified |
Files Changed
remoting/host/linux/BUILD.gnremoting/host/linux/remote_display_session_manager_unittest.ccremoting/host/linux/remote_display_session_manager.hremoting/host/linux/remote_display_session_manager.cc
Patch
From be8d13d1376f1419a3d4a99a8907266c71fc2b67 Mon Sep 17 00:00:00 2001 From: Yuwei Huang <[email protected]> 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 <[email protected]> Reviewed-by: Lambros Lambrou <[email protected]> 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",
Regression Test / PoC
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
Original Bug Report
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:
remoting/host/linux/remote_display_session_manager.cc:469-479— DCHECK-only guard;end()dereference and container write inOnSessionInfoReadyremoting/host/linux/remote_display_session_manager.cc:191-202—QuerySessionInfoissues the async query with no cancellation when a display is removedremoting/host/linux/remote_display_session_manager.cc:287-312—OnRemoteDisplayRemovederases the entry mid-query (sessions.eraseat 305,remote_displays_.eraseat 310)
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
- Async query with no cancellation:
remote_display_session_manager.cc:191-202—QuerySessionInfobindsOnSessionInfoReadyto the long-lived manager’sWeakPtrwith captureddisplay_name/display_path. Removing the display (:287-312) erases the map entry but leaves the query in flight. - 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 end()points at destroyed-but-unscrubbed residue:remote_displays_isbase::flat_map<std::string, RemoteDisplayInfo>= sortedstd::vector<std::pair<...>>(remote_display_session_manager.h:75-76;base/containers/flat_map.h:190-193). Tailvector::erasedestroys 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 — soend() == begin()after the map empties, an in-bounds pointer to the destroyed pair.- The residue is self-consistent, so no hardening fires: under Chromium’s libc++ ABI v2 size-based vector layout,
~RemoteDisplayInfofrees the innersessionsheap 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_EXTENSIVEis enabled but_LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTORis not, and__wrap_iterdereference is unchecked (__iterator/wrap_iter.h:58), so no assert or trap interrupts the path. - 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 ofpair<ObjectPath, RemoteDisplaySession>at the dangling__begin_— a write into the freed PartitionAlloc slot, followed by theSessionInfo/PasswdUserInfowrites at deterministic offsets. None of the involved pointers areraw_ptr<>, so MiraclePtr offers no protection. - 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_pathagainst the fake element viaObjectPath::operator<=>(remoting/host/base/gvariant_ref.h:629-658), reading the freed slot’s bytes. If the attacker has reclaimed the slot (everyGetDesktopSessioncopies attacker-length strings into daemon heap,daemon_process.cc:354), the “found” path’soptional/string move-assigns deallocate attacker-forged “long” string buffers —free()of attacker-chosen pointer values in the root process. - Attacker levers from the compromised network process:
mojom::DesktopSessionManager(remoting/host/mojom/desktop_session.mojom:321-334) lets it create displays at will (GetDesktopSessionwith freshclient_id→desktop_session_factory_linux.cc:479-530→ GDM greeter spawn) and terminate sessions at a chosen instant (sameclient_id, differentrequired_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 independentlyBindPostTask-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.
- From the network process, bind
mojom::DesktopSessionManager(rebinding is tolerated,daemon_process.cc:111-136) and callGetDesktopSession(client_id="C", required_username="alice"). The daemon creates a remote display; GDM spawns a greeter;OnRemoteDisplayCreatedinserts the map entry and fires the logind query. - From an ordinary system-bus connection, flood
org.freedesktop.login1with serialized calls so its replies to the daemon lag GDM’s signals. - 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. - 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), andOnRemoteDisplayRemovedemptiessessionsand erases theremote_displays_entry while the reply is pending. - Optionally spray
GetDesktopSessioncalls withclient_idstrings sized to reclaim the freed sessions-buffer slot. - The delayed reply dispatches into
OnSessionInfoReadyfor the erased display. - Expected: the callback detects the missing entry and returns early, as
OnRemoteDisplayRemoved/OnRemoteDisplayChangeddo.
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_networkaccount in production).chromium-changes.patchinjects the simulated compromised-process logic intoHostProcess::ApplyHostConfig(remoting_me2me_host.cc, network-process-only code): it sends twomojom::DesktopSessionManager::GetDesktopSessionmessages over the real daemon<->network Mojo channel:client_id="attacker-client", required_username="alice"— daemon creates a GDM remote display and starts async logind queries.- 8s later: same
client_id,required_username="bob"— the username mismatch makes the daemonTerminateSession→ synchronous delete →TerminateRemoteDisplay→login1.Session.Terminatefor 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.cimplements 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: theProperties.GetAllreply 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’sInterfacesRemovedsignals 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, andsetgroups()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_hostwithchromium-changes.patchapplied (git apply chromium-changes.patchfrom the source root). The out dir must haveis_asan=trueanddcheck_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)destroyingRemoteDisplayInfoin 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.
- https://source.chromium.org/chromium/chromium/src/+/main:__iterator/wrap_iter.h;l=58
- https://source.chromium.org/chromium/chromium/src/+/main:__vector/layout.h;l=208
- https://source.chromium.org/chromium/chromium/src/+/main:__vector/vector.h;l=242
- https://source.chromium.org/chromium/chromium/src/+/main:base/containers/flat_map.h;l=190
- https://source.chromium.org/chromium/chromium/src/+/main:base/containers/flat_map.h;l=317
- https://source.chromium.org/chromium/chromium/src/+/main:base/containers/flat_tree.h;l=1057
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/base/gvariant_ref.h;l=629
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/daemon_process.cc;l=111
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/gdbus_connection_ref.cc;l=93
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/login_session_manager.cc;l=73
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/remote_display_session_manager.cc;l=191
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/remote_display_session_manager.cc;l=287
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/linux/remote_display_session_manager.cc;l=469
- https://source.chromium.org/chromium/chromium/src/+/main:remoting/host/mojom/desktop_session.mojom;l=321