CVE-2026-8513
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/input/input_router_impl.cc |
modified | |
ifcomponents/input/passthrough_touch_event_queue.cc |
modified | |
ifcomponents/input/passthrough_touch_event_queue_unittest.cc |
modified | |
TEST_Fcomponents/input/passthrough_touch_event_queue_unittest.cc |
modified | |
TEST_Fcontent/browser/renderer_host/input/input_router_impl_unittest.cc |
modified | |
ifcontent/browser/renderer_host/input/mock_input_disposition_handler.cc |
modified |
Files Changed
components/input/input_router_impl.cccomponents/input/passthrough_touch_event_queue.cccomponents/input/passthrough_touch_event_queue_unittest.cccontent/browser/renderer_host/input/input_router_impl_unittest.cccontent/browser/renderer_host/input/mock_input_disposition_handler.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.cccomponents/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.
- A compromised renderer sends unconsumed touch events (e.g.,
TouchStart,TouchMove,TouchEnd) simulating a long press. - The renderer sends an
InputEventAckto the browser, which is handled byPassthroughTouchEventQueue::AckCompletedEvents(). - The browser removes the first event from the set and calls
AckTouchEventToClient(), which synchronously bubbles up toRenderWidgetHostViewAura::ProcessAckedTouchEvent. - The unconsumed long press triggers a
kGestureLongPress, leading to a context menu request (RunContextMenu), which spins a nested message loop. - During this nested loop, the renderer sends an IPC (e.g.,
RequestClosePopupor a navigation request) that destroys theRenderWidgetHostImpland thePassthroughTouchEventQueue. - The renderer immediately sprays the browser heap with fake
std::setstructures to reclaim the freed memory chunk, setting a non-zero size and controlling the internal red-black tree node pointers. - The context menu closes, the nested loop exits, and execution returns to
AckCompletedEvents(). - The loop condition reads the attacker’s fake size (evaluating to true) and executes
outstanding_touches_.erase(iter). - The
std::set::eraserebalancing 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.