CVE-2026-13787
Overview
Background
- Chromoting
- Chrome Remote Desktop’s remote-access host, which on Windows runs a
DesktopEventHandlerto observe desktop/session events on behalf of a connected client. - `DesktopEventHandler::Core`
- the reference-counted (
thisis passed tobase::BindOnce) inner object that owns aDelegateand a worker thread, coordinating callbacks between a caller sequence and a worker sequence. - `Delegate`
- a callee object owned via
std::unique_ptr<Delegate> delegate_(guarded bydelegate_lock_) whose methods (e.g.OnWorkerThreadStopping()) are invoked from the worker thread. - Cross-sequence destruction
- freeing an object on one thread while another thread may still be executing its methods, which violates the object’s sequence-affinity invariant and produces a use-after-free.
Root Cause Analysis
In the pre-patch DesktopEventHandler::Core::Stop(), the code took delegate_lock_ and immediately called delegate_.reset(), destroying the Delegate on the caller sequence, and only afterward posted Core::DestroyWorkerThread to the worker thread. Because the Delegate is used from the worker sequence (DestroyWorkerThread() calls delegate_->OnWorkerThreadStopping() under the lock), destroying it on the caller thread while the worker thread could still be running or about to run created a sequence-safety violation and a window in which the freed Delegate was dereferenced. The commit message states directly that “destroying the delegate on the caller thread while it is active on the worker thread causes sequence safety issues.”
The fix adds a stopping_ flag (guarded by delegate_lock_) and, in Stop(), only destroys the Delegate inline when there is no worker_task_runner_; when a worker runner exists it defers destruction by posting DestroyWorkerThread(). In DestroyWorkerThread(), which runs on the worker sequence, the Delegate is now moved into a local delegate_to_destroy (when stopping_ is set) and destroyed after the lock is released, guaranteeing the Delegate is freed on the same sequence that uses it. This works because destruction is now bound to the worker sequence whenever that sequence is live, eliminating the concurrent free/use race.
delegate_ on the caller sequence in Stop() while the worker sequence still had access to it; the fix records a stopping_ intent and moves the actual delete onto the worker sequence inside DestroyWorkerThread() so the object is always freed where it is used.Attack Path
- Establish a Chromoting host session
An attacker interacts with (or induces teardown of) a Chrome Remote Desktop host on Windows that has instantiated a
DesktopEventHandler::Corewith a running worker thread and a liveDelegate.(inferred)— the diff shows only the internal teardown logic, not the client-facing trigger. - Trigger `Stop()` while the worker sequence is active
Session teardown calls
DesktopEventHandler::Core::Stop()on the caller sequence at a moment when the worker thread is executing (or scheduled to execute)Delegatemethods such asOnWorkerThreadStopping(). - Race the delegate free against the delegate use
The old
Stop()runsdelegate_.reset()on the caller sequence, freeing theDelegatewhile the worker sequence still holds/uses it, so the worker’s dereference ofdelegate_(orthis-bound callback state) lands on freed memory — a use-after-free. - Corrupt or hijack via the freed allocation
Reclaiming the freed
Delegateallocation with attacker-controlled data before the worker’s dereference could turn the UAF into memory corruption or control-flow influence.(inferred)— the patch does not demonstrate a reclamation primitive, a specific dereferenced field, or code execution.
Impact Assessment
Delegate object within the Chrome Remote Desktop host process on Windows, with the precondition that the worker thread is concurrently active during Stop(). Beyond the memory-corruption primitive itself, escalation to arbitrary code execution or privilege escalation is (inferred) and not demonstrated by the diff. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/win/desktop_event_handler.cc |
modified |
Files Changed
remoting/host/win/desktop_event_handler.cc
Audit Directions
- Cross-sequence object destructionSearch for reference-counted or thread-hopping objects (
base::BindOnce(&Class::Method, this), workerSequencedTaskRunners) whose owned members arereset()/destroyed on the caller sequence rather than the sequence that invokes their methods. - Reset-under-lock, use-elsewhereLook for
unique_ptrmembers guarded by a lock that are freed while holding the lock in one method but dereferenced from a different thread’s callback, especially inStop()/Shutdown()/teardown paths. - Missing teardown-intent flagsFlag stop/destroy sequences that lack a state flag (like the new
stopping_) to coordinate whether a deferred worker-side path is responsible for final destruction, since absence of such a flag often signals ambiguous ownership during shutdown.
Patch
From 2c8e0329acc02ccd42b831afb1e25bdfa04601fd Mon Sep 17 00:00:00 2001 From: Yuwei Huang <[email protected]> Date: Fri, 12 Jun 2026 12:31:21 -0700 Subject: [PATCH] remoting: Destroy DesktopEventHandler::Delegate on the worker thread Destroying the delegate on the caller thread while it is active on the worker thread causes sequence safety issues. This change defers destruction to the worker thread when the event handler is stopped. Bug: 522919313 Change-Id: Id5ca8d77c8f139cb549ce04d207110aa8395f24e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7927860 Auto-Submit: Yuwei Huang <[email protected]> Reviewed-by: Joe Downing <[email protected]> Commit-Queue: Yuwei Huang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1646172} --- diff --git a/remoting/host/win/desktop_event_handler.cc b/remoting/host/win/desktop_event_handler.cc index 959820ca..2dbe82a 100644 --- a/remoting/host/win/desktop_event_handler.cc +++ b/remoting/host/win/desktop_event_handler.cc @@ -103,6 +103,7 @@ // This is set to null after Stop() is called, i.e., DesktopEventHandler has // been destroyed. std::unique_ptr<Delegate> delegate_ GUARDED_BY(delegate_lock_); + bool stopping_ GUARDED_BY(delegate_lock_) = false; // Fields below are only initialized after the worker thread has started. std::unique_ptr<webrtc::Desktop> desktop_; @@ -159,11 +160,21 @@ void DesktopEventHandler::Core::Stop() { DCHECK_CALLED_ON_VALID_SEQUENCE(caller_sequence_checker_); - base::AutoLock lock(delegate_lock_); - delegate_.reset(); - if (worker_task_runner_) { - worker_task_runner_->PostTask( - FROM_HERE, base::BindOnce(&Core::DestroyWorkerThread, this)); + // Extract the delegate under the lock but destroy it outside the lock block + // to avoid deadlock and minimize lock contention. + std::unique_ptr<Delegate> delegate_to_destroy; + scoped_refptr<base::SequencedTaskRunner> task_runner; + { + base::AutoLock lock(delegate_lock_); + stopping_ = true; + task_runner = worker_task_runner_; + if (!task_runner) { + delegate_to_destroy = std::move(delegate_); + } + } + if (task_runner) { + task_runner->PostTask(FROM_HERE, + base::BindOnce(&Core::DestroyWorkerThread, this)); } } @@ -244,18 +255,25 @@ void DesktopEventHandler::Core::DestroyWorkerThread() { DCHECK_CALLED_ON_VALID_SEQUENCE(worker_sequence_checker_); + // Extract the delegate under the lock but destroy it outside the lock block + // to avoid deadlock and minimize lock contention. + std::unique_ptr<Delegate> delegate_to_destroy; + scoped_refptr<base::SequencedTaskRunner> task_runner_to_release; { base::AutoLock lock(delegate_lock_); if (delegate_) { delegate_->OnWorkerThreadStopping(); + if (stopping_) { + delegate_to_destroy = std::move(delegate_); + } } + task_runner_to_release = std::move(worker_task_runner_); } check_input_desktop_timer_.Stop(); if (win_event_hook_) { UnhookWinEvent(win_event_hook_); win_event_hook_ = nullptr; } - worker_task_runner_ = nullptr; } // DesktopEventHandler implementation
Original Bug Report
Potential Use-After-Free in MouseCursorMonitorWin via unsafe cross-thread destruction
Flapjack, 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 the Chrome Remote Desktop Host on Windows (remoting_desktop.exe). An unsafe cross-thread destruction of MouseCursorMonitorWin::Delegate during session teardown creates a race condition with a high-frequency polling timer, potentially leading to arbitrary code execution as SYSTEM.
Affected files:
remoting/host/win/desktop_event_handler.ccremoting/host/win/mouse_cursor_monitor_win.cc
Estimated timestamp from git blame: 2025-11-18
Summary
A potential Use-After-Free (UAF) vulnerability exists in the Windows host component of Chrome Remote Desktop (remoting_desktop.exe). The issue occurs due to the improper destruction of the MouseCursorMonitorWin::Delegate object across different threads during session teardown. This leads to a race condition with a 100Hz polling timer, causing a worker thread to dereference a freed std::unique_ptr object.
Vulnerability Details
In MouseCursorMonitorWin, cursor capturing is handled by a nested Delegate class running on a dedicated worker thread managed by DesktopEventHandler.
- Timer Setup: In
Delegate::OnWorkerThreadStarted(), abase::RepeatingTimeris started on the worker thread at 100Hz to poll for cursor changes viaCaptureCursorImage(). The callback is bound usingbase::Unretained(this). TheDelegatealso creates and owns astd::unique_ptr<webrtc::MouseCursorMonitor> webrtc_monitor_. - Unsafe Teardown: When a remote session is disconnected,
MouseCursorMonitorWinis destroyed on the caller sequence (typically the Network thread). Its destructor triggersDesktopEventHandler::~DesktopEventHandler, which callsDesktopEventHandler::Core::Stop():
void DesktopEventHandler::Core::Stop() {
DCHECK_CALLED_ON_VALID_SEQUENCE(caller_sequence_checker_);
base::AutoLock lock(delegate_lock_);
delegate_.reset(); // Unsafe cross-thread destruction
if (worker_task_runner_) {
worker_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&Core::DestroyWorkerThread, this));
}
}
- The Race Condition: The call to
delegate_.reset()immediately destroys theDelegateobject on the caller sequence. This destroys thewebrtc_monitor_(freeing its heap memory) and thebase::RepeatingTimer. - Use-After-Free:
base::RepeatingTimeris sequence-affine. Destroying it on a sequence different from the one it was started on is unsafe and fails to safely cancel pending or currently executing tasks on the worker thread. Because the timer fires every 10ms, there is a high probability that the worker thread is simultaneously executingCaptureCursorImage(). The worker thread evaluateswebrtc_monitor_->Capture(), dereferencing the just-freedunique_ptrmemory, resulting in a Use-After-Free.
Impact
In Chrome Remote Desktop on Windows, remoting_desktop.exe typically runs with SYSTEM privileges. Because the freed memory belongs to a std::unique_ptr, it is not protected by MiraclePtr (BRP). Furthermore, the UAF occurs on a virtual method call (Capture()). This provides an ideal primitive for an attacker to hijack control flow by reallocating the memory and forging the vtable, potentially leading to local privilege escalation and arbitrary code execution as SYSTEM.
Potential Attacker Steps
(Note: These are suggested steps based on code analysis; our tooling cannot run live code to verify.)
- An attacker initiates an authenticated remote desktop connection to a Windows host.
- The connection is established, and the host spawns the
WinEventWorkerThreadand begins 100Hz cursor polling. - The attacker sprays the heap with forged vtable pointers (e.g., using RDP channels or other manipulable input streams).
- The attacker abruptly drops the WebRTC connection or sends a Jingle
session-terminatemessage. - The host caller sequence executes
delegate_.reset(), freeing the monitor. - If timed correctly, the worker thread simultaneously executes
CaptureCursorImage()and dereferences the attacker-controlled memory, hijacking control flow.
Suggested Fix
The Delegate and its members must be destroyed on the sequence where they are used. DesktopEventHandler::Core::Stop() should not call delegate_.reset() directly on the caller thread.
Instead, the destruction of the delegate should be posted to the worker thread as part of DestroyWorkerThread, or Stop() should be refactored to cleanly signal the worker thread to stop the timer and delete the delegate before the thread is joined or dropped.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.