Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
Bug ClassUAF
Tracker501576946
Fix commita861debbe5c3 (chromium/src) +92/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
ui/aura/window_event_dispatcher.cc
modified
DeleteWindowOnHeldTouchMove
ui/aura/window_event_dispatcher_unittest.cc
modified
EXPECT_TRUE
ui/aura/window_event_dispatcher_unittest.cc
modified
TEST_F
ui/aura/window_event_dispatcher_unittest.cc
modified

Files Changed

  • ui/aura/window_event_dispatcher.cc
  • ui/aura/window_event_dispatcher_unittest.cc
From a861debbe5c30ef947b4ec0762a06935bba47e77 Mon Sep 17 00:00:00 2001
From: Keren Zhu <kerenzhu@chromium.org>
Date: Wed, 06 May 2026 23:42:38 -0700
Subject: [PATCH] Fix UAF in WindowEventDispatcher::DispatchGestureEvent

Use WeakPtr to monitor the lifetime of the target window across the
DispatchHeldEvents() call. This prevents a use-after-free if the target
window is destroyed during the held event dispatch.

Bug: 501576946
Change-Id: I7e3c553a22be2d16d95661febb199d32771a810c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7816493
Commit-Queue: Keren Zhu <kerenzhu@chromium.org>
Reviewed-by: Mitsuru Oshima <oshima@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1626744}
---

diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc
index 51e38bfb..6fb3833 100644
--- a/ui/aura/window_event_dispatcher.cc
+++ b/ui/aura/window_event_dispatcher.cc
@@ -152,16 +152,17 @@
 void WindowEventDispatcher::DispatchGestureEvent(
     ui::GestureConsumer* raw_input_consumer,
     ui::GestureEvent* event) {
+  base::WeakPtr<ui::GestureConsumer> consumer_weak_ptr =
+      raw_input_consumer ? raw_input_consumer->GetWeakPtr() : nullptr;
+
   DispatchDetails details = DispatchHeldEvents();
-  if (details.dispatcher_destroyed)
+  if (details.dispatcher_destroyed || !consumer_weak_ptr) {
     return;
-  Window* target = ConsumerToWindow(raw_input_consumer);
-  if (target) {
-    event->ConvertLocationToTarget(window(), target);
-    details = DispatchEvent(target, event);
-    if (details.dispatcher_destroyed)
-      return;
   }
+
+  Window* target = ConsumerToWindow(consumer_weak_ptr.get());
+  event->ConvertLocationToTarget(window(), target);
+  details = DispatchEvent(target, event);
 }
 
 DispatchDetails WindowEventDispatcher::DispatchMouseExitAtPoint(
diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc
index 30095431..b9d7edf0 100644
--- a/ui/aura/window_event_dispatcher_unittest.cc
+++ b/ui/aura/window_event_dispatcher_unittest.cc
@@ -17,6 +17,7 @@
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/aura/client/aura_constants.h"
@@ -2173,6 +2174,89 @@
 
 }  // namespace
 
+namespace {
+
+// Pre-target handler on the root window that synchronously deletes a target
+// window the first time it sees a kTouchMoved event. Mirrors what a
+// close-on-exit widget / popup-dismissal handler can do during dispatch of a
+// held move event.
+class DeleteWindowOnHeldTouchMove : public ui::EventHandler {
+ public:
+  explicit DeleteWindowOnHeldTouchMove(aura::Window* victim)
+      : victim_(victim) {}
+
+  DeleteWindowOnHeldTouchMove(const DeleteWindowOnHeldTouchMove&) = delete;
+  DeleteWindowOnHeldTouchMove& operator=(const DeleteWindowOnHeldTouchMove&) =
+      delete;
+
+  bool did_delete() const { return did_delete_; }
+
+  void OnTouchEvent(ui::TouchEvent* event) override {
+    if (event->type() == ui::EventType::kTouchMoved && victim_) {
+      aura::Window* w = victim_.ExtractAsDangling();
+      did_delete_ = true;
+      delete w;
+      event->StopPropagation();
+    }
+  }
+
+ private:
+  raw_ptr<aura::Window> victim_;
+  bool did_delete_ = false;
+};
+
+}  // namespace
+
+// Regression test for a use-after-free in
+// WindowEventDispatcher::DispatchGestureEvent. On the timer-driven gesture
+// path (SHOW_PRESS / LONG_PRESS), DispatchGestureEvent receives the gesture
+// target as a bare ui::GestureConsumer* and first calls DispatchHeldEvents().
+// If a handler reached during the held-event dispatch synchronously destroys
+// the touched window, DispatchGestureEvent must not dereference the
+// now-dangling consumer pointer. Before the fix this test triggers an ASAN
+// heap-use-after-free at Window::ConvertPointToTarget (called from
+// LocatedEvent::ConvertLocationToTarget).
+TEST_F(WindowEventDispatcherTest,
+       GestureConsumerDestroyedDuringHeldEventDispatch) {
+  // Owned by root_window() (and later deleted by |handler|).
+  Window* w = CreateNormalWindow(1, root_window(), nullptr);
+  w->SetBounds(gfx::Rect(0, 0, 40, 40));
+
+  // 1) Touch-press on |w|: arms the SHOW_PRESS timer (5 ms in tests) and
+  //    creates a GestureProviderAura whose gesture_consumer_ is |w|.
+  ui::TouchEvent press(ui::EventType::kTouchPressed, gfx::Point(10, 10),
+                       ui::EventTimeForNow(),
+                       ui::PointerDetails(ui::EventPointerType::kTouch, 0));
+  DispatchEventUsingWindowDispatcher(&press);
+
+  // 2) Hold pointer moves so the next touch-move is queued as
+  //    held_move_event_ instead of dispatched immediately. This is the same
+  //    state that WindowTreeHost::OnCompositingChildResizing produces on
+  //    ChromeOS in production.
+  host()->dispatcher()->HoldPointerMoves();
+  ui::TouchEvent move(ui::EventType::kTouchMoved, gfx::Point(11, 10),
+                      ui::EventTimeForNow(),
+                      ui::PointerDetails(ui::EventPointerType::kTouch, 0));
+  DispatchEventUsingWindowDispatcher(&move);
+
+  // 3) Install a root pre-target handler that deletes |w| when the held
+  //    touch-move is later flushed by DispatchGestureEvent ->
+  //    DispatchHeldEvents().
+  DeleteWindowOnHeldTouchMove handler(w);
+  root_window()->AddPreTargetHandler(&handler);
+
+  // 4) Spin the loop until the SHOW_PRESS timer fires. The timer callback
+  //    calls WindowEventDispatcher::DispatchGestureEvent(w, show_press),
+  //    which first runs DispatchHeldEvents() -> dispatches the held
+  //    kTouchMoved -> |handler| deletes |w| -> control returns to
+  //    DispatchGestureEvent which then dereferences the freed |w| via
+  //    ConsumerToWindow / ConvertLocationToTarget.
+  EXPECT_TRUE(base::test::RunUntil([&]() { return handler.did_delete(); }));
+
+  root_window()->RemovePreTargetHandler(&handler);
+  host()->dispatcher()->ReleasePointerMoves();
+}
+
 // Verifies if a WindowTreeHost is deleted from dispatching a held mouse event
 // we don't crash.
 TEST_F(WindowEventDispatcherTest, DeleteHostFromHeldMouseEvent) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc
index 30095431..b9d7edf0 100644
--- a/ui/aura/window_event_dispatcher_unittest.cc
+++ b/ui/aura/window_event_dispatcher_unittest.cc
@@ -17,6 +17,7 @@
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/aura/client/aura_constants.h"
@@ -2173,6 +2174,89 @@
 
 }  // namespace
 
