CVE-2026-9934
Overview
Background
- `DispatchHeldEvents()`
- Flushes events that were queued while pointer-move holding was active (e.g. during compositing/resize). Flushing runs arbitrary event handlers synchronously, so it is a re-entrancy point where application code can delete windows.
- `ui::GestureConsumer` / `ConsumerToWindow`
- A
GestureConsumeris the gesture target;ConsumerToWindowmaps it back to anaura::Window*. The consumer is owned elsewhere (theGestureProviderAura/window), so the dispatcher holds no lifetime stake in it. - `base::WeakPtr` / `GetWeakPtr()`
- The fix relies on
GestureConsumerexposingGetWeakPtr(), giving the dispatcher an invalidate-on-destroy handle to detect that the consumer died during the nested dispatch. - MiraclePtr/BackupRefPtr
- Chrome’s
raw_ptrUAF mitigation. It only quarantines freed memory while liveraw_ptrreferences remain; the dispatcher’s stack holds bare C++ pointers, so BRP does not protect this path (inferred from the advisory; the diff only shows theWeakPtrfix).
Root Cause Analysis
The vulnerable WindowEventDispatcher::DispatchGestureEvent called DispatchHeldEvents() before resolving and using its target, while holding the target only as a bare ui::GestureConsumer* raw_input_consumer. The old code was: DispatchHeldEvents(), bail only on details.dispatcher_destroyed, then ConsumerToWindow(raw_input_consumer) and DispatchEvent(target, event). The invariant that is violated is lifetime: DispatchHeldEvents() can synchronously run an event handler that destroys the gesture target window (and thus the GestureConsumer), but the function only guarded against the dispatcher being destroyed, never against the target consumer being destroyed. After the nested flush, raw_input_consumer is a dangling pointer that is still passed to ConsumerToWindow and whose resulting target is dereferenced via event->ConvertLocationToTarget(window(), target) and DispatchEvent.
The patch captures base::WeakPtr<ui::GestureConsumer> consumer_weak_ptr = raw_input_consumer->GetWeakPtr() before the DispatchHeldEvents() call, then after the flush returns it checks if (details.dispatcher_destroyed || !consumer_weak_ptr) return;. The consumer is only re-resolved through the validated weak pointer: ConsumerToWindow(consumer_weak_ptr.get()). If the target was destroyed during held-event dispatch, the WeakPtr is invalidated and the function returns instead of touching freed memory. Note the commit message and diff implement a WeakPtr guard; the reporter’s suggested fix used a WindowTracker instead — same bug, different (equivalent) mitigation, so the advisory’s suggested-fix code does not match the landed code.
DispatchHeldEvents) that can run handlers which free that target, and only checked whether the dispatcher — not the target — survived.Attack Path
- Arm a timer gesture
Initiate a long-press/show-press style gesture on a target
aura::Windowso the timer-driven path that ends inDispatchGestureEvent(target, event)is scheduled. - Force pointer-move holding
Cause the dispatcher to hold pointer moves (e.g. a compositing child resize /
HoldPointerMoves), so a subsequent touch-move is queued asheld_move_event_rather than dispatched immediately. - Install a destroying handler
Arrange a pre-target handler that, on the held
kTouchMoved, synchronously destroys the touched window (mirrors a close-on-exit popup/widget-dismiss handler). (inferred reachability from a web/renderer context; the diff’s regression test drives this directly in-process.) - Fire the timer
When the gesture timer fires,
DispatchGestureEventrunsDispatchHeldEvents(), which dispatches the held move, invoking the handler that frees the target window and itsGestureConsumer. - Dangling use
Control returns to
DispatchGestureEvent, which (pre-patch) callsConsumerToWindow(raw_input_consumer)andConvertLocationToTarget/DispatchEventon the freed window — a use-after-free in the browser process. - Primitive
Read/write of a freed
aura::Window; with heap reclamation an attacker-controlled object is used in dispatch, yielding a controlled write primitive and potential browser-process RCE (escalation inferred; the diff only proves the UAF/crash).
Impact Assessment
AC:H/UI:R reflects this). The proven primitive from the diff is a dereference of freed memory (crash); the advisory’s escalation to a controlled std::vector/observer-list write and browser RCE is plausible but inferred and not demonstrated by the diff. Because the dispatch stack holds bare pointers, BackupRefPtr does not quarantine the freed window, aiding reclamation (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/aura/window_event_dispatcher.cc |
modified | |
DeleteWindowOnHeldTouchMoveui/aura/window_event_dispatcher_unittest.cc |
modified | |
EXPECT_TRUEui/aura/window_event_dispatcher_unittest.cc |
modified | |
TEST_Fui/aura/window_event_dispatcher_unittest.cc |
modified |
Files Changed
ui/aura/window_event_dispatcher.ccui/aura/window_event_dispatcher_unittest.cc
Audit Directions
- Bare consumer/target pointers across nested dispatchAudit other
WindowEventDispatcherandGestureRecognizerentry points (mouse-exit, scroll, capture changes) that resolve aGestureConsumer/Windowbefore or acrossDispatchHeldEvents()or any synchronous observer callout. - "dispatcher_destroyed but not target_destroyed" guardsSearch for code that checks only
details.dispatcher_destroyedafter a nested dispatch yet continues to use a separately-held target pointer. - Timer-driven gesture callbacksReview
GestureProviderAura/GestureRecognizerImpltimer callbacks that pass raw targets into dispatch, since window teardown (CleanupGestureState) runs synchronously during destruction.
Patch
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) {
Regression Test / PoC
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) {
Original Bug Report
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.ccui/events/gestures/gesture_recognizer_impl.ccui/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.
- Initiate a timer-based gesture (e.g., long-press) on a target window.
- Before the timer fires, trigger a layout change (e.g., viewport resize) to cause the
WindowEventDispatcherto hold pointer moves. - Send pointer move events, which are then aggregated into
held_move_event_. - When the long-press timer fires,
DispatchGestureEventis invoked with a bare pointer to the target window. DispatchGestureEventimmediately callsDispatchHeldEvents(), dispatching the held move event.- The move event handler executes and synchronously deletes the target window. The memory is immediately freed and reclaimed by the attacker with crafted data.
DispatchGestureEventresumes, dereferencing the dangling pointer, bypassing theContainscheck, and writing an observer pointer to an attacker-controlled address viastd::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.