Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible (tier revised from patch-now)
The mapped commit may not contain the whole fix.
Reviewer notes: The commit is hardening that follows up two earlier CLs, so the core fix may be there. The trigger needs user gesture, wheel or pinch input plus a synchronous UI teardown (overscroll navigation, popup, context menu), so 'default' overstates it.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker554953456
Fix commit8e8909f79833 (chromium/src) +195/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedTIENPA
Reported2026-08-31 (per vendor advisory)
Fix landed2026-09-01 (public git)
Disclosed2026-09-15
Patch gap14 days — fix public in source before the advisory

Background

`InputRouterImpl`
a browser-process object, owned by RenderWidgetHostImpl, that routes input events to the renderer and processes their acknowledgements; it embeds the GestureEventQueue, MouseWheelEventQueue, and TouchpadPinchEventQueue.
ACK callback re-entrancy
when the browser processes an input-event ACK it synchronously invokes a client/disposition handler (OnGestureEventAck, OnWheelEventAck, OnGestureEventForPinchAck), which can trigger synchronous UI (overscroll navigation, popup dismissal, context-menu loops) that destroys the owning RenderWidgetHostImpl and therefore the queue itself.
`base::WeakPtrFactory` / `base::WeakAutoReset`
WeakPtrFactory hands out weak pointers that become null once the owning object is destroyed; WeakAutoReset is an AutoReset variant that only writes the saved value back if the weak pointer is still valid, avoiding a write into freed memory.
`base::Unretained(this)`
binds a raw, unchecked this into a callback, which invokes the method with no liveness check even if the object was freed before the callback runs.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: The commit is hardening that follows up two earlier CLs, so the core fix may be there. The trigger needs user gesture, wheel or pinch input plus a synchronous UI teardown (overscroll navigation, popup, context menu), so 'default' overstates it.

Each of the three input queues, and the two InputRouterImpl ACK entry points, invoked a client/disposition callback synchronously and then continued to touch this-owned state after that call returned. Because such a callback can synchronously destroy the RenderWidgetHostImpl that owns the InputRouterImpl and its embedded queues, the code that ran afterward — the loop re-check of sent_events_awaiting_ack_ in the gesture queue, disposition_handler_-> / gesture_event_queue_. accesses in InputRouterImpl, CanGenerateGestureScroll and pinch_event_awaiting_ack_.reset() in the wheel/pinch queues, and even the base::AutoReset<bool> destructor writing back processing_acks_ — dereferenced freed memory, a use-after-free. The invariant violated is that this (and every member it reaches through) must still be alive after a synchronous callback returns.

The fix captures a base::WeakPtr before each such callback and returns early with if (!weak_this) return; if the object was destroyed, swaps the raw AutoReset for a WeakAutoReset so the write-back is skipped on a dead object, and replaces base::Unretained(this) in the wheel/pinch dispatch callbacks with weak pointers so a queued ACK never runs against a freed queue.

Key insight
The queues assumed that a synchronous ACK callback could not destroy their owner, so they kept using this after the callback returned; the fix threads a WeakPtr liveness check across every post-callback access (and the AutoReset write-back and re-posted callbacks), bailing out the instant the object is gone.

Attack Path

  1. Queue a blocking input event A page arranges an input event (a gesture such as kGestureTapDown, a blocking mouse wheel, or a touchpad pinch) that the browser sends to the renderer and awaits an ACK for, leaving it in a queue’s awaiting-ack slot.
  2. Deliver an ACK that re-enters UI The renderer’s ACK is processed synchronously by the queue, which calls the client/disposition handler (OnGestureEventAck, OnMouseWheelEventAck, OnGestureEventForPinchAck); the handler performs a synchronous UI action (overscroll navigation, popup dismissal, context-menu loop) that tears down the owning RenderWidgetHostImpl / InputRouterImpl.
  3. Return into freed queue state Control returns from the callback into queue code that still dereferences freed members (the gesture loop’s sent_events_awaiting_ack_, InputRouterImpl’s disposition_handler_ / gesture_event_queue_, or the wheel/pinch queue’s pinch_event_awaiting_ack_), and the AutoReset destructor writes processing_acks_ back into freed memory — the use-after-free.
  4. Corrupt reclaimed memory (inferred) With heap grooming the freed queue can be replaced with attacker-controlled data before the stale accesses, turning the UAF into a controlled read/write in the browser process; the diff does not itself demonstrate this.

