Critical watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
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
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.
Tracker495939973
Fix commit33e106371bd8 (chromium/src) +67/-7
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
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-04-27 (public git)
Disclosed2026-05-12
Patch gap15 days — fix public in source before the advisory

Background

`InputRouterImpl`
the browser-side object that routes input events to the renderer and processes their acknowledgments, calling into a disposition handler when a touch event is acked.
`PassthroughTouchEventQueue`
the queue that holds outstanding touch events and drains them, invoking client callbacks as each event is acknowledged.
`base::AutoReset` vs `base::WeakAutoReset`
AutoReset restores a member’s value in its destructor by writing through a raw pointer to that member, while WeakAutoReset first checks a WeakPtr and skips the restore if the owning object was destroyed.
Re-entrant destruction
a callback invoked mid-processing synchronously destroys the very object whose method is still on the stack, leaving later statements operating on freed memory.

Root Cause Analysis

When InputRouterImpl::OnTouchEventAck ran, it called disposition_handler_->OnTouchEventAck(...) and then continued to touch this (e.g. touch_action_filter_.DecreaseActiveTouches()); likewise PassthroughTouchEventQueue::FlushQueue and the other ack-draining method called AckTouchEventToClient(...)/client callbacks and then continued their loops and let a trailing base::AutoReset restore processing_acks_ through a raw member pointer. If the disposition handler or client synchronously destroyed the router/queue during the ack, every subsequent statement — including the AutoReset destructor writing back processing_acks_ — would dereference freed memory, a use-after-free. The commit is explicit that there is currently no active code path that destroys these objects while notifying an observer, so this is defense-in-depth hardening against a future re-entrant path.

The fix captures weak_ptr_factory_.GetWeakPtr() before the outbound call and returns early if the weak pointer is invalidated afterward, and swaps base::AutoReset for base::WeakAutoReset so the processing_acks_ restore is skipped when the queue has been freed.

Key insight
The code assumed an object remained alive across a synchronous observer/client notification and then kept using this (and let AutoReset write back a member) afterward; the fix adds WeakPtr liveness checks after each notification and replaces AutoReset with WeakAutoReset so no freed memory is accessed if the object self-destructs mid-ack.

Attack Path

  1. Queue a touch event Input reaches the browser and a touch event is enqueued in PassthroughTouchEventQueue / routed through InputRouterImpl, pending acknowledgment.
  2. Ack triggers a notification As the event is acked, OnTouchEventAck invokes the disposition handler (and the queue invokes its client), synchronously handing control to observer code.
  3. Re-entrant destruction (hypothetical path) That notified code destroys the InputRouterImpl/PassthroughTouchEventQueue (as the regression tests force via router->reset()/queue_.reset()), freeing the object whose method is still executing.
  4. Use-after-free on return Without the fix, control returns into the freed object’s method, which continues its loop and runs the base::AutoReset destructor writing through a dangling member pointer, reading/writing freed memory (further consequences are inferred; the diff shows only the unsafe continued access).

Impact Assessment

The patch hardens the browser-process input stack against a use-after-free caused by re-entrant destruction during touch-event acknowledgment; the vendor advisory rates it critical and labels it a use-after-free in Input. Notably the commit states no active path currently reaches this destruction, so triggering it in a stock build would require a future or auxiliary code path that destroys the router/queue mid-ack, and any memory-corruption escalation beyond the freed-memory access is (inferred).

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

Audit Directions

  • Member restore via raw pointer across callbacks
    Flag base::AutoReset (or manual scope-guard writes to a member) whose scope spans an outbound synchronous call that could destroy the object; these need base::WeakAutoReset or an early-out.
  • Use of `this` after observer/handler notification
    Audit methods that call into disposition_handler_, client_, or other observers and then keep touching member state; each such return point needs a WeakPtr liveness check.
  • Drain loops over member containers
    Review loops iterating outstanding_touches_-style members that invoke callbacks per element, ensuring the loop re-checks object liveness before the next iteration or final restore.
From 33e106371bd8720e519c6b7b8149a090e21ad37d Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
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 <jonross@chromium.org>
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
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 vm...@google.com

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