Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Bug ClassUAF
Tracker513394258
Fix commit3484cc819abb (chromium/src) +40/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
remoting/host/input_injector_win.cc
modified
if
remoting/host/touch_injector_win.cc
modified

Files Changed

  • remoting/host/input_injector_win.cc
  • remoting/host/touch_injector_win.cc
  • remoting/host/touch_injector_win.h
From 3484cc819abb4bf06aec11e87aa786c9b414480f Mon Sep 17 00:00:00 2001
From: Joe Downing <[email protected]>
Date: Wed, 20 May 2026 11:44:52 -0700
Subject: [PATCH] Fixing a potential race condition in TouchInputInjector

Move TouchInjectorWin creation and deletion to main_task_runner_.

Bug: 513394258
Change-Id: I8872bcae6012a53c9016395802f4b33f80c294ac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858188
Commit-Queue: Joe Downing <[email protected]>
Reviewed-by: Yuwei Huang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633732}
---

diff --git a/remoting/host/input_injector_win.cc b/remoting/host/input_injector_win.cc
index 10c9125..afc03ac 100644
--- a/remoting/host/input_injector_win.cc
+++ b/remoting/host/input_injector_win.cc
@@ -21,6 +21,7 @@
 #include "base/memory/ptr_util.h"
 #include "base/memory/ref_counted.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/task/sequenced_task_runner.h"
 #include "base/task/single_thread_task_runner.h"
 #include "remoting/base/util.h"
 #include "remoting/host/clipboard.h"
@@ -247,10 +248,14 @@
     void HandleMouse(const MouseEvent& event);
     void HandleTouch(const TouchEvent& event);
 
+    void StartTouchInjector();
+    void StopTouchInjector();
+
     scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
     scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner_;
     std::unique_ptr<Clipboard> clipboard_;
-    std::unique_ptr<TouchInjectorWin> touch_injector_;
+    std::unique_ptr<TouchInjectorWin, base::OnTaskRunnerDeleter>
+        touch_injector_;
   };
 
   scoped_refptr<Core> core_;
@@ -297,7 +302,7 @@
     : main_task_runner_(main_task_runner),
       ui_task_runner_(ui_task_runner),
       clipboard_(Clipboard::Create()),
-      touch_injector_(new TouchInjectorWin()) {}
+      touch_injector_(nullptr, base::OnTaskRunnerDeleter(main_task_runner)) {}
 
 void InputInjectorWin::Core::InjectClipboardEvent(const ClipboardEvent& event) {
   if (!ui_task_runner_->BelongsToCurrentThread()) {
@@ -360,7 +365,8 @@
   }
 
   clipboard_->Start(std::move(client_clipboard));
-  touch_injector_->Init();
+  main_task_runner_->PostTask(FROM_HERE,
+                              base::BindOnce(&Core::StartTouchInjector, this));
 }
 
 void InputInjectorWin::Core::Stop() {
@@ -370,8 +376,22 @@
   }
 
   clipboard_.reset();
+  main_task_runner_->PostTask(FROM_HERE,
+                              base::BindOnce(&Core::StopTouchInjector, this));
+}
+
+void InputInjectorWin::Core::StartTouchInjector() {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
+  DCHECK(!touch_injector_);
+  touch_injector_.reset(new TouchInjectorWin());
+  touch_injector_->Init();
+}
+
+void InputInjectorWin::Core::StopTouchInjector() {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
   if (touch_injector_) {
     touch_injector_->Deinitialize();
+    touch_injector_.reset();
   }
 }
 
@@ -457,8 +477,9 @@
 }
 
 void InputInjectorWin::Core::HandleTouch(const TouchEvent& event) {
-  DCHECK(touch_injector_);
-  touch_injector_->InjectTouchEvent(event);
+  if (touch_injector_) {
+    touch_injector_->InjectTouchEvent(event);
+  }
 }
 
 }  // namespace