Impact Assessment

An attacker who can drive input-event ACK processing so that a handler synchronously destroys the RenderWidgetHostImpl gains a use-after-free of the input queues in the privileged browser process, the precondition being an input flow whose ACK triggers a synchronous UI teardown (overscroll navigation, popup dismissal, or context-menu loop). Successful heap grooming could escalate this to memory corruption and potentially sandbox-level code execution in the browser process (inferred). The advisory classifies this as a use-after-free in Input and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
if
components/input/gesture_event_queue.cc
modified
if
components/input/input_router_impl.cc
modified
if
components/input/mouse_wheel_event_queue.cc
modified
if
components/input/touchpad_pinch_event_queue.cc
modified
if
content/browser/renderer_host/input/mock_input_disposition_handler.cc
modified
if
components/input/gesture_event_queue_unittest.cc
modified
TEST_F
components/input/gesture_event_queue_unittest.cc
modified
if
components/input/mouse_wheel_event_queue_unittest.cc
modified
TEST_F
components/input/mouse_wheel_event_queue_unittest.cc
modified
if
components/input/touchpad_pinch_event_queue_unittest.cc
modified

Files Changed

  • components/input/gesture_event_queue.h
  • components/input/mouse_wheel_event_queue.cc
  • content/browser/renderer_host/input/mock_input_disposition_handler.cc
  • components/input/input_router_impl.cc
  • components/input/gesture_event_queue.cc
  • components/input/mouse_wheel_event_queue_unittest.cc
  • components/input/mouse_wheel_event_queue.h
  • content/browser/renderer_host/input/mock_input_disposition_handler.h
  • components/input/touchpad_pinch_event_queue.h
  • components/input/touchpad_pinch_event_queue_unittest.cc
  • content/browser/renderer_host/input/input_router_impl_unittest.cc
  • components/input/touchpad_pinch_event_queue.cc
  • components/input/gesture_event_queue_unittest.cc

Audit Directions

  • Post-callback member access
    Audit any browser-side handler that invokes a client, observer, or disposition callback synchronously and then reads or writes this-owned state; each such site needs a WeakPtr liveness check because the callback may destroy the owner.
  • `base::Unretained(this)` in re-postable callbacks
    Flag input/dispatch callbacks bound with base::Unretained(this) (or raw pointers) whose invocation is deferred past a point where the owning object can be destroyed; prefer WeakPtr.
  • Scoped write-back objects on destructible owners
    Look for base::AutoReset and similar RAII helpers that write back into a member during their destructor while a nested call could free the owner; these need the WeakAutoReset pattern to avoid a write into freed memory.
From 8e8909f79833d1f1ec59c3cde98ce29e5f97e201 Mon Sep 17 00:00:00 2001
From: Robert Flack <[email protected]>
Date: Tue, 01 Sep 2026 13:50:37 -0700
Subject: [PATCH] Harden GestureEventQueue, MouseWheelEventQueue, and TouchpadPinchEventQueue

Follow up to crrev.com/c/7776327 and crrev.com/c/7919026. This change
hardens the remaining input queues embedded in InputRouterImpl against
synchronous destruction during ACK processing if an ACK callback
triggers synchronous UI actions (e.g. overscroll navigation, popup
dismissals, or context menu loops) that destroy the owning
RenderWidgetHostImpl / InputRouterImpl.

Bug: 554953456, 552283275
Change-Id: I9b65eeeb1f9478275ae29bb52afc41de8c6011ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8328368
Commit-Queue: Robert Flack <[email protected]>
Reviewed-by: Dave Tapuska <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1690090}
---
diff --git a/components/input/gesture_event_queue.cc b/components/input/gesture_event_queue.cc
index 9444a4d..098fa35 100644
--- a/components/input/gesture_event_queue.cc
+++ b/components/input/gesture_event_queue.cc
@@ -6,6 +6,7 @@
 
 #include "base/auto_reset.h"
 #include "base/logging.h"
