Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker501575979
Fix commitee116a75e4b4 (chromium/src) +195/-37
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
DeleteHostOnTouchCancelHandler
ui/aura/window_event_dispatcher_unittest.cc
modified
if
ui/aura/window_event_dispatcher_unittest.cc
modified

Files Changed

  • ui/aura/gestures/gesture_recognizer_unittest.cc
  • ui/aura/window_event_dispatcher.cc
  • ui/aura/window_event_dispatcher.h
  • ui/aura/window_event_dispatcher_unittest.cc
From ee116a75e4b45eb59b2ab8cc3ba546f98fda5a77 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
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 <amanvr@google.com>
Reviewed-by: Nico Weber <thakis@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/aura/window_event_dispatcher.cc
  • ui/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.

  1. From a compromised renderer, open a new popup window or widget (creating a new WindowTreeHost and WindowEventDispatcher).
  2. Dispatch at least two simultaneous active touch events to the popup.
  3. 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).
  4. If the popup’s event handler processes the first kTouchCancelled event by synchronously closing itself (e.g., calling a destruction sequence that triggers CloseNow()), the underlying dispatcher is freed.
  5. The loop in CancelActiveTouchesImpl proceeds 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.

View on issue tracker