diff --git a/remoting/host/touch_injector_win.cc b/remoting/host/touch_injector_win.cc
index 0634678..8829275 100644
--- a/remoting/host/touch_injector_win.cc
+++ b/remoting/host/touch_injector_win.cc
@@ -12,6 +12,7 @@
 #include "base/logging.h"
 #include "base/native_library.h"
 #include "base/notreached.h"
+#include "base/sequence_checker.h"
 #include "base/time/time.h"
 #include "remoting/proto/event.pb.h"
 #include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
@@ -163,6 +164,7 @@
 // so that a mock delegate can be injected in tests and set expectations on the
 // mock and return value of this method.
 bool TouchInjectorWin::Init() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!delegate_) {
     delegate_ = TouchInjectorWinDelegate::Create();
   }
@@ -186,6 +188,7 @@
 }
 
 void TouchInjectorWin::Deinitialize() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   touches_in_contact_.clear();
   // Same reason as TouchInjectorWin::Init(). For injecting mock delegates for
   // tests, a new delegate is created here.
@@ -195,6 +198,7 @@
 }
 
 void TouchInjectorWin::InjectTouchEvent(const TouchEvent& event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!delegate_) {
     VLOG(3) << "Touch injection functions are not initialized.";
     return;
@@ -224,6 +228,7 @@
 }
 
 void TouchInjectorWin::AddNewTouchPoints(const TouchEvent& event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_START);
 
   std::vector<POINTER_TOUCH_INFO> touches;
@@ -249,6 +254,7 @@
 }
 
 void TouchInjectorWin::MoveTouchPoints(const TouchEvent& event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_MOVE);
 
   for (const TouchEventPoint& touch_point : event.touch_points()) {
@@ -269,6 +275,7 @@
 }
 
 void TouchInjectorWin::EndTouchPoints(const TouchEvent& event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_END);
 
   std::vector<POINTER_TOUCH_INFO> touches;
@@ -288,6 +295,7 @@
 }
 
 void TouchInjectorWin::CancelTouchPoints(const TouchEvent& event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_CANCEL);
 
   std::vector<POINTER_TOUCH_INFO> touches;
@@ -309,6 +317,7 @@
 
 bool TouchInjectorWin::InjectTouchInput(
     const std::vector<POINTER_TOUCH_INFO>& touches) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (delegate_->InjectTouchInput(touches.size(), touches.data()) == 0) {
     return false;
   }
@@ -318,6 +327,7 @@
 }
 
 void TouchInjectorWin::UpdateKeepAliveTimer() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (touches_in_contact_.empty()) {
     keep_alive_timer_.Stop();
     return;
@@ -329,6 +339,7 @@
 }
 
 void TouchInjectorWin::OnKeepAlive() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if ((base::TimeTicks::Now() - last_injected_time_) < kKeepAliveInterval) {
     return;
   }
diff --git a/remoting/host/touch_injector_win.h b/remoting/host/touch_injector_win.h
index 5abdab5e..6d01e96 100644
--- a/remoting/host/touch_injector_win.h
+++ b/remoting/host/touch_injector_win.h
@@ -14,6 +14,7 @@
 #include <vector>
 
 #include "base/scoped_native_library.h"
+#include "base/sequence_checker.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
Loading diff…

Original Bug Report

reported by [email protected]

Potential Race Condition and Use-After-Free in Chrome Remote Desktop TouchInjectorWin

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: A race condition in the Windows touch injection logic of Chrome Remote Desktop allows for unsynchronized access to shared state across different threads. By bypassing event tracking, an attacker can trigger a Use-After-Free or heap corruption during session teardown. This could potentially result in arbitrary code execution with SYSTEM privileges in the remoting_desktop.exe process.

Affected files:

  • remoting/host/touch_injector_win.cc
  • remoting/host/input_injector_win.cc
  • remoting/protocol/input_event_tracker.cc
  • remoting/host/desktop_session_agent.cc