+#include "base/memory/weak_auto_reset.h"
 #include "base/trace_event/trace_event.h"
 #include "components/input/touchpad_tap_suppression_controller.h"
 #include "components/input/touchscreen_tap_suppression_controller.h"
@@ -223,7 +224,10 @@
   if (processing_acks_) {
     return;
   }
-  base::AutoReset<bool> process_acks(&processing_acks_, true);
+  base::WeakAutoReset reset_processing_acks(
+      weak_ptr_factory_.GetWeakPtr(), &GestureEventQueue::processing_acks_,
+      true);
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   while (!sent_events_awaiting_ack_.empty()) {
     auto iter = sent_events_awaiting_ack_.begin();
     if (iter->ack_state() == blink::mojom::InputEventResultState::kUnknown) {
@@ -233,6 +237,9 @@
     sent_events_awaiting_ack_.erase(iter);
 
     AckGestureEventToClient(event, event.ack_source(), event.ack_state());
+    if (!weak_this) {
+      return;
+    }
   }
 }
 
diff --git a/components/input/gesture_event_queue.h b/components/input/gesture_event_queue.h
index 7a610d0..db528c9 100644
--- a/components/input/gesture_event_queue.h
+++ b/components/input/gesture_event_queue.h
@@ -10,6 +10,7 @@
 #include "base/component_export.h"
 #include "base/containers/circular_deque.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "components/input/dispatch_to_renderer_callback.h"
@@ -246,6 +247,8 @@
   // True when the last GSE event is either in the debouncing_deferral_queue_ or
   // pushed to the queue and dropped from it later on.
   bool scroll_end_filtered_by_deboucing_deferral_queue_ = false;
+
+  base::WeakPtrFactory<GestureEventQueue> weak_ptr_factory_{this};
 };
 
 }  // namespace input
diff --git a/components/input/input_router_impl.cc b/components/input/input_router_impl.cc
index 2d3de8fb..51baeda2 100644
--- a/components/input/input_router_impl.cc
+++ b/components/input/input_router_impl.cc
@@ -553,7 +553,11 @@
     const GestureEventWithLatencyInfo& event,
     blink::mojom::InputEventResultSource ack_source,
     blink::mojom::InputEventResultState ack_result) {
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   touch_event_queue_.OnGestureEventAck(event, ack_result);
+  if (!weak_this) {
+    return;
+  }
   disposition_handler_->OnGestureEventAck(event, ack_source, ack_result);
 }
 
@@ -588,7 +592,11 @@
     const MouseWheelEventWithLatencyInfo& event,
     blink::mojom::InputEventResultSource ack_source,
     blink::mojom::InputEventResultState ack_result) {
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   disposition_handler_->OnWheelEventAck(event, ack_source, ack_result);
+  if (!weak_this) {
+    return;
+  }
   gesture_event_queue_.OnWheelEventAck(event, ack_source, ack_result);
 }
 
diff --git a/components/input/mouse_wheel_event_queue.cc b/components/input/mouse_wheel_event_queue.cc
index f5468fd..8a8dea1 100644
--- a/components/input/mouse_wheel_event_queue.cc
+++ b/components/input/mouse_wheel_event_queue.cc
@@ -132,8 +132,12 @@
     return;
 
   event_sent_for_gesture_ack_->latency.AddNewLatencyFrom(ack_event.latency);
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   client_->OnMouseWheelEventAck(*event_sent_for_gesture_ack_, ack_source,
                                 ack_result);
+  if (!weak_this) {
+    return;
+  }
 
   // If event wasn't consumed then generate a gesture scroll for it.
   if (CanGenerateGestureScroll(ack_result)) {
@@ -311,7 +315,7 @@
   client_->SendMouseWheelEventImmediately(
       *event_sent_for_gesture_ack_,
       base::BindOnce(&MouseWheelEventQueue::ProcessMouseWheelAck,
-                     base::Unretained(this)),
+                     weak_ptr_factory_.GetWeakPtr()),
       event_sent_for_gesture_ack_->dispatch_callback);
 }
 
