CVE-2026-9997
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/input/render_widget_host_input_event_router.cc |
modified | |
forcontent/browser/renderer_host/render_widget_host_view_aura.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_aura.cc |
modified | |
ViewDestroyingPreTargetHandlercontent/browser/renderer_host/render_widget_host_view_aura_browsertest.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_aura_browsertest.cc |
modified |
Files Changed
components/input/render_widget_host_input_event_router.cccontent/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
Patch
From 133561714bafa5045d195ce187409d99d9a1db08 Mon Sep 17 00:00:00 2001
From: Kartar Singh <kartarsingh@google.com>
Date: Mon, 18 May 2026 22:58:17 -0700
Subject: [PATCH] Fix Use-After-Free and dangling pointer in TouchEventAckQueue
This CL resolves two critical safety issues in the input event pipeline:
1. Use-After-Free in RenderWidgetHostViewAura::ProcessAckedTouchEvent:
During synchronous gesture event dispatch triggered via
ProcessedTouchEvent, window or focus observers can synchronously
destroy the WebContents and RWHVA view. This leads to a
heap-use-after-free when we subsequently dereference the freed view
object (via the host() helper). We guard this with a base::WeakPtr
check immediately following synchronous dispatch.
2. Dangling raw_ptr in TouchEventAckQueue:
- In TouchEventAckQueue::ProcessAckedTouchEvents, the pending AckData
(holding raw_ptrs to target_view and root_view) was copied to a
local stack variable. During synchronous view destruction, these
raw_ptrs became dangling and crashed when the stack variable went
out of scope. We resolve this by popping from the queue
immediately and extracting bare pointers/scalars before
synchronous calls.
- In TouchEventAckQueue::UpdateQueueAfterTargetDestroyed, when
target_view is being destroyed, the raw_ptr references from
TouchEventAckQueue::ack_queue_ are removed to avoid dangling
raw_ptr references.
Bug: 513324041
Change-Id: I8651da1d0311c95401087d1df3deeb028884c640
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852409
Commit-Queue: Kartar Singh <kartarsingh@google.com>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632634}
---
diff --git a/components/input/render_widget_host_input_event_router.cc b/components/input/render_widget_host_input_event_router.cc
index f0939b21..1c8ad393 100644
--- a/components/input/render_widget_host_input_event_router.cc
+++ b/components/input/render_widget_host_input_event_router.cc
@@ -189,18 +189,20 @@
client_->GetTouchEmulator(/*create_if_necessary=*/false);
while (!ack_queue_.empty() && ack_queue_.front().touch_event_ack_status ==
TouchEventAckStatus::TouchEventAcked) {
- TouchEventAckQueue::AckData ack_data = ack_queue_.front();
+ // Extract values and bare pointers to avoid holding raw_ptrs on the stack
+ // across synchronous view destruction boundaries.
+ TouchEventWithLatencyInfo touch_event = ack_queue_.front().touch_event;
+ blink::mojom::InputEventResultState ack_result =
+ ack_queue_.front().ack_result;
+ RenderWidgetHostViewInput* root_view = ack_queue_.front().root_view;
ack_queue_.pop_front();
if ((!touch_emulator ||
- !touch_emulator->HandleTouchEventAck(ack_data.touch_event.event,
- ack_data.ack_result)) &&
- (client_->IsViewInMap(ack_data.root_view) ||
- client_->ViewMapIsEmpty())) {
+ !touch_emulator->HandleTouchEventAck(touch_event.event, ack_result)) &&
+ (client_->IsViewInMap(root_view) || client_->ViewMapIsEmpty())) {
// Forward acked event and result to the root view associated with the
// event. The view map is only empty for AndroidWebView.
- ack_data.root_view->ProcessAckedTouchEvent(ack_data.touch_event,
- ack_data.ack_result);
+ root_view->ProcessAckedTouchEvent(touch_event, ack_result);
}
}
}
@@ -212,9 +214,11 @@
return data.root_view == target_view;
});
- // Otherwise, mark its status accordingly.
+ // Otherwise, mark its status accordingly and clear target_view to prevent
+ // dangling raw pointers.
for_each(ack_queue_.begin(), ack_queue_.end(), [target_view](AckData& data) {
if (data.target_view == target_view) {
+ data.target_view = nullptr;
data.touch_event_ack_status = TouchEventAckStatus::TouchEventAcked;
data.ack_result = blink::mojom::InputEventResultState::kNoConsumerExists;
}
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index 6abce33..3c0b9aeb 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -1356,9 +1356,18 @@
for (size_t i = 0; i < touch.event.touches_length; ++i) {
if (touch.event.touches[i].state == required_state) {
CHECK(!sent_ack);
+ // ProcessedTouchEvent() triggers synchronous gesture dispatch, which can
+ // lead to focus or window activation changes. Observers of these changes
+ // may synchronously destroy the WebContents and this view. We must guard
+ // this call with a WeakPtr liveness check before dereferencing 'this'
+ // (e.g., via host() or delegate calls below).
+ auto weak_this = weak_ptr_factory_.GetWeakPtr();
window_host->dispatcher()->ProcessedTouchEvent(
touch.event.unique_touch_event_id, window_, result,
input::InputEventResultStateIsSetBlocking(ack_result));
+ if (!weak_this) {
+ return;
+ }
if (touch.event.touch_start_or_first_touch_move &&
result == ui::ER_HANDLED && host()->delegate() &&
host()->delegate()->GetInputEventRouter()) {
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
index e64f63b..6e7ea91b 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
@@ -5,6 +5,7 @@
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
@@ -27,6 +28,7 @@
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/hit_test_region_observer.h"
#include "content/shell/browser/shell.h"
#include "content/shell/common/shell_switches.h"
#include "net/dns/mock_host_resolver.h"
@@ -35,6 +37,7 @@
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/screen.h"
+#include "ui/events/event_handler.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
@@ -840,4 +843,69 @@
mouse_wheel_phase_handler.touchpad_scroll_phase_state_for_test());
}
+namespace {
+class ViewDestroyingPreTargetHandler : public ui::EventHandler {
+ public:
+ explicit ViewDestroyingPreTargetHandler(aura::Window* root_window,
+ RenderWidgetHostViewAura* view)
+ : root_window_(root_window), view_(view) {
+ root_window_->AddPreTargetHandler(this);
+ }
+
+ ~ViewDestroyingPreTargetHandler() override {
+ if (root_window_) {
+ root_window_->RemovePreTargetHandler(this);
+ }
+ }
+
+ void OnGestureEvent(ui::GestureEvent* event) override {
+ if (event->type() == ui::EventType::kGestureTapDown && view_) {
+ RenderWidgetHostViewAura* view_to_destroy = view_;
+ view_ = nullptr;
+ view_to_destroy->Destroy();
+ gesture_tap_down_seen_ = true;
+ root_window_->RemovePreTargetHandler(this);
+ root_window_ = nullptr;
+ }
+ }
+
+ bool gesture_tap_down_seen() const { return gesture_tap_down_seen_; }
+
+ private:
+ raw_ptr<aura::Window> root_window_;
+ raw_ptr<RenderWidgetHostViewAura> view_;
+ bool gesture_tap_down_seen_ = false;
+};
+} // namespace
+
+IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewAuraBrowserTest,
+ ProcessAckedTouchEventUseAfterFree) {
+ GURL page(
+ "data:text/html;charset=utf-8,"
+ "<!DOCTYPE html>"
+ "<html>"
+ "<body style='width: 100vw; height: 100vh;'>"
+ "</body>"
+ "</html>");
+ EXPECT_TRUE(NavigateToURL(shell(), page));
+
+ auto* web_contents = shell()->web_contents();
+ SimulateEndOfPaintHoldingOnPrimaryMainFrame(web_contents);
+ WaitForHitTestData(web_contents->GetPrimaryMainFrame());
+
+ auto* rwhva = GetRenderWidgetHostView();
+ ASSERT_TRUE(rwhva);
+ auto* root_window = rwhva->GetNativeView()->GetRootWindow();
+ ASSERT_TRUE(root_window);
+
+ ViewDestroyingPreTargetHandler handler(root_window, rwhva);
+
+ ui::test::EventGenerator generator(root_window, rwhva->GetNativeView());
+ generator.MoveTouch(rwhva->GetNativeView()->bounds().CenterPoint());
+ generator.PressTouch();
+
+ EXPECT_TRUE(
+ base::test::RunUntil([&]() { return handler.gesture_tap_down_seen(); }));
+}
+
} // namespace content
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
index e64f63b..6e7ea91b 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
@@ -5,6 +5,7 @@
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
@@ -27,6 +28,7 @@
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/hit_test_region_observer.h"
#include "content/shell/browser/shell.h"
#include "content/shell/common/shell_switches.h"
#include "net/dns/mock_host_resolver.h"
@@ -35,6 +37,7 @@
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/screen.h"
+#include "ui/events/event_handler.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
@@ -840,4 +843,69 @@
mouse_wheel_phase_handler.touchpad_scroll_phase_state_for_test());
}
+namespace {
+class ViewDestroyingPreTargetHandler : public ui::EventHandler {
+ public:
+ explicit ViewDestroyingPreTargetHandler(aura::Window* root_window,
+ RenderWidgetHostViewAura* view)
+ : root_window_(root_window), view_(view) {
+ root_window_->AddPreTargetHandler(this);
+ }
+
+ ~ViewDestroyingPreTargetHandler() override {
+ if (root_window_) {
+ root_window_->RemovePreTargetHandler(this);
+ }
+ }
+
+ void OnGestureEvent(ui::GestureEvent* event) override {
+ if (event->type() == ui::EventType::kGestureTapDown && view_) {
+ RenderWidgetHostViewAura* view_to_destroy = view_;
+ view_ = nullptr;
+ view_to_destroy->Destroy();
+ gesture_tap_down_seen_ = true;
+ root_window_->RemovePreTargetHandler(this);
+ root_window_ = nullptr;
+ }
+ }
+
+ bool gesture_tap_down_seen() const { return gesture_tap_down_seen_; }
+
+ private:
+ raw_ptr<aura::Window> root_window_;
+ raw_ptr<RenderWidgetHostViewAura> view_;
+ bool gesture_tap_down_seen_ = false;
+};
+} // namespace
+
+IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewAuraBrowserTest,
+ ProcessAckedTouchEventUseAfterFree) {
+ GURL page(
+ "data:text/html;charset=utf-8,"
+ "<!DOCTYPE html>"
+ "<html>"
+ "<body style='width: 100vw; height: 100vh;'>"
+ "</body>"
+ "</html>");
+ EXPECT_TRUE(NavigateToURL(shell(), page));
+
+ auto* web_contents = shell()->web_contents();
+ SimulateEndOfPaintHoldingOnPrimaryMainFrame(web_contents);
+ WaitForHitTestData(web_contents->GetPrimaryMainFrame());
+
+ auto* rwhva = GetRenderWidgetHostView();
+ ASSERT_TRUE(rwhva);
+ auto* root_window = rwhva->GetNativeView()->GetRootWindow();
+ ASSERT_TRUE(root_window);
+
+ ViewDestroyingPreTargetHandler handler(root_window, rwhva);
+
+ ui::test::EventGenerator generator(root_window, rwhva->GetNativeView());
+ generator.MoveTouch(rwhva->GetNativeView()->bounds().CenterPoint());
+ generator.PressTouch();
+
+ EXPECT_TRUE(
+ base::test::RunUntil([&]() { return handler.gesture_tap_down_seen(); }));
+}
+
} // namespace content
Original Bug Report
Potential Use-After-Free in RenderWidgetHostViewAura::ProcessAckedTouchEvent via synchronous dispatch
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Use-After-Free vulnerability exists in the browser process when handling touch event acknowledgments in Aura-based environments. A compromised renderer can trigger synchronous object destruction during gesture dispatch, leading to a memory safety violation when the view is accessed after deletion. This could potentially lead to a sandbox escape on Windows, Linux, and ChromeOS.
Affected files:
content/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/render_widget_host_view_base.h
Estimated timestamp from git blame: 2024-06-06
Technical Analysis
A potential Use-After-Free (UAF) has been identified in RenderWidgetHostViewAura::ProcessAckedTouchEvent in content/browser/renderer_host/render_widget_host_view_aura.cc. The vulnerability occurs when a touch event acknowledgment (ACK) from the renderer triggers synchronous gesture dispatch, which in turn leads to the destruction of the RenderWidgetHostViewAura object while it is still being processed on the stack.
In ProcessAckedTouchEvent, the following call occurs:
// content/browser/renderer_host/render_widget_host_view_aura.cc:1335
window_host->dispatcher()->ProcessedTouchEvent(
touch.event.unique_touch_event_id, window_, result,
input::InputEventResultStateIsSetBlocking(ack_result));
if (touch.event.touch_start_or_first_touch_move &&
result == ui::ER_HANDLED && host()->delegate() && // Potential UAF
host()->delegate()->GetInputEventRouter()) { // Potential UAF
When a renderer ACKs a touch event as ‘consumed’ (ui::ER_HANDLED), aura::WindowEventDispatcher::ProcessedTouchEvent dispatches generated gestures synchronously. On desktop platforms, if kEnableGestureBeginEndTypes is enabled, a kGestureBegin event is dispatched.
This synchronous dispatch chain can reach wm::FocusController, which may trigger window activation or focus changes. Observers of these changes (such as a ‘dismiss-on-blur’ UI element or tab closure logic) can synchronously destroy the WebContents, which in turn destroys the RenderWidgetHostViewAura.
Because RenderWidgetHostViewAura implements aura::WindowDelegate, its destruction often goes through OnWindowDestroyed, which calls delete this;. When the synchronous stack unwinds back to ProcessAckedTouchEvent, the code continues to execute at line 1339, dereferencing this (via the host() helper) after it has been freed. Since this is a bare C++ pointer, MiraclePtr (BRP) does not protect this specific access to the object’s member fields.
Suggested steps to reproduce (Potential)
- Compromise a renderer process to control input event ACKs.
- Trigger a touch start event in a context where a focus change would cause the associated view to be destroyed (e.g., a popup window or a specific tab configuration).
- Have the renderer send a ‘Consumed’ ACK for the
touchstartevent. - The browser dispatches a synchronous
kGestureBeginevent, triggers the focus-based destruction, and subsequently accesses the freed view object inProcessAckedTouchEvent.
Impact
An attacker capable of compromising the renderer process could potentially exploit this UAF to achieve arbitrary code execution in the browser process. As the browser process is unsandboxed, this represents a sandbox escape.
Suggested Fix
The call to ProcessedTouchEvent should be guarded by a base::WeakPtr check. Since RenderWidgetHostViewAura inherits from RenderWidgetHostViewBase, which provides a base::WeakPtrFactory, a liveness check should be performed immediately after the synchronous call returns.
auto weak_ptr = base::AsWeakPtr<RenderWidgetHostViewBase>(this);
window_host->dispatcher()->ProcessedTouchEvent(...);
if (!weak_ptr)
return;
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.