Estimated timestamp from git blame: 2023-07-20

Summary

A potential race condition exists in TouchInjectorWin because its internal state is accessed from both the UI thread and the Input thread without synchronization. In the Windows Desktop Process (remoting_desktop.exe), lifecycle methods for touch injection are executed on the UI thread, while input events and keep-alive timers are handled on the Input thread. This lack of thread affinity can lead to a Use-After-Free (UAF) on the injection delegate or heap corruption in the active touch points map during session disconnection.

Root Cause Analysis

In remoting/host/input_injector_win.cc, InputInjectorWin::Core manages a TouchInjectorWin instance. It dispatches tasks across two task runners:

  1. UI Thread (ui_task_runner_): Executes Start() and Stop(), which call TouchInjectorWin::Init() and TouchInjectorWin::Deinitialize() respectively.
  2. Input Thread (main_task_runner_): Executes InjectTouchEvent(), which calls TouchInjectorWin::InjectTouchEvent() and manages a base::RepeatingTimer for keep-alive events.

TouchInjectorWin contains two critical members:

  • std::unique_ptr<TouchInjectorWinDelegate> delegate_
  • std::map<uint32_t, POINTER_TOUCH_INFO> touches_in_contact_

When a session is stopped, the UI thread calls Deinitialize(), which performs the following (remoting/host/touch_injector_win.cc):

void TouchInjectorWin::Deinitialize() {
  touches_in_contact_.clear();
  delegate_ = TouchInjectorWinDelegate::Create();
  // ...
  keep_alive_timer_.Stop();
}

Simultaneously, the Input thread may be executing OnKeepAlive(), which iterates over touches_in_contact_ and calls a virtual method on delegate_:

bool TouchInjectorWin::InjectTouchInput(...) {
  if (delegate_->InjectTouchInput(touches.size(), touches.data()) == 0)
  // ...
}

Because delegate_ is a std::unique_ptr and is reset on the UI thread, the Input thread can dereference a dangling pointer during the virtual call dispatch. Additionally, base::Timer is not thread-safe and requires Stop() to be called on the same sequence it was started on, which is violated here.

Attacker-Controlled Trigger

An attacker can ensure the race condition is reachable by bypassing the InputEventTracker logic. Normally, DesktopSessionAgent::Stop() calls input_tracker_->ReleaseAll(), which should cancel all active touches. However, InputEventTracker only tracks touch IDs for TOUCH_POINT_START events.

Potential steps to trigger the vulnerability:

  1. An authenticated client sends a TOUCH_POINT_MOVE event containing a new, arbitrary touch ID.
  2. InputEventTracker::InjectTouchEvent ignores this event for ID tracking purposes, leaving its tracking set empty.
  3. TouchInjectorWin::MoveTouchPoints processes the event, inserts the new ID into touches_in_contact_, and starts the 100ms keep-alive timer on the Input thread.
  4. The client disconnects, triggering DesktopSessionAgent::Stop() on the UI thread.
  5. The tracker’s ReleaseAll() does nothing because it is unaware of the active touch ID, and the UI thread proceeds to Deinitialize(), racing against the active timer on the Input thread.

Impact

Successful exploitation could lead to arbitrary code execution in the context of the remoting_desktop.exe process. On Windows, this process often runs with SYSTEM privileges or at a High Integrity level with uiAccess="true". This represents a potential privilege escalation path from an authenticated user to SYSTEM.

Suggested Fix

To resolve this issue, ensure that all access to TouchInjectorWin is restricted to a single sequence. This can be achieved by:

  1. Moving the calls to Init() and Deinitialize() from the UI thread to the Input thread within InputInjectorWin::Core.
  2. Adding a base::SequenceChecker to TouchInjectorWin to enforce thread affinity.
  3. Updating InputEventTracker to track touch point IDs for all touch event types, ensuring that all active points are correctly released during teardown.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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