diff --git a/components/input/mouse_wheel_event_queue.h b/components/input/mouse_wheel_event_queue.h
index d7c13d1..e6d136d9 100644
--- a/components/input/mouse_wheel_event_queue.h
+++ b/components/input/mouse_wheel_event_queue.h
@@ -11,6 +11,7 @@
 #include "base/containers/circular_deque.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "components/input/dispatch_to_renderer_callback.h"
 #include "components/input/event_with_latency_info.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
@@ -121,6 +122,8 @@
   bool send_wheel_events_async_;
 
   blink::WebGestureDevice scrolling_device_;
+
+  base::WeakPtrFactory<MouseWheelEventQueue> weak_ptr_factory_{this};
 };
 
 }  // namespace input
diff --git a/components/input/touchpad_pinch_event_queue.cc b/components/input/touchpad_pinch_event_queue.cc
index af8e12f..c5f2c17 100644
--- a/components/input/touchpad_pinch_event_queue.cc
+++ b/components/input/touchpad_pinch_event_queue.cc
@@ -148,8 +148,12 @@
         (ack_result == blink::mojom::InputEventResultState::kConsumed);
 
   pinch_event_awaiting_ack_->latency.AddNewLatencyFrom(ack_event.latency);
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   client_->OnGestureEventForPinchAck(*pinch_event_awaiting_ack_, ack_source,
                                      ack_result);
+  if (!weak_this) {
+    return;
+  }
 
   pinch_event_awaiting_ack_.reset();
   TryForwardNextEventToRenderer();
@@ -167,13 +171,22 @@
 
   if (pinch_event_awaiting_ack_->event.GetType() ==
       blink::WebInputEvent::Type::kGesturePinchBegin) {
+    auto weak_this = weak_ptr_factory_.GetWeakPtr();
+    auto dispatch_callback =
+        std::move(pinch_event_awaiting_ack_->dispatch_callback);
+    const blink::WebGestureEvent event = pinch_event_awaiting_ack_->event;
     client_->OnGestureEventForPinchAck(
         *pinch_event_awaiting_ack_,
         blink::mojom::InputEventResultSource::kBrowser,
         blink::mojom::InputEventResultState::kIgnored);
-    std::move(pinch_event_awaiting_ack_->dispatch_callback)
-        .Run(pinch_event_awaiting_ack_->event,
-             DispatchToRendererResult::kNotDispatched);
+    if (!weak_this) {
+      return;
+    }
+    std::move(dispatch_callback)
+        .Run(event, DispatchToRendererResult::kNotDispatched);
+    if (!weak_this) {
+      return;
+    }
     pinch_event_awaiting_ack_.reset();
     TryForwardNextEventToRenderer();
     return;
@@ -215,7 +228,7 @@
   client_->SendMouseWheelEventForPinchImmediately(
       pinch_event_awaiting_ack_->event, synthetic_wheel,
       base::BindOnce(&TouchpadPinchEventQueue::ProcessMouseWheelAck,
-                     base::Unretained(this)),
+                     weak_ptr_factory_.GetWeakPtr()),
       pinch_event_awaiting_ack_->dispatch_callback);
 }
 
diff --git a/components/input/touchpad_pinch_event_queue.h b/components/input/touchpad_pinch_event_queue.h
index b6a559b..d3c32cd3 100644
--- a/components/input/touchpad_pinch_event_queue.h
+++ b/components/input/touchpad_pinch_event_queue.h
@@ -11,6 +11,7 @@
 #include "base/component_export.h"
 #include "base/containers/circular_deque.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "components/input/dispatch_to_renderer_callback.h"
 #include "components/input/event_with_latency_info.h"
 #include "third_party/blink/public/common/input/web_input_event.h"
@@ -80,6 +81,8 @@
   base::circular_deque<std::unique_ptr<QueuedTouchpadPinchEvent>> pinch_queue_;
   std::unique_ptr<QueuedTouchpadPinchEvent> pinch_event_awaiting_ack_;
   std::optional<bool> first_event_prevented_;
+
+  base::WeakPtrFactory<TouchpadPinchEventQueue> weak_ptr_factory_{this};
 };
 
 }  // namespace input
