Critical watch CVSS 8.1 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Sub-componentRemoting
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker522919313
Fix commit2c8e0329acc0 (chromium/src) +24/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-11 (per vendor advisory)
Fix landed2026-06-12 (public git)
Disclosed2026-06-30
Patch gap18 days — fix public in source before the advisory

Background

Chromoting
Chrome Remote Desktop’s remote-access host, which on Windows runs a DesktopEventHandler to observe desktop/session events on behalf of a connected client.
`DesktopEventHandler::Core`
the reference-counted (this is passed to base::BindOnce) inner object that owns a Delegate and 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 by delegate_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.

Key insight
The single mistake was destroying 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

  1. 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::Core with a running worker thread and a live Delegate. (inferred) — the diff shows only the internal teardown logic, not the client-facing trigger.
  2. 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) Delegate methods such as OnWorkerThreadStopping().
  3. Race the delegate free against the delegate use The old Stop() runs delegate_.reset() on the caller sequence, freeing the Delegate while the worker sequence still holds/uses it, so the worker’s dereference of delegate_ (or this-bound callback state) lands on freed memory — a use-after-free.
  4. Corrupt or hijack via the freed allocation Reclaiming the freed Delegate allocation 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

An attacker who can drive a Chromoting host session teardown gains a use-after-free of the 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

FunctionChangeNotes
if
remoting/host/win/desktop_event_handler.cc
modified

Files Changed

  • remoting/host/win/desktop_event_handler.cc

Audit Directions

  • Cross-sequence object destruction
    Search for reference-counted or thread-hopping objects (base::BindOnce(&Class::Method, this), worker SequencedTaskRunners) whose owned members are reset()/destroyed on the caller sequence rather than the sequence that invokes their methods.
  • Reset-under-lock, use-elsewhere
    Look for unique_ptr members guarded by a lock that are freed while holding the lock in one method but dereferenced from a different thread’s callback, especially in Stop()/Shutdown()/teardown paths.
  • Missing teardown-intent flags
    Flag 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.
From 2c8e0329acc02ccd42b831afb1e25bdfa04601fd Mon Sep 17 00:00:00 2001
From: Yuwei Huang <yuweih@chromium.org>
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 <yuweih@chromium.org>
Reviewed-by: Joe Downing <joedow@chromium.org>
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
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
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • remoting/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.

  1. Timer Setup: In Delegate::OnWorkerThreadStarted(), a base::RepeatingTimer is started on the worker thread at 100Hz to poll for cursor changes via CaptureCursorImage(). The callback is bound using base::Unretained(this). The Delegate also creates and owns a std::unique_ptr<webrtc::MouseCursorMonitor> webrtc_monitor_.
  2. Unsafe Teardown: When a remote session is disconnected, MouseCursorMonitorWin is destroyed on the caller sequence (typically the Network thread). Its destructor triggers DesktopEventHandler::~DesktopEventHandler, which calls DesktopEventHandler::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));
  }
}
  1. The Race Condition: The call to delegate_.reset() immediately destroys the Delegate object on the caller sequence. This destroys the webrtc_monitor_ (freeing its heap memory) and the base::RepeatingTimer.
  2. Use-After-Free: base::RepeatingTimer is 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 executing CaptureCursorImage(). The worker thread evaluates webrtc_monitor_->Capture(), dereferencing the just-freed unique_ptr memory, 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.)

  1. An attacker initiates an authenticated remote desktop connection to a Windows host.
  2. The connection is established, and the host spawns the WinEventWorkerThread and begins 100Hz cursor polling.
  3. The attacker sprays the heap with forged vtable pointers (e.g., using RDP channels or other manipulable input streams).
  4. The attacker abruptly drops the WebRTC connection or sends a Jingle session-terminate message.
  5. The host caller sequence executes delegate_.reset(), freeing the monitor.
  6. 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.

View on issue tracker