diff --git a/remoting/host/touch_injector_win_unittest.cc b/remoting/host/touch_injector_win_unittest.cc --- a/remoting/host/touch_injector_win_unittest.cc +++ b/remoting/host/touch_injector_win_unittest.cc @@ -10,6 +10,10 @@ #include #include +#include "base/run_loop.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "base/time/time.h" #include "base/compiler_specific.h" #include "base/test/task_environment.h" #include "base/time/time.h" @@ -622,5 +626,93 @@ TEST_F(TouchInjectorWinTest, injector_.InjectTouchEvent(touch_end); } +// ----------------------------------------------------------------------------- +// POC: cross-thread Deinitialize() vs OnKeepAlive()/InjectTouchEvent() race. +// +// Mirrors InputInjectorWin::Core's real threading: InjectTouchEvent() runs on +// the "Input thread" (main_task_runner_) while Stop() -> Deinitialize() runs on +// the "UI thread" (ui_task_runner_). TouchInjectorWin has no synchronization, +// so under ASAN this produces heap-use-after-free on TouchInjectorWinDelegate +// (virtual call through freed object) and/or std::map node UAF; under TSAN it +// reports a data race on delegate_ / touches_in_contact_. +// +// Build & run on Windows: +// autoninja -C out\asan remoting_unittests +// out\asan\remoting_unittests.exe ^ +// --gtest_filter=TouchInjectorWinRaceTest.CrossThreadDeinitializeRace ^ +// --gtest_repeat=200 --gtest_break_on_failure +// ----------------------------------------------------------------------------- +class SlowDelegate : public TouchInjectorWinDelegate { + public: + SlowDelegate() : TouchInjectorWinDelegate(nullptr, nullptr, nullptr) {} + ~SlowDelegate() override = default; + + BOOL InitializeTouchInjection(UINT32, DWORD) override { return 1; } + + // Widen the race window: the Input thread is parked inside the virtual call + // on |delegate_| while the UI thread frees it via Deinitialize(). + DWORD InjectTouchInput(UINT32, const POINTER_TOUCH_INFO*) override { + ::Sleep(5); + return 1; + } +}; + +TEST(TouchInjectorWinRaceTest, CrossThreadDeinitializeRace) { + // Real-time TaskEnvironment so the keep_alive_timer_ actually fires on the + // input thread's sequence (no MOCK_TIME). + base::test::TaskEnvironment env{ + base::test::TaskEnvironment::TimeSource::SYSTEM_TIME}; + + for (int iter = 0; iter < 50; ++iter) { + auto injector = std::make_unique(); + + // Separate "Input thread" — same topology as desktop_process_main.cc. + base::Thread input_thread("CRD Input thread"); + ASSERT_TRUE(input_thread.Start()); + + base::WaitableEvent injected; + + // Phase 1: on Input thread, init + inject TOUCH_POINT_MOVE so that + // touches_in_contact_ is populated via std::map::operator[] and the + // 100 ms keep_alive_timer_ is bound to the Input thread sequence. + input_thread.task_runner()->PostTask( + FROM_HERE, base::BindOnce( + [](TouchInjectorWin* inj, base::WaitableEvent* done) { + inj->SetInjectorDelegateForTest( + std::make_unique()); + ASSERT_TRUE(inj->Init()); + TouchEvent ev; + ev.set_event_type(TouchEvent::TOUCH_POINT_MOVE); + for (uint32_t id = 0; id < 8; ++id) { + auto* p = ev.add_touch_points(); + p->set_id(id); + p->set_x(100.f); + p->set_y(100.f); + } + inj->InjectTouchEvent(ev); // starts keep-alive timer + done->Signal(); + }, + injector.get(), &injected)); + injected.Wait(); + + // Phase 2: let OnKeepAlive() begin firing on the Input thread. + base::PlatformThread::Sleep(TouchInjectorWin::kKeepAliveInterval + + base::Milliseconds(2)); + + // Phase 3: UI thread (this thread) calls Deinitialize() — exactly what + // InputInjectorWin::Core::Stop() does on ui_task_runner_. This frees the + // SlowDelegate and clears the std::map while the Input thread may be + // inside OnKeepAlive() / SlowDelegate::InjectTouchInput(). + injector->Deinitialize(); + + // Phase 4: tear down — mirrors ~InputInjectorWin::Core on the UI thread + // after Stop() returns. Cross-sequence ~RepeatingTimer race (c). + injector.reset(); + input_thread.Stop(); + } +} + } // namespace } // namespace remoting