diff --git a/content/browser/renderer_host/input/mock_input_disposition_handler.cc b/content/browser/renderer_host/input/mock_input_disposition_handler.cc
index 56c2588..9dd1908 100644
--- a/content/browser/renderer_host/input/mock_input_disposition_handler.cc
+++ b/content/browser/renderer_host/input/mock_input_disposition_handler.cc
@@ -40,6 +40,9 @@
   acked_wheel_event_ = event.event;
   acked_wheel_event_state_ = ack_result;
   RecordAckCalled(event.event.GetType(), ack_result);
+  if (on_wheel_event_ack_closure_) {
+    std::move(on_wheel_event_ack_closure_).Run();
+  }
 }
 
 void MockInputDispositionHandler::OnTouchEventAck(
@@ -73,6 +76,9 @@
   VLOG(1) << __FUNCTION__ << " called!";
   acked_gesture_event_ = event.event;
   RecordAckCalled(event.event.GetType(), ack_result);
+  if (on_gesture_event_ack_closure_) {
+    std::move(on_gesture_event_ack_closure_).Run();
+  }
 }
 
 size_t MockInputDispositionHandler::GetAndResetAckCount() {
diff --git a/content/browser/renderer_host/input/mock_input_disposition_handler.h b/content/browser/renderer_host/input/mock_input_disposition_handler.h
index c2e5528..5c3d709 100644
--- a/content/browser/renderer_host/input/mock_input_disposition_handler.h
+++ b/content/browser/renderer_host/input/mock_input_disposition_handler.h
@@ -57,6 +57,14 @@
     on_touch_event_ack_closure_ = std::move(closure);
   }
 
+  void set_on_gesture_event_ack_closure(base::OnceClosure closure) {
+    on_gesture_event_ack_closure_ = std::move(closure);
+  }
+
+  void set_on_wheel_event_ack_closure(base::OnceClosure closure) {
+    on_wheel_event_ack_closure_ = std::move(closure);
+  }
+
   blink::mojom::InputEventResultState ack_state() const { return ack_state_; }
 
   blink::mojom::InputEventResultState acked_wheel_event_state() const {
@@ -105,6 +113,8 @@
   std::unique_ptr<input::GestureEventWithLatencyInfo> gesture_followup_event_;
   std::unique_ptr<input::TouchEventWithLatencyInfo> touch_followup_event_;
   base::OnceClosure on_touch_event_ack_closure_;
+  base::OnceClosure on_gesture_event_ack_closure_;
+  base::OnceClosure on_wheel_event_ack_closure_;
 };
 
 }  // namespace content
diff --git a/components/input/gesture_event_queue_unittest.cc b/components/input/gesture_event_queue_unittest.cc
index 72b7f345..8ff53f8 100644
--- a/components/input/gesture_event_queue_unittest.cc
+++ b/components/input/gesture_event_queue_unittest.cc
@@ -88,6 +88,10 @@
       blink::mojom::InputEventResultState ack_result) override {
     ++acked_gesture_event_count_;
     last_acked_event_ = event.event;
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+      return;
+    }
     if (sync_followup_event_) {
       auto sync_followup_event = std::move(sync_followup_event_);
       SimulateGestureEvent(*sync_followup_event);
@@ -242,6 +246,10 @@
 
   GestureEventQueue* queue() const { return queue_.get(); }
 
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
+  }
+
  private:
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<GestureEventQueue> queue_;
@@ -250,6 +258,7 @@
   WebGestureEvent last_acked_event_;
   std::unique_ptr<blink::mojom::InputEventResultState> sync_ack_result_;
   std::unique_ptr<WebGestureEvent> sync_followup_event_;
+  bool destroy_queue_on_ack_ = false;
   base::test::ScopedFeatureList feature_list_;
 };
 
@@ -644,4 +653,16 @@
   EXPECT_EQ(0U, GetAndResetSentGestureEventCount());
 }
 
+TEST_F(GestureEventQueueTest, SynchronousDestructionDuringAck) {
+  SimulateGestureEvent(WebInputEvent::Type::kGestureTapDown,
+                       blink::WebGestureDevice::kTouchscreen);
+  EXPECT_EQ(1U, GestureEventQueueSize());
+
+  set_destroy_queue_on_ack(true);
+  SendInputEventACK(WebInputEvent::Type::kGestureTapDown,
+                    blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, queue());
+}
+
 }  // namespace input
