High patch-now ✔︎ verified CVSS 8.3 chrome UAF 🔧 Commit mapped 🧬 A nested callout frees the object, then it is used

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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 AIConfirmed
Reviewer notes: Diff matches: WeakPtr guards, WeakAutoReset, Unretained->WeakPtr rebinds across ACK callbacks. Reachability needs real input events plus a callback that synchronously destroys the host, so web only partly drives it.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker552283275
Fix commit8e8909f79833 (chromium/src) +195/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedHafiizh
Reported2026-08-25 · bounty $1,500 (per vendor advisory)
Fix landed2026-09-01 (public git)
Disclosed2026-09-15
Patch gap14 days — fix public in source before the advisory

Background

`InputRouterImpl`
the browser-process object that routes input events to the renderer and owns the embedded per-type queues (GestureEventQueue, MouseWheelEventQueue, TouchpadPinchEventQueue).
Input event ACK
the acknowledgement for a dispatched input event (returned by the renderer or synthesized by the browser) whose processing invokes client and disposition-handler callbacks.
`base::WeakPtr` / `base::WeakAutoReset`
a weak reference that is invalidated when its referent is destroyed; base::WeakAutoReset restores a member’s prior value on scope exit only if the object still exists (unlike base::AutoReset<bool>, which writes back unconditionally).
Reentrant destruction
an ACK callback can synchronously run UI actions (overscroll navigation, popup dismissal, context-menu loop) that destroy the owning RenderWidgetHostImpl / InputRouterImpl, and therefore the queue, in the middle of a queue method.

Root Cause Analysis

Each of the three queues, and InputRouterImpl itself, processed an ACK by invoking a client or disposition-handler callback and then continued to dereference this members afterward. Those callbacks can reenter and synchronously destroy the owning RenderWidgetHostImpl / InputRouterImpl — and thus the queue — so the subsequent member accesses (e.g. sent_events_awaiting_ack_.erase, gesture_event_queue_.OnWheelEventAck, pinch_event_awaiting_ack_.reset(), TryForwardNextEventToRenderer()) operate on freed memory, a use-after-free in the browser process. In the gesture-ack drain loop the danger was compounded by base::AutoReset<bool> process_acks(&processing_acks_, true), whose destructor writes back to processing_acks_ after the object is gone.

The fix captures a base::WeakPtr (via a newly added weak_ptr_factory_) before each callback and early-returns when the weak pointer is invalidated, replaces base::AutoReset with base::WeakAutoReset so the write-back is skipped after destruction, and rebinds the pending-ack callbacks from base::Unretained(this) to weak_ptr_factory_.GetWeakPtr(). Because the guard runs before every post-callback member access, the freed this is never touched.

Key insight
The code assumed the queue / router always outlived every ACK callback, but a callback can synchronously free it; the fix detects that destruction with a base::WeakPtr (and a base::WeakAutoReset) and bails out before dereferencing the freed this.

Attack Path

  1. Deliver a blocking input event The attacker page induces a gesture, mouse-wheel, or touchpad-pinch event that the browser dispatches to the renderer and tracks while awaiting an ACK.
  2. ACK invokes a reentrant UI action When the ACK is processed, a client/disposition callback (AckGestureEventToClient, OnWheelEventAck, OnGestureEventForPinchAck, OnGestureEventAck) synchronously runs a UI action — overscroll navigation, popup dismissal, or a context-menu loop — that destroys the owning RenderWidgetHostImpl / InputRouterImpl and its queues.
  3. Post-callback access on freed object Control returns into the queue/router, which (pre-patch) continues touching freed members or lets base::AutoReset write back to processing_acks_, dereferencing the freed this — the use-after-free.
  4. Exploitation (inferred) grooming the freed queue/router allocation so it is reused by attacker-controlled data could turn the dangling access into a browser-process read/write primitive; the patch itself does not demonstrate a specific sink, offset, or field.

Impact Assessment

An attacker who can deliver input events and trigger a synchronous destroy path gains a use-after-free of an InputRouterImpl-owned queue in the privileged browser process, where reused memory can corrupt live state. Escalation to arbitrary read/write, sandbox escape, or code execution is (inferred) and not demonstrated by the diff. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.

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

  • Callback-then-member-access
    audit any browser-process code that invokes an interface/client/observer callback and then reads or writes this state, since such callbacks can reenter and destroy the owning object; require a base::WeakPtr guard between the call and the access.
  • `base::Unretained(this)` on async/pending callbacks
    flag base::BindOnce(..., base::Unretained(this)) for callbacks whose invocation is not lifetime-bound to the object (e.g. renderer ACKs), and prefer weak_ptr_factory_.GetWeakPtr().
  • `base::AutoReset` across reentrant calls
    check scopes where a base::AutoReset<bool> reentrancy guard spans a callback that may free the object, as its destructor writes to a freed member; base::WeakAutoReset is the safe replacement.
From 8e8909f79833d1f1ec59c3cde98ce29e5f97e201 Mon Sep 17 00:00:00 2001
From: Robert Flack <flackr@chromium.org>
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 <flackr@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
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.