+namespace {
+
+// Pre-target handler on the root window that synchronously deletes a target
+// window the first time it sees a kTouchMoved event. Mirrors what a
+// close-on-exit widget / popup-dismissal handler can do during dispatch of a
+// held move event.
+class DeleteWindowOnHeldTouchMove : public ui::EventHandler {
+ public:
+  explicit DeleteWindowOnHeldTouchMove(aura::Window* victim)
+      : victim_(victim) {}
+
+  DeleteWindowOnHeldTouchMove(const DeleteWindowOnHeldTouchMove&) = delete;
+  DeleteWindowOnHeldTouchMove& operator=(const DeleteWindowOnHeldTouchMove&) =
+      delete;
+
+  bool did_delete() const { return did_delete_; }
+
+  void OnTouchEvent(ui::TouchEvent* event) override {
+    if (event->type() == ui::EventType::kTouchMoved && victim_) {
+      aura::Window* w = victim_.ExtractAsDangling();
+      did_delete_ = true;
+      delete w;
+      event->StopPropagation();
+    }
+  }
+
+ private:
+  raw_ptr<aura::Window> victim_;
+  bool did_delete_ = false;
+};
+
+}  // namespace
+
+// Regression test for a use-after-free in
+// WindowEventDispatcher::DispatchGestureEvent. On the timer-driven gesture
+// path (SHOW_PRESS / LONG_PRESS), DispatchGestureEvent receives the gesture
+// target as a bare ui::GestureConsumer* and first calls DispatchHeldEvents().
+// If a handler reached during the held-event dispatch synchronously destroys
+// the touched window, DispatchGestureEvent must not dereference the
+// now-dangling consumer pointer. Before the fix this test triggers an ASAN
+// heap-use-after-free at Window::ConvertPointToTarget (called from
+// LocatedEvent::ConvertLocationToTarget).
+TEST_F(WindowEventDispatcherTest,
+       GestureConsumerDestroyedDuringHeldEventDispatch) {
+  // Owned by root_window() (and later deleted by |handler|).
+  Window* w = CreateNormalWindow(1, root_window(), nullptr);
+  w->SetBounds(gfx::Rect(0, 0, 40, 40));
+
+  // 1) Touch-press on |w|: arms the SHOW_PRESS timer (5 ms in tests) and
+  //    creates a GestureProviderAura whose gesture_consumer_ is |w|.
+  ui::TouchEvent press(ui::EventType::kTouchPressed, gfx::Point(10, 10),
+                       ui::EventTimeForNow(),
+                       ui::PointerDetails(ui::EventPointerType::kTouch, 0));
+  DispatchEventUsingWindowDispatcher(&press);
+
+  // 2) Hold pointer moves so the next touch-move is queued as
+  //    held_move_event_ instead of dispatched immediately. This is the same
+  //    state that WindowTreeHost::OnCompositingChildResizing produces on
+  //    ChromeOS in production.
+  host()->dispatcher()->HoldPointerMoves();
+  ui::TouchEvent move(ui::EventType::kTouchMoved, gfx::Point(11, 10),
+                      ui::EventTimeForNow(),
+                      ui::PointerDetails(ui::EventPointerType::kTouch, 0));
+  DispatchEventUsingWindowDispatcher(&move);
+
+  // 3) Install a root pre-target handler that deletes |w| when the held
+  //    touch-move is later flushed by DispatchGestureEvent ->
+  //    DispatchHeldEvents().
+  DeleteWindowOnHeldTouchMove handler(w);
+  root_window()->AddPreTargetHandler(&handler);
+
+  // 4) Spin the loop until the SHOW_PRESS timer fires. The timer callback
+  //    calls WindowEventDispatcher::DispatchGestureEvent(w, show_press),
+  //    which first runs DispatchHeldEvents() -> dispatches the held
+  //    kTouchMoved -> |handler| deletes |w| -> control returns to
+  //    DispatchGestureEvent which then dereferences the freed |w| via
+  //    ConsumerToWindow / ConvertLocationToTarget.
+  EXPECT_TRUE(base::test::RunUntil([&]() { return handler.did_delete(); }));
+
+  root_window()->RemovePreTargetHandler(&handler);
+  host()->dispatcher()->ReleasePointerMoves();
+}
+
 // Verifies if a WindowTreeHost is deleted from dispatching a held mouse event
 // we don't crash.
 TEST_F(WindowEventDispatcherTest, DeleteHostFromHeldMouseEvent) {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in WindowEventDispatcher::DispatchGestureEvent via DispatchHeldEvents

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 a gesture event target aura::Window is synchronously destroyed during the processing of held events. Because DispatchGestureEvent lacks proper lifetime tracking before dispatching held events, it continues to use a dangling pointer after the memory is freed. This can lead to an arbitrary memory write and potential Remote Code Execution in the browser process.

Affected files:

  • ui/aura/window_event_dispatcher.cc
  • ui/events/gestures/gesture_recognizer_impl.cc
  • ui/events/gestures/gesture_provider_aura.cc

Estimated timestamp from git blame: 2019-09-26

Summary

A potential Use-After-Free (UAF) vulnerability exists in WindowEventDispatcher::DispatchGestureEvent within the browser process. The issue occurs because the target aura::Window can be synchronously destroyed during the execution of held events, leaving a dangling pointer that is subsequently used for event dispatch and observer tracking.

Technical Details

In ui/aura/window_event_dispatcher.cc, the DispatchGestureEvent function takes a bare pointer ui::GestureConsumer* raw_input_consumer. Before processing the gesture event itself, it synchronously flushes any held events:

void WindowEventDispatcher::DispatchGestureEvent(
    ui::GestureConsumer* raw_input_consumer,
    ui::GestureEvent* event) {
  DispatchDetails details = DispatchHeldEvents();
  if (details.dispatcher_destroyed)
    return;
  Window* target = ConsumerToWindow(raw_input_consumer);
  if (target) {
    // ... uses dangling target pointer
  }
}

If DispatchHeldEvents() triggers an event handler that synchronously destroys the target window, raw_input_consumer becomes a dangling pointer.

MiraclePtr Bypass: During aura::Window destruction, CleanupGestureState() is called. This synchronously deletes the GestureProviderAura object, which destroys its gesture_consumer_ raw_ptr. All other raw_ptr references (such as the parent/child hierarchy and tracking maps in GestureRecognizerImpl) are also cleared synchronously. Since the stack frames in the immediate dispatch path only hold bare C++ pointers, the BackupRefPtr reference count drops to zero. Consequently, the memory is immediately returned to the PartitionAlloc general pool without being quarantined.

Exploitation Primitive: Once the held events finish executing, DispatchGestureEvent converts the dangling pointer to target and proceeds. It eventually calls WindowEventDispatcher::PreDispatchEvent, which executes CHECK(window()->Contains(target_window)). An attacker who has reallocated the freed memory can spoof the parent_ pointer to point back to the root window, easily bypassing this check. Finally, a WindowTracker is instantiated on the stack, which calls target_window->AddObserver(this). This executes an emplace_back on the inline std::vector inside the Window’s base::ObserverList. By controlling the vector’s internal pointers (e.g., finish) in the reallocated memory block, the attacker gains a highly reliable arbitrary memory write primitive in the browser process.

Suggested Attacker Steps

Please note these are potential steps; our tooling has not yet executed a working proof-of-concept.

  1. Initiate a timer-based gesture (e.g., long-press) on a target window.
  2. Before the timer fires, trigger a layout change (e.g., viewport resize) to cause the WindowEventDispatcher to hold pointer moves.
  3. Send pointer move events, which are then aggregated into held_move_event_.
  4. When the long-press timer fires, DispatchGestureEvent is invoked with a bare pointer to the target window.
  5. DispatchGestureEvent immediately calls DispatchHeldEvents(), dispatching the held move event.
  6. The move event handler executes and synchronously deletes the target window. The memory is immediately freed and reclaimed by the attacker with crafted data.
  7. DispatchGestureEvent resumes, dereferencing the dangling pointer, bypassing the Contains check, and writing an observer pointer to an attacker-controlled address via std::vector::emplace_back.

Potential Fix

To fix this issue, DispatchGestureEvent should track the raw_input_consumer’s lifecycle during DispatchHeldEvents(). Since GestureConsumer doesn’t inherently support observers, the safest approach is to resolve the Window first and use a WindowTracker, identical to the pattern already used in PreDispatchEvent:

void WindowEventDispatcher::DispatchGestureEvent(
    ui::GestureConsumer* raw_input_consumer,
    ui::GestureEvent* event) {
  Window* target = ConsumerToWindow(raw_input_consumer);
  WindowTracker target_window_tracker;
  if (target)
    target_window_tracker.Add(target);

  DispatchDetails details = DispatchHeldEvents();
  if (details.dispatcher_destroyed)
    return;

  if (!target || target_window_tracker.windows().empty())
    return; // Target window was destroyed during held events dispatch.

  event->ConvertLocationToTarget(window(), target);
  details = DispatchEvent(target, event);
  // ...
}

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