diff --git a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc index 7f4bb682fb189..3ca65ea0855c9 100644 --- a/remoting/host/delegating_desktop_display_info_monitor_unittest.cc +++ b/remoting/host/delegating_desktop_display_info_monitor_unittest.cc @@ -137,6 +137,61 @@ TEST_F(DelegatingDesktopDisplayInfoMonitorTest, ASSERT_FALSE(underlying_monitor_.callbacks.empty()); } +// POC: Models the production lifetime mismatch on the Linux Wayland XDG-portal +// path, where PortalRemoteDesktopSession (a base::NoDestructor singleton) owns +// a process-lifetime PollingDesktopDisplayInfoMonitor, while each ClientSession +// owns a per-connection DelegatingDesktopDisplayInfoMonitor wrapping it. +// +// Sequence (mirrors connect -> disconnect -> display-change in remoting_host): +// 1. Long-lived |singleton_underlying| is created (NoDestructor singleton). +// 2. CRD client connects: per-session |delegating| is heap-allocated and +// Start() is called. Start() registers base::Unretained(delegating) into +// the singleton's RepeatingClosureList via AddUnsafe() -- no subscription +// handle is returned, and ~DelegatingDesktopDisplayInfoMonitor is +// `= default`, so the callback is never removed. +// 3. CRD client disconnects: ClientSession::OnConnectionClosed resets the +// desktop_environment_, destroying |delegating|. The singleton's callback +// list now holds a dangling Unretained pointer. +// 4. The singleton's 100ms polling timer observes a display-layout change +// and calls callback_list_.Notify(), which invokes +// DelegatingDesktopDisplayInfoMonitor::OnUnderlyingDisplayInfoChanged on +// the freed object -> heap-use-after-free reading |callbacks_|. +// +// In the real remoting_host binary this is NOT MiraclePtr-protected because +// HostMain() never calls PartitionAllocSupport::ReconfigureAfterFeatureListInit +// so BackupRefPtr is disabled and the Unretained-dangling check is a no-op. +TEST(DelegatingDesktopDisplayInfoMonitorUAF, + UnderlyingOutlivesDelegating_NotifyAfterDestruction_UseAfterFree) { + base::test::TaskEnvironment task_environment; + + // Step 1: process-lifetime underlying monitor (the NoDestructor singleton's + // PollingDesktopDisplayInfoMonitor in production). + FakeDesktopDisplayInfoMonitor singleton_underlying; + + // Step 2: first CRD client connects -> per-session delegating monitor. + auto delegating = std::make_unique( + singleton_underlying.GetWeakPtr()); + // ClientSession calls Start(): registers Unretained(delegating.get()) into + // |singleton_underlying.callbacks| via AddUnsafe() -- never removed. + delegating->Start(); + ASSERT_FALSE(singleton_underlying.callbacks.empty()); + + // Step 3: CRD client disconnects -> desktop_environment_.reset() destroys + // the delegating monitor. Dtor is `= default`: the dangling Unretained + // callback remains in the singleton's list. + delegating.reset(); + ASSERT_FALSE(singleton_underlying.callbacks.empty()); // still registered + + // Step 4: a later display-layout change (e.g. second client sends a resize + // and the 100ms polling timer observes it) triggers Notify() on the + // singleton's callback list. This dispatches to + // DelegatingDesktopDisplayInfoMonitor::OnUnderlyingDisplayInfoChanged on the + // freed object, which reads the freed |callbacks_| member. + // + // ASAN: heap-use-after-free at delegating_desktop_display_info_monitor.cc:72. + singleton_underlying.NotifyChange(); +} + TEST_F(DelegatingDesktopDisplayInfoMonitorTest, UnderlyingUpdate) { underlying_monitor_.started = true; underlying_monitor_.AddFakeDisplay();