High CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
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
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-11 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-05-27
Patch gap12 days — fix public in source before the advisory

Background

`GestureRecognizerImpl`
the browser-process gesture engine (ui/events/gestures/gesture_recognizer_impl.cc). It tracks active touch consumers and the GestureEventHelpers (one per root, typically a WindowEventDispatcher) that dispatch synthetic events.
`CancelActiveTouchesImpl`
cancels all active touches for a consumer: it looks up the consumer’s GestureEventHelper* helper once, builds a list of synthetic kTouchCancelled events (one per active pointer), and loops calling helper->DispatchSyntheticTouchEvent(...) for each.
`WindowEventDispatcher`
an aura class that implements GestureEventHelper. DispatchSyntheticTouchEvent has a void return, so it cannot report that it (and its owning WindowTreeHost) 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: when WindowEventDispatcher is destroyed it unregisters itself via RemoveGestureEventHelper(this), dropping the only raw_ptr (in helpers_), so the BRP refcount hits zero and the memory is freed, not quarantined.
`base::WeakPtr` / `GetWeakPtr()`
the fix’s chosen liveness mechanism; GestureEventHelper gains a GetWeakPtr() 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.

Key insight
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

  1. Renderer (attacker) from a compromised renderer, open a popup window/widget that owns its own WindowTreeHost/WindowEventDispatcher.
  2. Two active touches dispatch at least two simultaneous touch pointers to the popup so a later cancel loops more than once.
  3. Force cancellation trigger a browser action that cancels active touches (capture change, or navigation path reaching CancelActiveTouches/CancelActiveTouchesExcept).
  4. Destroy on first cancel the popup’s handler responds to the first kTouchCancelled by synchronously closing itself (CloseNow()), destroying the WindowEventDispatcher and (via its destructor) removing its only raw_ptr so BRP frees the memory.
  5. Reclaim (inferred) heap-spray to place attacker-controlled data in the freed dispatcher chunk before the loop continues.
  6. 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

The UAF is in the browser process, so a successful exploit is a sandbox escape — the most severe outcome. It is reachable by influencing browser-side touch cancellation (a renderer can drive popups and navigations), but requires ≥2 active touches and a handler that synchronously destroys the dispatcher during the first cancel. The diff substantiates the use-after-free (the ASAN regression test deletes the host mid-loop and expects the second dispatch to be safe) and the 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

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

Audit Directions

  • Bare helper/dispatcher pointers across dispatch loops
    audit ui/events/ui/aura for any loop that caches a GestureEventHelper*/WindowEventDispatcher*/observer and re-dereferences it after a void dispatch that could destroy it; this is the exact pattern fixed here.
  • `void`-returning synchronous dispatch
    methods like DispatchSyntheticTouchEvent that internally detect dispatcher_destroyed but discard it via a void return are liability points; review whether callers can observe destruction.
  • Destructor-time `raw_ptr` drop defeating BRP
    objects that unregister their own last raw_ptr during teardown (removing themselves from a std::vector<raw_ptr>) negate MiraclePtr protection; flag similar self-deregistration patterns.
  • Completeness of the WeakPtr migration
    confirm every GestureEventHelper implementer and every call site that stores a helper pointer across potential re-entrant destruction now goes through GetWeakPtr().
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);
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 [email protected]

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