CVE-2026-9933
Overview
Background
- `GestureRecognizerImpl`
- the browser-process gesture engine (
ui/events/gestures/gesture_recognizer_impl.cc). It tracks active touch consumers and theGestureEventHelpers (one per root, typically aWindowEventDispatcher) that dispatch synthetic events. - `CancelActiveTouchesImpl`
- cancels all active touches for a consumer: it looks up the consumer’s
GestureEventHelper* helperonce, builds a list of synthetickTouchCancelledevents (one per active pointer), and loops callinghelper->DispatchSyntheticTouchEvent(...)for each. - `WindowEventDispatcher`
- an aura class that implements
GestureEventHelper.DispatchSyntheticTouchEventhas avoidreturn, so it cannot report that it (and its owningWindowTreeHost) was synchronously destroyed by a handler during dispatch. - MiraclePtr/BackupRefPtr
- would normally quarantine a freed object while
raw_ptrs to it exist. Here it does not help: whenWindowEventDispatcheris destroyed it unregisters itself viaRemoveGestureEventHelper(this), dropping the onlyraw_ptr(inhelpers_), so the BRP refcount hits zero and the memory is freed, not quarantined. - `base::WeakPtr` / `GetWeakPtr()`
- the fix’s chosen liveness mechanism;
GestureEventHelpergains aGetWeakPtr()so the loop can detect destruction between iterations.
Root Cause Analysis
The invariant is that a GestureEventHelper* captured once must not be dereferenced again after a dispatch that can synchronously destroy it. GestureRecognizerImpl::CancelActiveTouchesImpl stored FindDispatchHelperForConsumer(consumer) into a bare local helper and then looped helper->DispatchSyntheticTouchEvent(cancelling_touch.get()) over every active pointer with no liveness recheck. If the handler receiving the first kTouchCancelled synchronously tears down the window (e.g. Widget::CloseNow()), the WindowTreeHost and its WindowEventDispatcher are destroyed; because DispatchSyntheticTouchEvent returns void, the loop never learns this and the next iteration performs a virtual call on the freed dispatcher — a use-after-free in the browser process. As described, BRP does not mitigate it because the dispatcher’s own destructor removes the last raw_ptr from helpers_, dropping the refcount to zero and releasing the memory immediately.
The patch migrates GestureEventHelper to base::WeakPtr-based liveness. The interface (ui/events/gestures/gesture_types.h) gains a GetWeakPtr() method; WindowEventDispatcher implements it (window_event_dispatcher.cc/.h) via a new base::WeakPtrFactory<WindowEventDispatcher> weak_ptr_factory_{this} and GetWeakPtr() override. The gesture recognizer’s consumer/helper bookkeeping is converted from bare raw_ptr lookups to weak-pointer-aware containers — visible in the unittest changes where consumers_ membership is now tested with std::find_if over weak_ptr.get() instead of consumers.find(window.get()). The intended effect (per the commit message “Harden GestureRecognizer … updating the GestureEventHelper to use base::WeakPtr”) is that CancelActiveTouchesImpl holds a weak reference and stops dispatching once the helper is destroyed. Note: the exact rewritten body of CancelActiveTouchesImpl is not present in the supplied (truncated) diff — the diff shows the GetWeakPtr plumbing, the container migration, and the regression test, but not the loop’s new liveness check — so the precise guard form is (inferred) from the commit message and the added infrastructure. The regression test WindowEventDispatcherTest.CancelActiveTouchesUAFWhenHandlerDeletesHost builds a second host with two active touch pointers and a pre-target handler that deletes the host on the first kTouchCancelled, exercising the second-iteration dereference that was the UAF. The advisory’s “Use-after-free” (CWE-416) matches.
CancelActiveTouchesImpl cached a GestureEventHelper* once and dereferenced it on every loop iteration, even though the first synthetic cancel could synchronously destroy that WindowEventDispatcher (whose void dispatch call hid the destruction) — a missing per-iteration liveness check, now provided by migrating the helper to base::WeakPtr.Attack Path
- Renderer (attacker)
from a compromised renderer, open a popup window/widget that owns its own
WindowTreeHost/WindowEventDispatcher. - Two active touches dispatch at least two simultaneous touch pointers to the popup so a later cancel loops more than once.
- Force cancellation
trigger a browser action that cancels active touches (capture change, or navigation path reaching
CancelActiveTouches/CancelActiveTouchesExcept). - Destroy on first cancel
the popup’s handler responds to the first
kTouchCancelledby synchronously closing itself (CloseNow()), destroying theWindowEventDispatcherand (via its destructor) removing its onlyraw_ptrso BRP frees the memory. - Reclaim (inferred) heap-spray to place attacker-controlled data in the freed dispatcher chunk before the loop continues.
- Primitive (inferred)
the second iteration calls the virtual
helper->DispatchSyntheticTouchEvent(...)on the freed/forged object, hijacking the vtable dispatch — browser-process code execution / sandbox escape.
Impact Assessment
WeakPtr hardening infrastructure; the heap reclamation and vtable-hijack-to-RCE are (inferred) from the report. BRP does not protect this path because the dispatcher drops its own last raw_ptr during teardown, freeing rather than quarantining the memory. CVSS 7.5 (S:U) reflects browser-process memory corruption whose escalation is unproven here.Changed Functions
| Function | Change | Notes |
|---|---|---|
DeleteHostOnTouchCancelHandlerui/aura/window_event_dispatcher_unittest.cc |
modified | |
ifui/aura/window_event_dispatcher_unittest.cc |
modified |
Files Changed
ui/aura/gestures/gesture_recognizer_unittest.ccui/aura/window_event_dispatcher.ccui/aura/window_event_dispatcher.hui/aura/window_event_dispatcher_unittest.cc
Audit Directions
- Bare helper/dispatcher pointers across dispatch loopsaudit
ui/events/ui/aurafor any loop that caches aGestureEventHelper*/WindowEventDispatcher*/observer and re-dereferences it after avoiddispatch that could destroy it; this is the exact pattern fixed here. - `void`-returning synchronous dispatchmethods like
DispatchSyntheticTouchEventthat internally detectdispatcher_destroyedbut discard it via avoidreturn are liability points; review whether callers can observe destruction. - Destructor-time `raw_ptr` drop defeating BRPobjects that unregister their own last
raw_ptrduring teardown (removing themselves from astd::vector<raw_ptr>) negate MiraclePtr protection; flag similar self-deregistration patterns. - Completeness of the WeakPtr migrationconfirm every
GestureEventHelperimplementer and every call site that stores a helper pointer across potential re-entrant destruction now goes throughGetWeakPtr().
Patch
From ee116a75e4b45eb59b2ab8cc3ba546f98fda5a77 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Thu, 14 May 2026 19:02:02 -0700 Subject: [PATCH] Harden GestureRecognizer Currently GestureRecognizerImpl::CancelActiveTouchesImpl has the potential to trigger a UAF if there are multiple dispatchers, and one is closed while processing an earlier one. We are updating the `GestureEventHelper` to use `base::WeakPtr` in order to prevent this. Bug: 501575979 Change-Id: If6711590cb397009805fc84a71be1b47b29815b2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7835038 Reviewed-by: Aman Verma <[email protected]> Reviewed-by: Nico Weber <[email protected]> Commit-Queue: Jonathan Ross <[email protected]> Cr-Commit-Position: refs/heads/main@{#1630993} --- diff --git a/ui/aura/gestures/gesture_recognizer_unittest.cc b/ui/aura/gestures/gesture_recognizer_unittest.cc index c37a852..70e1237 100644 --- a/ui/aura/gestures/gesture_recognizer_unittest.cc +++ b/ui/aura/gestures/gesture_recognizer_unittest.cc @@ -1963,7 +1963,11 @@ auto* gesture_recognizer = static_cast<ui::GestureRecognizerImpl*>( aura::Env::GetInstance()->gesture_recognizer()); const auto& consumers = gesture_recognizer->consumers_; - EXPECT_NE(consumers.cend(), consumers.find(window1.get())); + EXPECT_NE(consumers.end(), std::find_if(consumers.begin(), consumers.end(), + [&window1](const auto& weak_ptr) { + return weak_ptr.get() == + window1.get(); + })); // Create a second window for handling touch events. std::unique_ptr<QueueTouchEventDelegate> delegate2( @@ -1983,7 +1987,11 @@ /*time_stamp=*/tes.Now(), ui::PointerDetails(ui::EventPointerType::kTouch, kTouchId2)); DispatchEventUsingWindowDispatcher(&press2); - EXPECT_NE(consumers.cend(), consumers.find(window2.get())); + EXPECT_NE(consumers.end(), std::find_if(consumers.begin(), consumers.end(), + [&window2](const auto& weak_ptr) { + return weak_ptr.get() == + window2.get(); + })); // Verify that `press2` is associated with a gesture provider raw pointer. const auto& event_provider_mappings = diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc index f70ef8c..aa12229 100644 --- a/ui/aura/window_event_dispatcher.cc +++ b/ui/aura/window_event_dispatcher.cc @@ -613,6 +613,10 @@ //////////////////////////////////////////////////////////////////////////////// // WindowEventDispatcher, ui::GestureEventHelper implementation: +base::WeakPtr<ui::GestureEventHelper> WindowEventDispatcher::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + bool WindowEventDispatcher::CanDispatchToConsumer( ui::GestureConsumer* consumer) { Window* consumer_window = ConsumerToWindow(consumer); diff --git a/ui/aura/window_event_dispatcher.h b/ui/aura/window_event_dispatcher.h index 7ed490e3..02f4bc5 100644 --- a/ui/aura/window_event_dispatcher.h +++ b/ui/aura/window_event_dispatcher.h @@ -219,6 +219,7 @@ const ui::Event& event) override; // Overridden from ui::GestureEventHelper. + base::WeakPtr<ui::GestureEventHelper> GetWeakPtr() override; bool CanDispatchToConsumer(ui::GestureConsumer* consumer) override; void DispatchGestureEvent(ui::GestureConsumer* raw_input_consumer, ui::GestureEvent* event) override; @@ -337,6 +338,8 @@ // Used to schedule DispatchHeldEvents() when |move_hold_count_| goes to 0. base::WeakPtrFactory<WindowEventDispatcher> held_event_factory_{this}; + + base::WeakPtrFactory<WindowEventDispatcher> weak_ptr_factory_{this}; }; } // namespace aura diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc index b9d7edf0..2f0ea0c5 100644 --- a/ui/aura/window_event_dispatcher_unittest.cc +++ b/ui/aura/window_event_dispatcher_unittest.cc @@ -42,6 +42,7 @@ #include "ui/events/event_handler.h" #include "ui/events/event_utils.h" #include "ui/events/gesture_detection/gesture_configuration.h" +#include "ui/events/gestures/gesture_recognizer.h" #include "ui/events/keycodes/dom/dom_code.h" #include "ui/events/keycodes/keyboard_codes.h" #include "ui/events/test/event_generator.h" @@ -2280,6 +2281,108 @@ EXPECT_TRUE(delegate.got_destroy()); } +namespace { + +// Pre-target handler that synchronously deletes a WindowTreeHost the first +// time it sees a kTouchCancelled event. This simulates a browser-side handler +// (e.g. a Widget calling CloseNow()) that tears down a top-level +// DesktopWindowTreeHost while synthetic touch-cancels are being dispatched. +class DeleteHostOnTouchCancelHandler : public ui::EventHandler { + public: + explicit DeleteHostOnTouchCancelHandler(WindowTreeHost* host) : host_(host) {} + + DeleteHostOnTouchCancelHandler(const DeleteHostOnTouchCancelHandler&) = + delete; + DeleteHostOnTouchCancelHandler& operator=( + const DeleteHostOnTouchCancelHandler&) = delete; + + ~DeleteHostOnTouchCancelHandler() override = default; + + int touch_cancel_count() const { return touch_cancel_count_; } + bool host_deleted() const { return host_deleted_; } + + void OnTouchEvent(ui::TouchEvent* event) override { + if (event->type() != ui::EventType::kTouchCancelled) { + return; + } + ++touch_cancel_count_; + if (host_deleted_) { + return; + } + host_deleted_ = true; + // Stop propagation so that the (about-to-be-freed) target window doesn't + // see this event after we tear everything down underneath it. + event->StopPropagation(); + WindowTreeHost* host = host_; + host_ = nullptr; + // Destroys the WindowTreeHost, its root window, all child windows, and + // (last) the WindowEventDispatcher that is currently dispatching to us. + delete host; + } + + private: + raw_ptr<WindowTreeHost, base::RawPtrTraits::kMayDangle> host_; + bool host_deleted_ = false; + int touch_cancel_count_ = 0; +}; + +} // namespace + +// Regression test for a use-after-free in +// GestureRecognizerImpl::CancelActiveTouchesImpl. That function captures the +// consumer's WindowEventDispatcher* once into a bare local |helper| and then +// loops over one synthetic kTouchCancelled per active pointer, calling the +// pure-virtual helper->DispatchSyntheticTouchEvent() each iteration with no +// liveness check. If the first dispatch causes the WindowEventDispatcher to be +// synchronously destroyed (a first-class supported case — DispatchSynthetic- +// TouchEvent itself checks dispatcher_destroyed but discards it via its void +// return), the second iteration is a virtual call on freed memory. +// +// In production this corresponds to: ≥2 fingers down on a popup that owns its +// own DesktopWindowTreeHost, then a capture change in another root triggers +// CancelActiveTouchesExcept; the first synthetic cancel reaches a handler that +// CloseNow()'s the popup, freeing its WindowEventDispatcher mid-loop. +TEST_F(WindowEventDispatcherTest, + CancelActiveTouchesUAFWhenHandlerDeletesHost) { + // Second host with its own WindowEventDispatcher (the |helper|). Ownership + // is transferred to the handler, which deletes it during dispatch. + WindowTreeHost* h2 = WindowTreeHost::Create(ui::PlatformWindowInitProperties{ + gfx::Rect(0, 0, 200, 200)}) + .release(); + h2->InitHost(); + h2->window()->Show(); + + // Install the destroying handler as a pre-target handler on Env so it + // outlives |h2|'s window tree (which is torn down when |h2| is deleted). + DeleteHostOnTouchCancelHandler handler(h2); + Env::GetInstance()->AddPreTargetHandler(&handler); + + // A child window in |h2| acts as the GestureConsumer for the touches. + test::TestWindowDelegate delegate; + Window* child = CreateNormalWindow(1, h2->window(), &delegate); + child->SetBounds(gfx::Rect(0, 0, 200, 200)); + + // Register two active touch pointers on |child| via |h2|'s dispatcher so + // that CancelActiveTouchesImpl will loop twice. + ui::TouchEvent press0(ui::EventType::kTouchPressed, gfx::Point(20, 20), + ui::EventTimeForNow(), + ui::PointerDetails(ui::EventPointerType::kTouch, 0)); + h2->dispatcher()->OnEventFromSource(&press0); + ui::TouchEvent press1(ui::EventType::kTouchPressed, gfx::Point(60, 60), + ui::EventTimeForNow(), + ui::PointerDetails(ui::EventPointerType::kTouch, 1)); + h2->dispatcher()->OnEventFromSource(&press1); + + // This walks both active pointers; the first DispatchSyntheticTouchEvent + // reaches |handler| which deletes |h2| (and its WindowEventDispatcher). + // The second loop iteration then calls helper->DispatchSyntheticTouchEvent() + // on a freed WindowEventDispatcher: heap-use-after-free under ASAN. + Env::GetInstance()->gesture_recognizer()->CancelActiveTouchesExcept(nullptr);
Regression Test / PoC
diff --git a/ui/aura/gestures/gesture_recognizer_unittest.cc b/ui/aura/gestures/gesture_recognizer_unittest.cc
index c37a852..70e1237 100644
--- a/ui/aura/gestures/gesture_recognizer_unittest.cc
+++ b/ui/aura/gestures/gesture_recognizer_unittest.cc
@@ -1963,7 +1963,11 @@
auto* gesture_recognizer = static_cast<ui::GestureRecognizerImpl*>(
aura::Env::GetInstance()->gesture_recognizer());
const auto& consumers = gesture_recognizer->consumers_;
- EXPECT_NE(consumers.cend(), consumers.find(window1.get()));
+ EXPECT_NE(consumers.end(), std::find_if(consumers.begin(), consumers.end(),
+ [&window1](const auto& weak_ptr) {
+ return weak_ptr.get() ==
+ window1.get();
+ }));
// Create a second window for handling touch events.
std::unique_ptr<QueueTouchEventDelegate> delegate2(
@@ -1983,7 +1987,11 @@
/*time_stamp=*/tes.Now(),
ui::PointerDetails(ui::EventPointerType::kTouch, kTouchId2));
DispatchEventUsingWindowDispatcher(&press2);
- EXPECT_NE(consumers.cend(), consumers.find(window2.get()));
+ EXPECT_NE(consumers.end(), std::find_if(consumers.begin(), consumers.end(),
+ [&window2](const auto& weak_ptr) {
+ return weak_ptr.get() ==
+ window2.get();
+ }));
// Verify that `press2` is associated with a gesture provider raw pointer.
const auto& event_provider_mappings =
diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc
index b9d7edf0..2f0ea0c5 100644
--- a/ui/aura/window_event_dispatcher_unittest.cc
+++ b/ui/aura/window_event_dispatcher_unittest.cc
@@ -42,6 +42,7 @@
#include "ui/events/event_handler.h"
#include "ui/events/event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
+#include "ui/events/gestures/gesture_recognizer.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/test/event_generator.h"
@@ -2280,6 +2281,108 @@
EXPECT_TRUE(delegate.got_destroy());
}
+namespace {
+
+// Pre-target handler that synchronously deletes a WindowTreeHost the first
+// time it sees a kTouchCancelled event. This simulates a browser-side handler
+// (e.g. a Widget calling CloseNow()) that tears down a top-level
+// DesktopWindowTreeHost while synthetic touch-cancels are being dispatched.
+class DeleteHostOnTouchCancelHandler : public ui::EventHandler {
+ public:
+ explicit DeleteHostOnTouchCancelHandler(WindowTreeHost* host) : host_(host) {}
+
+ DeleteHostOnTouchCancelHandler(const DeleteHostOnTouchCancelHandler&) =
+ delete;
+ DeleteHostOnTouchCancelHandler& operator=(
+ const DeleteHostOnTouchCancelHandler&) = delete;
+
+ ~DeleteHostOnTouchCancelHandler() override = default;
+
+ int touch_cancel_count() const { return touch_cancel_count_; }
+ bool host_deleted() const { return host_deleted_; }
+
+ void OnTouchEvent(ui::TouchEvent* event) override {
+ if (event->type() != ui::EventType::kTouchCancelled) {
+ return;
+ }
+ ++touch_cancel_count_;
+ if (host_deleted_) {
+ return;
+ }
+ host_deleted_ = true;
+ // Stop propagation so that the (about-to-be-freed) target window doesn't
+ // see this event after we tear everything down underneath it.
+ event->StopPropagation();
+ WindowTreeHost* host = host_;
+ host_ = nullptr;
+ // Destroys the WindowTreeHost, its root window, all child windows, and
+ // (last) the WindowEventDispatcher that is currently dispatching to us.
+ delete host;
+ }
+
+ private:
+ raw_ptr<WindowTreeHost, base::RawPtrTraits::kMayDangle> host_;
+ bool host_deleted_ = false;
+ int touch_cancel_count_ = 0;
+};
+
+} // namespace
+
+// Regression test for a use-after-free in
+// GestureRecognizerImpl::CancelActiveTouchesImpl. That function captures the
+// consumer's WindowEventDispatcher* once into a bare local |helper| and then
+// loops over one synthetic kTouchCancelled per active pointer, calling the
+// pure-virtual helper->DispatchSyntheticTouchEvent() each iteration with no
+// liveness check. If the first dispatch causes the WindowEventDispatcher to be
+// synchronously destroyed (a first-class supported case — DispatchSynthetic-
+// TouchEvent itself checks dispatcher_destroyed but discards it via its void
+// return), the second iteration is a virtual call on freed memory.
+//
+// In production this corresponds to: ≥2 fingers down on a popup that owns its
+// own DesktopWindowTreeHost, then a capture change in another root triggers
+// CancelActiveTouchesExcept; the first synthetic cancel reaches a handler that
+// CloseNow()'s the popup, freeing its WindowEventDispatcher mid-loop.
+TEST_F(WindowEventDispatcherTest,
+ CancelActiveTouchesUAFWhenHandlerDeletesHost) {
+ // Second host with its own WindowEventDispatcher (the |helper|). Ownership
+ // is transferred to the handler, which deletes it during dispatch.
+ WindowTreeHost* h2 = WindowTreeHost::Create(ui::PlatformWindowInitProperties{
+ gfx::Rect(0, 0, 200, 200)})
+ .release();
+ h2->InitHost();
+ h2->window()->Show();
+
+ // Install the destroying handler as a pre-target handler on Env so it
+ // outlives |h2|'s window tree (which is torn down when |h2| is deleted).
+ DeleteHostOnTouchCancelHandler handler(h2);
+ Env::GetInstance()->AddPreTargetHandler(&handler);
+
+ // A child window in |h2| acts as the GestureConsumer for the touches.
+ test::TestWindowDelegate delegate;
+ Window* child = CreateNormalWindow(1, h2->window(), &delegate);
+ child->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+ // Register two active touch pointers on |child| via |h2|'s dispatcher so
+ // that CancelActiveTouchesImpl will loop twice.
+ ui::TouchEvent press0(ui::EventType::kTouchPressed, gfx::Point(20, 20),
+ ui::EventTimeForNow(),
+ ui::PointerDetails(ui::EventPointerType::kTouch, 0));
+ h2->dispatcher()->OnEventFromSource(&press0);
+ ui::TouchEvent press1(ui::EventType::kTouchPressed, gfx::Point(60, 60),
+ ui::EventTimeForNow(),
+ ui::PointerDetails(ui::EventPointerType::kTouch, 1));
+ h2->dispatcher()->OnEventFromSource(&press1);
+
+ // This walks both active pointers; the first DispatchSyntheticTouchEvent
+ // reaches |handler| which deletes |h2| (and its WindowEventDispatcher).
+ // The second loop iteration then calls helper->DispatchSyntheticTouchEvent()
+ // on a freed WindowEventDispatcher: heap-use-after-free under ASAN.
+ Env::GetInstance()->gesture_recognizer()->CancelActiveTouchesExcept(nullptr);
+
+ Env::GetInstance()->RemovePreTargetHandler(&handler);
+ EXPECT_TRUE(handler.host_deleted());
+}
+
TEST_F(WindowEventDispatcherTest, WindowHideCancelsActiveTouches) {
EventFilterRecorder recorder;
root_window()->AddPreTargetHandler(&recorder);
Original Bug Report
Potential Use-After-Free in GestureRecognizerImpl::CancelActiveTouchesImpl
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 without the Chrome Security team.
Overview: A potential Use-After-Free vulnerability exists in the browser process when synthetic touch cancellation events are dispatched. If a window is synchronously destroyed during the loop, a cached pointer becomes dangling, leading to a virtual call on freed memory. MiraclePtr does not prevent this due to the object explicitly dropping its raw_ptr reference during destruction.
Affected files:
ui/events/gestures/gesture_recognizer_impl.ccui/aura/window_event_dispatcher.ccui/events/gestures/gesture_types.h
Estimated timestamp from git blame: 2025-10-17
Description
A potential Use-After-Free (UAF) vulnerability exists in the browser process within ui/events/gestures/gesture_recognizer_impl.cc.
When multiple active touches need to be canceled, GestureRecognizerImpl::CancelActiveTouchesImpl retrieves a GestureEventHelper* (typically a WindowEventDispatcher) and stores it in a bare local pointer variable (helper). It then enters a for loop, iterating over all active touch points and dispatching a synthetic kTouchCancelled event for each:
bool GestureRecognizerImpl::CancelActiveTouchesImpl(GestureConsumer* consumer) {
GestureEventHelper* helper = FindDispatchHelperForConsumer(consumer);
// ...
std::vector<std::unique_ptr<TouchEvent>> cancelling_touches =
GetCancelledEventPerPointForConsumer(consumer);
// ...
for (const std::unique_ptr<TouchEvent>& cancelling_touch : cancelling_touches)
helper->DispatchSyntheticTouchEvent(cancelling_touch.get());
return true;
}
The vulnerability occurs if the UI event handler receiving the first kTouchCancelled event responds by synchronously destroying the window (e.g., via Widget::CloseNow()). This destroys the WindowTreeHost, which immediately destroys the WindowEventDispatcher.
Because WindowEventDispatcher::DispatchSyntheticTouchEvent has a void return type, it cannot signal its destruction back to the caller. The for loop simply proceeds to the next active touch point and calls helper->DispatchSyntheticTouchEvent(...) using the now-dangling helper pointer. Since this is a virtual method call, it requires reading the object’s vtable, providing an attacker a potential path to hijack the control flow.
MiraclePtr / BRP Bypass
MiraclePtr (BackupRefPtr) does not protect this specific code path. When WindowEventDispatcher is destroyed, its destructor explicitly unregisters itself by calling Env::GetInstance()->gesture_recognizer()->RemoveGestureEventHelper(this). This removes the dispatcher from GestureRecognizerImpl::helpers_ (a std::vector of raw_ptr).
Because the only active raw_ptr is destroyed during the teardown sequence, the BRP reference count for the object drops to zero. The memory is immediately freed to the allocator rather than quarantined, allowing an attacker to reclaim the memory via heap spraying before the second loop iteration occurs.
Potential Reproduction Steps
Note: These are suggested/potential steps to trigger the vulnerability. Our tooling agent does not yet have the ability to run code to verify an exploit chain.
- From a compromised renderer, open a new popup window or widget (creating a new
WindowTreeHostandWindowEventDispatcher). - Dispatch at least two simultaneous active touch events to the popup.
- Trigger an action that forces the browser to cancel the active touches (e.g., a capture change, or initiating a navigation that triggers
RenderWidgetHostViewAura::CancelActiveTouches). - If the popup’s event handler processes the first
kTouchCancelledevent by synchronously closing itself (e.g., calling a destruction sequence that triggersCloseNow()), the underlying dispatcher is freed. - The loop in
CancelActiveTouchesImplproceeds to the second touch point and dereferences the freed dispatcher, executing a hijacked virtual call if the heap was successfully sprayed.
Suggested Fix
Do not use a bare pointer across loop iterations where synchronous dispatch might destroy the object. One potential fix is to verify the liveness of the helper before each iteration. Since helpers_ is maintained by the GestureRecognizerImpl, the loop could verify the helper still exists in the helpers_ vector:
for (const std::unique_ptr<TouchEvent>& cancelling_touch : cancelling_touches) {
// Verify the helper hasn't been destroyed by the previous dispatch.
if (std::ranges::find(helpers_, helper) == helpers_.end())
break;
helper->DispatchSyntheticTouchEvent(cancelling_touch.get());
}
Alternatively, GestureEventHelper could be updated to support base::WeakPtr, allowing the loop to safely track its lifetime.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.