diff --git a/components/input/mouse_wheel_event_queue_unittest.cc b/components/input/mouse_wheel_event_queue_unittest.cc
index 901e4a0e..ed28ebb 100644
--- a/components/input/mouse_wheel_event_queue_unittest.cc
+++ b/components/input/mouse_wheel_event_queue_unittest.cc
@@ -198,6 +198,10 @@
     ++acked_event_count_;
     last_acked_event_ = event.event;
     last_acked_event_state_ = ack_result;
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+      return;
+    }
   }
 
   bool IsWheelScrollInProgress() override {
@@ -402,6 +406,10 @@
     EXPECT_EQ(1U, GetAndResetSentEventCount());
   }
 
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
+  }
+
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<MouseWheelEventQueue> queue_;
   std::vector<std::unique_ptr<WebInputEvent>> sent_events_;
@@ -409,6 +417,7 @@
   size_t acked_event_count_;
   blink::mojom::InputEventResultState last_acked_event_state_;
   WebMouseWheelEvent last_acked_event_;
+  bool destroy_queue_on_ack_ = false;
 
  private:
   bool is_wheel_scroll_in_progress_ = false;
@@ -709,4 +718,18 @@
   EXPECT_EQ(2U, GetAndResetSentEventCount());
 }
 #endif
+TEST_F(MouseWheelEventQueueTest, SynchronousDestructionDuringAck) {
+  SendMouseWheel(kWheelScrollX, kWheelScrollY, kWheelScrollGlobalX,
+                 kWheelScrollGlobalY, 1, 1, 0, false,
+                 WebMouseWheelEvent::kPhaseBegan,
+                 WebMouseWheelEvent::kPhaseNone);
+  EXPECT_EQ(0U, queued_event_count());
+  EXPECT_TRUE(event_in_flight());
+
+  set_destroy_queue_on_ack(true);
+  SendMouseWheelEventAck(blink::mojom::InputEventResultState::kNotConsumed);
+
+  EXPECT_EQ(nullptr, queue_);
+}
+
 }  // namespace input
diff --git a/components/input/touchpad_pinch_event_queue_unittest.cc b/components/input/touchpad_pinch_event_queue_unittest.cc
index 49e5d9e..3ce1231 100644
--- a/components/input/touchpad_pinch_event_queue_unittest.cc
+++ b/components/input/touchpad_pinch_event_queue_unittest.cc
@@ -127,11 +127,19 @@
       blink::mojom::InputEventResultSource ack_source,
       blink::mojom::InputEventResultState ack_result) override {
     mock_client_.OnGestureEventForPinchAck(event, ack_source, ack_result);
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+    }
+  }
+
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
   }
 
   testing::StrictMock<MockTouchpadPinchEventQueueClient> mock_client_;
   std::unique_ptr<TouchpadPinchEventQueue> queue_;
   base::circular_deque<HandleEventCallback> callbacks_;
+  bool destroy_queue_on_ack_ = false;
 };
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/input/gesture_event_queue_unittest.cc b/components/input/gesture_event_queue_unittest.cc
index 72b7f345..8ff53f8 100644
--- a/components/input/gesture_event_queue_unittest.cc
+++ b/components/input/gesture_event_queue_unittest.cc
@@ -88,6 +88,10 @@
       blink::mojom::InputEventResultState ack_result) override {
     ++acked_gesture_event_count_;
     last_acked_event_ = event.event;
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+      return;
+    }
     if (sync_followup_event_) {
       auto sync_followup_event = std::move(sync_followup_event_);
       SimulateGestureEvent(*sync_followup_event);
@@ -242,6 +246,10 @@
 
   GestureEventQueue* queue() const { return queue_.get(); }
 
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
+  }
+
  private:
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<GestureEventQueue> queue_;
@@ -250,6 +258,7 @@
   WebGestureEvent last_acked_event_;
   std::unique_ptr<blink::mojom::InputEventResultState> sync_ack_result_;
   std::unique_ptr<WebGestureEvent> sync_followup_event_;
