Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker495939973
Fix commit33e106371bd8 (chromium/src) +67/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
if
components/input/input_router_impl.cc
modified
if
components/input/passthrough_touch_event_queue.cc
modified
if
components/input/passthrough_touch_event_queue_unittest.cc
modified
TEST_F
components/input/passthrough_touch_event_queue_unittest.cc
modified
TEST_F
content/browser/renderer_host/input/input_router_impl_unittest.cc
modified
if
content/browser/renderer_host/input/mock_input_disposition_handler.cc
modified

Files Changed

  • components/input/input_router_impl.cc
  • components/input/passthrough_touch_event_queue.cc
  • components/input/passthrough_touch_event_queue_unittest.cc
  • content/browser/renderer_host/input/input_router_impl_unittest.cc
  • content/browser/renderer_host/input/mock_input_disposition_handler.cc
From 33e106371bd8720e519c6b7b8149a090e21ad37d Mon Sep 17 00:00:00 2001
From: Jonathan Ross <[email protected]>
Date: Mon, 27 Apr 2026 15:49:02 -0700
Subject: [PATCH] Harden InputRouterImpl and PassthroughTouchEventQueue

There is currently no active path to destroy InputRouterImpl or
PassthroughTouchEventQueue while they are notifying observer.

Here we harden against such a path being inadvertently introduced in
the future.

This change addresses a Use-After-Free (UAF) vulnerability where the
synchronous destruction of an InputRouterImpl during touch event
acknowledgment processing led to memory corruption.

The fix implements liveness checks using base::WeakPtr:
- In InputRouterImpl::OnTouchEventAck, we now verify the object's
  continued existence after the disposition handler call.
- In PassthroughTouchEventQueue, we've replaced base::AutoReset with
  WeakAutoReset and added WeakPtr checks within the ACK loop
  to detect re-entrant destruction.

Regression tests have been added to ensure that synchronous destruction
mid-ACK is handled safely without accessing freed memory.

Bug: 498776820, 495939973
Change-Id: I1c6a3de8dd30457154b9211e2b3dffffdda70091
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7776327
Commit-Queue: Jonathan Ross <[email protected]>
Reviewed-by: Kyle Charbonneau <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1621363}
---

