CVE-2026-10978
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/input_injector_win.cc |
modified | |
ifremoting/host/touch_injector_win.cc |
modified |
Files Changed
remoting/host/input_injector_win.ccremoting/host/touch_injector_win.ccremoting/host/touch_injector_win.h
Patch
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"
Original Bug Report
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.ccremoting/host/input_injector_win.ccremoting/protocol/input_event_tracker.ccremoting/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:
- UI Thread (
ui_task_runner_): ExecutesStart()andStop(), which callTouchInjectorWin::Init()andTouchInjectorWin::Deinitialize()respectively. - Input Thread (
main_task_runner_): ExecutesInjectTouchEvent(), which callsTouchInjectorWin::InjectTouchEvent()and manages abase::RepeatingTimerfor 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:
- An authenticated client sends a
TOUCH_POINT_MOVEevent containing a new, arbitrary touch ID. InputEventTracker::InjectTouchEventignores this event for ID tracking purposes, leaving its tracking set empty.TouchInjectorWin::MoveTouchPointsprocesses the event, inserts the new ID intotouches_in_contact_, and starts the 100ms keep-alive timer on the Input thread.- The client disconnects, triggering
DesktopSessionAgent::Stop()on the UI thread. - The tracker’s
ReleaseAll()does nothing because it is unaware of the active touch ID, and the UI thread proceeds toDeinitialize(), 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:
- Moving the calls to
Init()andDeinitialize()from the UI thread to the Input thread withinInputInjectorWin::Core. - Adding a
base::SequenceCheckertoTouchInjectorWinto enforce thread affinity. - Updating
InputEventTrackerto 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.