+  bool destroy_queue_on_ack_ = false;
   base::test::ScopedFeatureList feature_list_;
 };
 
@@ -644,4 +653,16 @@
   EXPECT_EQ(0U, GetAndResetSentGestureEventCount());
 }
 
+TEST_F(GestureEventQueueTest, SynchronousDestructionDuringAck) {
+  SimulateGestureEvent(WebInputEvent::Type::kGestureTapDown,
+                       blink::WebGestureDevice::kTouchscreen);
+  EXPECT_EQ(1U, GestureEventQueueSize());
+
+  set_destroy_queue_on_ack(true);
+  SendInputEventACK(WebInputEvent::Type::kGestureTapDown,
+                    blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, queue());
+}
+
 }  // namespace input
diff --git a/components/input/mouse_wheel_event_queue_unittest.cc b/components/input/mouse_wheel_event_queue_unittest.cc
index 901e4a0e..ed28ebb 100644
--- a/components/input/mouse_wheel_event_queue_unittest.cc
+++ b/components/input/mouse_wheel_event_queue_unittest.cc
@@ -198,6 +198,10 @@
     ++acked_event_count_;
     last_acked_event_ = event.event;
     last_acked_event_state_ = ack_result;
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+      return;
+    }
   }
 
   bool IsWheelScrollInProgress() override {
@@ -402,6 +406,10 @@
     EXPECT_EQ(1U, GetAndResetSentEventCount());
   }
 
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
+  }
+
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<MouseWheelEventQueue> queue_;
   std::vector<std::unique_ptr<WebInputEvent>> sent_events_;
@@ -409,6 +417,7 @@
   size_t acked_event_count_;
   blink::mojom::InputEventResultState last_acked_event_state_;
   WebMouseWheelEvent last_acked_event_;
+  bool destroy_queue_on_ack_ = false;
 
  private:
   bool is_wheel_scroll_in_progress_ = false;
@@ -709,4 +718,18 @@
   EXPECT_EQ(2U, GetAndResetSentEventCount());
 }
 #endif
+TEST_F(MouseWheelEventQueueTest, SynchronousDestructionDuringAck) {
+  SendMouseWheel(kWheelScrollX, kWheelScrollY, kWheelScrollGlobalX,
+                 kWheelScrollGlobalY, 1, 1, 0, false,
+                 WebMouseWheelEvent::kPhaseBegan,
+                 WebMouseWheelEvent::kPhaseNone);
+  EXPECT_EQ(0U, queued_event_count());
+  EXPECT_TRUE(event_in_flight());
+
+  set_destroy_queue_on_ack(true);
+  SendMouseWheelEventAck(blink::mojom::InputEventResultState::kNotConsumed);
+
+  EXPECT_EQ(nullptr, queue_);
+}
+
 }  // namespace input
diff --git a/components/input/touchpad_pinch_event_queue_unittest.cc b/components/input/touchpad_pinch_event_queue_unittest.cc
index 49e5d9e..3ce1231 100644
--- a/components/input/touchpad_pinch_event_queue_unittest.cc
+++ b/components/input/touchpad_pinch_event_queue_unittest.cc
@@ -127,11 +127,19 @@
       blink::mojom::InputEventResultSource ack_source,
       blink::mojom::InputEventResultState ack_result) override {
     mock_client_.OnGestureEventForPinchAck(event, ack_source, ack_result);
+    if (destroy_queue_on_ack_) {
+      queue_.reset();
+    }
+  }
+
+  void set_destroy_queue_on_ack(bool destroy) {
+    destroy_queue_on_ack_ = destroy;
   }
 
   testing::StrictMock<MockTouchpadPinchEventQueueClient> mock_client_;
   std::unique_ptr<TouchpadPinchEventQueue> queue_;
   base::circular_deque<HandleEventCallback> callbacks_;
+  bool destroy_queue_on_ack_ = false;
 };
 
 MATCHER_P(EventHasType,
@@ -630,4 +638,42 @@
                     blink::mojom::InputEventResultState::kIgnored);
 }
 