diff --git a/components/input/input_router_impl.cc b/components/input/input_router_impl.cc
index cfafb85..a03a167c 100644
--- a/components/input/input_router_impl.cc
+++ b/components/input/input_router_impl.cc
@@ -518,7 +518,11 @@
   if (event.event.IsTouchSequenceStart()) {
     touch_action_filter_.IncreaseActiveTouches();
   }
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   disposition_handler_->OnTouchEventAck(event, ack_source, ack_result);
+  if (!weak_this) {
+    return;
+  }
 
   if (event.event.IsTouchSequenceEnd()) {
     touch_action_filter_.DecreaseActiveTouches();
diff --git a/components/input/passthrough_touch_event_queue.cc b/components/input/passthrough_touch_event_queue.cc
index b8a243c..01afd243 100644
--- a/components/input/passthrough_touch_event_queue.cc
+++ b/components/input/passthrough_touch_event_queue.cc
@@ -10,6 +10,7 @@
 
 #include "base/auto_reset.h"
 #include "base/feature_list.h"
+#include "base/memory/weak_auto_reset.h"
 #include "base/metrics/field_trial_params.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/trace_event/trace_event.h"
@@ -243,19 +244,26 @@
 void PassthroughTouchEventQueue::FlushQueue() {
   // Don't allow acks to be processed in AckCompletedEvents as that can
   // interfere with gesture event dispatch ordering.
-  base::AutoReset<bool> process_acks(&processing_acks_, true);
+  base::WeakAutoReset reset_processing_acks(
+      weak_ptr_factory_.GetWeakPtr(),
+      &PassthroughTouchEventQueue::processing_acks_, true);
+
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   drop_remaining_touches_in_sequence_ = true;
   client_->FlushDeferredGestureQueue();
-  base::WeakPtr<PassthroughTouchEventQueue> weak_this =
-      weak_ptr_factory_.GetWeakPtr();
+  if (!weak_this) {
+    return;
+  }
+
   while (!outstanding_touches_.empty()) {
     auto iter = outstanding_touches_.begin();
     TouchEventWithLatencyInfoAndAckState event = *iter;
     outstanding_touches_.erase(iter);
-    if (event.ack_state() == blink::mojom::InputEventResultState::kUnknown)
+    if (event.ack_state() == blink::mojom::InputEventResultState::kUnknown) {
       event.set_ack_info(
           blink::mojom::InputEventResultSource::kBrowser,
           blink::mojom::InputEventResultState::kNoConsumerExists);
+    }
     AckTouchEventToClient(event, event.ack_source(), event.ack_state());
     if (!weak_this) {
       return;  // Object was destroyed during the ACK, bail out safely.
@@ -283,9 +291,11 @@
     TRACE_EVENT_INSTANT("input", "ProcessingAcksAlready");
     return;
   }
-  base::AutoReset<bool> process_acks(&processing_acks_, true);
-  base::WeakPtr<PassthroughTouchEventQueue> weak_this =
-      weak_ptr_factory_.GetWeakPtr();
+  base::WeakAutoReset reset_processing_acks(
+      weak_ptr_factory_.GetWeakPtr(),
+      &PassthroughTouchEventQueue::processing_acks_, true);
+
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   while (!outstanding_touches_.empty()) {
     auto iter = outstanding_touches_.begin();
     if (iter->ack_state() == blink::mojom::InputEventResultState::kUnknown) {
diff --git a/components/input/passthrough_touch_event_queue_unittest.cc b/components/input/passthrough_touch_event_queue_unittest.cc
index 21c6e40..da816663 100644
--- a/components/input/passthrough_touch_event_queue_unittest.cc
+++ b/components/input/passthrough_touch_event_queue_unittest.cc
@@ -83,6 +83,10 @@
       blink::mojom::InputEventResultSource ack_source,
       blink::mojom::InputEventResultState ack_result) override {
     ++acked_event_count_;
+    if (destroy_queue_on_ack_) {
+      ResetQueue();
+      return;
+    }
     if (followup_touch_event_) {
       std::unique_ptr<WebTouchEvent> followup_touch_event =
           std::move(followup_touch_event_);
@@ -358,6 +362,9 @@
     queue_->OnHasTouchEventHandlers(true);
   }
 
+  void ResetQueue() { queue_.reset(); }
+
+ protected:
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<PassthroughTouchEventQueue> queue_;
   size_t acked_event_count_;
@@ -367,6 +374,7 @@
   SyntheticWebTouchEvent touch_event_;
   int event_id_for_scroll_ = -1;
   bool will_start_scrolling_on_touch_move_ack_ = false;
+  bool destroy_queue_on_ack_ = false;
   std::unique_ptr<WebTouchEvent> followup_touch_event_;
   std::unique_ptr<blink::mojom::InputEventResultState> sync_ack_result_;
   double slop_length_dips_;
@@ -2063,4 +2071,14 @@
   EXPECT_EQ(2U, GetAndResetSentEventCount());
 }
 
+TEST_F(PassthroughTouchEventQueueTest, SynchronousDestructionDuringAck) {
+  PressTouchPoint(1, 1);
+  EXPECT_EQ(1U, queued_event_count());
+
+  destroy_queue_on_ack_ = true;
+  SendTouchEventAck(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 0744a67d..c9bb9de 100644
--- a/content/browser/renderer_host/input/input_router_impl_unittest.cc
+++ b/content/browser/renderer_host/input/input_router_impl_unittest.cc
@@ -2942,4 +2942,24 @@
                        WebInputEvent::Type::kGestureTwoFingerTap});
 }
 
+TEST_F(InputRouterImplTest, SynchronousDestructionDuringAck) {
+  blink::SyntheticWebTouchEvent touch;
+  touch.PressPoint(1, 1);
+
+  // Set the disposition handler to destroy the input router during ACK.
+  disposition_handler_->set_on_touch_event_ack_closure(base::BindOnce(
+      [](std::unique_ptr<input::InputRouterImpl>* router) { router->reset(); },
+      &input_router_));
+
+  // Trigger an ACK.
+  input::TouchEventWithLatencyInfo touch_event(touch);
+  // OnTouchEventAck is private in InputRouterImpl but public in the interface.
+  static_cast<input::PassthroughTouchEventQueueClient*>(input_router_.get())
+      ->OnTouchEventAck(touch_event,
+                        blink::mojom::InputEventResultSource::kMainThread,
+                        blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, input_router_);
+}
+
 }  // namespace content
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 cfc8672..56c2588 100644
--- a/content/browser/renderer_host/input/mock_input_disposition_handler.cc
+++ b/content/browser/renderer_host/input/mock_input_disposition_handler.cc
@@ -61,6 +61,9 @@
     input_router_->SendGestureEvent(*gesture_followup_event_,
                                     dispatch_callback.callback);
   }
+  if (on_touch_event_ack_closure_) {
+    std::move(on_touch_event_ack_closure_).Run();
+  }
 }
 
 void MockInputDispositionHandler::OnGestureEventAck(
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 688154f..c2e5528 100644
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/input/passthrough_touch_event_queue_unittest.cc b/components/input/passthrough_touch_event_queue_unittest.cc
index 21c6e40..da816663 100644
--- a/components/input/passthrough_touch_event_queue_unittest.cc
+++ b/components/input/passthrough_touch_event_queue_unittest.cc
@@ -83,6 +83,10 @@
       blink::mojom::InputEventResultSource ack_source,
       blink::mojom::InputEventResultState ack_result) override {
     ++acked_event_count_;
+    if (destroy_queue_on_ack_) {
+      ResetQueue();
+      return;
+    }
     if (followup_touch_event_) {
       std::unique_ptr<WebTouchEvent> followup_touch_event =
           std::move(followup_touch_event_);
@@ -358,6 +362,9 @@
     queue_->OnHasTouchEventHandlers(true);
   }
 
+  void ResetQueue() { queue_.reset(); }
+
+ protected:
   base::test::SingleThreadTaskEnvironment task_environment_;
   std::unique_ptr<PassthroughTouchEventQueue> queue_;
   size_t acked_event_count_;
@@ -367,6 +374,7 @@
   SyntheticWebTouchEvent touch_event_;
   int event_id_for_scroll_ = -1;
   bool will_start_scrolling_on_touch_move_ack_ = false;
+  bool destroy_queue_on_ack_ = false;
   std::unique_ptr<WebTouchEvent> followup_touch_event_;
   std::unique_ptr<blink::mojom::InputEventResultState> sync_ack_result_;
   double slop_length_dips_;
@@ -2063,4 +2071,14 @@
   EXPECT_EQ(2U, GetAndResetSentEventCount());
 }
 
