Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker513324041
Fix commit133561714baf (chromium/src) +89/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
components/input/render_widget_host_input_event_router.cc
modified
for
content/browser/renderer_host/render_widget_host_view_aura.cc
modified
if
content/browser/renderer_host/render_widget_host_view_aura.cc
modified
ViewDestroyingPreTargetHandler
content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
modified
if
content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
modified

Files Changed

  • components/input/render_widget_host_input_event_router.cc
  • content/browser/renderer_host/render_widget_host_view_aura.cc
  • content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • content/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)

  1. Compromise a renderer process to control input event ACKs.
  2. 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).
  3. Have the renderer send a ‘Consumed’ ACK for the touchstart event.
  4. The browser dispatches a synchronous kGestureBegin event, triggers the focus-based destruction, and subsequently accesses the freed view object in ProcessAckedTouchEvent.

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.

View on issue tracker