+TEST_F(TouchpadPinchEventQueueTest, SynchronousDestructionDuringPinchBeginAck) {
+  set_destroy_queue_on_ack(true);
+  EXPECT_CALL(mock_client_,
+              OnGestureEventForPinchAck(
+                  EventHasType(blink::WebInputEvent::Type::kGesturePinchBegin),
+                  blink::mojom::InputEventResultSource::kBrowser,
+                  blink::mojom::InputEventResultState::kIgnored));
+  QueuePinchBegin();
+  EXPECT_EQ(nullptr, queue_);
+}
+
+TEST_F(TouchpadPinchEventQueueTest,
+       SynchronousDestructionDuringPinchUpdateAck) {
+  ::testing::InSequence sequence;
+  EXPECT_CALL(mock_client_,
+              OnGestureEventForPinchAck(
+                  EventHasType(blink::WebInputEvent::Type::kGesturePinchBegin),
+                  blink::mojom::InputEventResultSource::kBrowser,
+                  blink::mojom::InputEventResultState::kIgnored));
+  EXPECT_CALL(mock_client_,
+              SendMouseWheelEventForPinchImmediately(
+                  ::testing::AllOf(EventHasCtrlModifier(), EventIsBlocking())));
+  EXPECT_CALL(mock_client_,
+              OnGestureEventForPinchAck(
+                  EventHasType(blink::WebInputEvent::Type::kGesturePinchUpdate),
+                  blink::mojom::InputEventResultSource::kMainThread,
+                  blink::mojom::InputEventResultState::kConsumed));
+
+  QueuePinchBegin();
+  QueuePinchUpdate(1.23, false);
+
+  set_destroy_queue_on_ack(true);
+  SendWheelEventAck(blink::mojom::InputEventResultSource::kMainThread,
+                    blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, queue_);
+}
+
 }  // namespace input
diff --git a/content/browser/renderer_host/input/input_router_impl_unittest.cc b/content/browser/renderer_host/input/input_router_impl_unittest.cc
index 72cff09..9dcc7ec 100644
--- a/content/browser/renderer_host/input/input_router_impl_unittest.cc
+++ b/content/browser/renderer_host/input/input_router_impl_unittest.cc
@@ -2957,4 +2957,46 @@
   EXPECT_EQ(nullptr, input_router_);
 }
 
+TEST_F(InputRouterImplTest, SynchronousDestructionDuringGestureAck) {
+  blink::WebGestureEvent gesture =
+      blink::SyntheticWebGestureEventBuilder::Build(
+          WebInputEvent::Type::kGestureTapDown,
+          blink::WebGestureDevice::kTouchscreen);
+
+  // Set the disposition handler to destroy the input router during ACK.
+  disposition_handler_->set_on_gesture_event_ack_closure(base::BindOnce(
+      [](std::unique_ptr<input::InputRouterImpl>* router) { router->reset(); },
+      &input_router_));
+
+  // Trigger an ACK.
+  input::GestureEventWithLatencyInfo gesture_event(gesture);
+  static_cast<input::GestureEventQueueClient*>(input_router_.get())
+      ->OnGestureEventAck(gesture_event,
+                          blink::mojom::InputEventResultSource::kMainThread,
+                          blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, input_router_);
+}
+
+TEST_F(InputRouterImplTest, SynchronousDestructionDuringWheelAck) {
+  blink::WebMouseWheelEvent wheel =
+      blink::SyntheticWebMouseWheelEventBuilder::Build(
+          10, 10, 50, 50, 1, 1, 0,
+          ui::ScrollGranularity::kScrollByPrecisePixel);
+
+  // Set the disposition handler to destroy the input router during ACK.
+  disposition_handler_->set_on_wheel_event_ack_closure(base::BindOnce(
+      [](std::unique_ptr<input::InputRouterImpl>* router) { router->reset(); },
+      &input_router_));
+
+  // Trigger an ACK.
+  input::MouseWheelEventWithLatencyInfo wheel_event(wheel);
+  static_cast<input::MouseWheelEventQueueClient*>(input_router_.get())
+      ->OnMouseWheelEventAck(wheel_event,
+                             blink::mojom::InputEventResultSource::kMainThread,
+                             blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, input_router_);
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.