+TEST_F(PassthroughTouchEventQueueTest, SynchronousDestructionDuringAck) {
+  PressTouchPoint(1, 1);
+  EXPECT_EQ(1U, queued_event_count());
+
+  destroy_queue_on_ack_ = true;
+  SendTouchEventAck(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 0744a67d..c9bb9de 100644
--- a/content/browser/renderer_host/input/input_router_impl_unittest.cc
+++ b/content/browser/renderer_host/input/input_router_impl_unittest.cc
@@ -2942,4 +2942,24 @@
                        WebInputEvent::Type::kGestureTwoFingerTap});
 }
 
+TEST_F(InputRouterImplTest, SynchronousDestructionDuringAck) {
+  blink::SyntheticWebTouchEvent touch;
+  touch.PressPoint(1, 1);
+
+  // Set the disposition handler to destroy the input router during ACK.
+  disposition_handler_->set_on_touch_event_ack_closure(base::BindOnce(
+      [](std::unique_ptr<input::InputRouterImpl>* router) { router->reset(); },
+      &input_router_));
+
+  // Trigger an ACK.
+  input::TouchEventWithLatencyInfo touch_event(touch);
+  // OnTouchEventAck is private in InputRouterImpl but public in the interface.
+  static_cast<input::PassthroughTouchEventQueueClient*>(input_router_.get())
+      ->OnTouchEventAck(touch_event,
+                        blink::mojom::InputEventResultSource::kMainThread,
+                        blink::mojom::InputEventResultState::kConsumed);
+
+  EXPECT_EQ(nullptr, input_router_);
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in PassthroughTouchEventQueue during synchronous touch ACK processing

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential Use-After-Free (UAF) exists in PassthroughTouchEventQueue when processing touch event acknowledgments. If an ACK triggers a nested message loop (e.g., showing a context menu), a compromised renderer can synchronously destroy the queue’s owner, leading to an exploitable UAF when the stack unwinds.

Affected files:

  • components/input/passthrough_touch_event_queue.cc
  • components/input/passthrough_touch_event_queue.h

Estimated timestamp from git blame: 2024-01-22

Summary

A potential Use-After-Free (UAF) vulnerability exists in components/input/passthrough_touch_event_queue.cc within PassthroughTouchEventQueue::AckCompletedEvents() and FlushQueue().

These methods iterate over the outstanding_touches_ set (a std::set) and synchronously dispatch acknowledgments via AckTouchEventToClient(). If an unconsumed touch event sequence forms a gesture like a long press, dispatching the ACK can synchronously trigger UI actions, such as opening a context menu. On platforms like Windows or Aura, showing a native context menu spins a nested message loop on the UI thread.

While the call stack is blocked in this nested message loop, the browser process continues to process incoming IPCs. A compromised renderer can send an IPC to synchronously destroy the widget/WebContents. This cascades into the destruction of RenderWidgetHostImpl, RenderInputRouter, InputRouterImpl, and its inline PassthroughTouchEventQueue member.

When the nested loop eventually exits and the stack unwinds back to the while loop in AckCompletedEvents(), the this pointer is dangling. The loop continues to access the freed memory, specifically evaluating !outstanding_touches_.empty() and calling outstanding_touches_.erase(iter). Additionally, the base::AutoReset<bool> used in this function uses RAW_PTR_EXCLUSION internally, bypassing MiraclePtr protections and causing a secondary UAF write upon destruction.

Potential Exploitation Steps

Note: These are suggested steps, as this analysis is provided by an LLM agent without a working Proof of Concept.

  1. A compromised renderer sends unconsumed touch events (e.g., TouchStart, TouchMove, TouchEnd) simulating a long press.
  2. The renderer sends an InputEventAck to the browser, which is handled by PassthroughTouchEventQueue::AckCompletedEvents().
  3. The browser removes the first event from the set and calls AckTouchEventToClient(), which synchronously bubbles up to RenderWidgetHostViewAura::ProcessAckedTouchEvent.
  4. The unconsumed long press triggers a kGestureLongPress, leading to a context menu request (RunContextMenu), which spins a nested message loop.
  5. During this nested loop, the renderer sends an IPC (e.g., RequestClosePopup or a navigation request) that destroys the RenderWidgetHostImpl and the PassthroughTouchEventQueue.
  6. The renderer immediately sprays the browser heap with fake std::set structures to reclaim the freed memory chunk, setting a non-zero size and controlling the internal red-black tree node pointers.
  7. The context menu closes, the nested loop exits, and execution returns to AckCompletedEvents().
  8. The loop condition reads the attacker’s fake size (evaluating to true) and executes outstanding_touches_.erase(iter).
  9. The std::set::erase rebalancing algorithm operates on the attacker’s fake pointers, providing a highly reliable arbitrary memory write primitive in the browser process, potentially leading to a Sandbox Escape and Remote Code Execution (RCE).

Suggested Fix

The most robust fix is to ensure the PassthroughTouchEventQueue detects if it has been destroyed during the synchronous AckTouchEventToClient() call.

Add a base::WeakPtrFactory<PassthroughTouchEventQueue> to the class. In AckCompletedEvents() and FlushQueue(), obtain a WeakPtr before the loop and check it after the potentially re-entrant call:

base::WeakPtr<PassthroughTouchEventQueue> weak_this = weak_ptr_factory_.GetWeakPtr();
while (!outstanding_touches_.empty()) {
  auto iter = outstanding_touches_.begin();
  // ... (setup event) ...
  outstanding_touches_.erase(iter);
  AckTouchEventToClient(event, event.ack_source(), event.ack_state());
  if (!weak_this) {
    return; // Object was destroyed during the ACK, bail out safely.
  }
}

Additionally, consider auditing other callers of AckTouchEventToClient for similar synchronous destruction risks.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker