Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassUAF
Tracker498991983
Fix commitd3038a70cb83 (chromium/src) +100/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
modified
if
third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
modified
HandlingState
third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h
modified
MockWidgetBaseClient
third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
modified
WidgetBaseInputHandlerTest
third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
modified
TEST_F
third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
modified

Files Changed

  • third_party/blink/renderer/platform/BUILD.gn
  • third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
  • third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h
  • third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
From d3038a70cb83faaf5b16c0847a4d8412ae995650 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
Date: Fri, 24 Apr 2026 14:48:03 -0700
Subject: [PATCH] Harden WidgetBaseInputHandler::HandleTouchEvent

A potential Use-After-Free (UAF) vulnerability existed in
WidgetBaseInputHandler::HandleTouchEvent when synchronous touch events
triggered a nested event loop in JavaScript. During this nested loop,
an unpausable IPC could destroy the WidgetBase, bypassing MiraclePtr
protections.

This CL adds a base::WeakPtr check in HandleTouchEvent to detect if
the WidgetBaseInputHandler (which is an inline member of WidgetBase)
has been destroyed during the synchronous event dispatch.

Bug: 498991983
Test: WidgetBaseInputHandlerTest.TouchEventDestroysWidget
Change-Id: Id5132bdf33ea170ab8e5e15f431f21d3f068dcab
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7788271
Reviewed-by: Aman Verma <amanvr@google.com>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620488}
---

diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
index 4919801..e9b5980 100644
--- a/third_party/blink/renderer/platform/BUILD.gn
+++ b/third_party/blink/renderer/platform/BUILD.gn
@@ -2423,6 +2423,7 @@
     "widget/input/mock_input_handler_proxy_client.h",
     "widget/input/prediction/filter_factory_unittest.cc",
     "widget/input/scroll_predictor_unittest.cc",
+    "widget/input/widget_base_input_handler_unittest.cc",
     "widget/input/widget_input_handler_manager_unittest.cc",
   ]
 
diff --git a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
index 71570ab..0411870 100644
--- a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
+++ b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
@@ -271,6 +271,7 @@
 
   const WebTouchEvent touch_event =
       static_cast<const WebTouchEvent&>(input_event);
+  auto weak_self = weak_ptr_factory_.GetWeakPtr();
   for (unsigned i = 0; i < touch_event.touches_length; ++i) {
     const WebTouchPoint& touch_point = touch_event.touches[i];
     if (touch_point.state != WebTouchPoint::State::kStateStationary) {
@@ -283,6 +284,9 @@
               coalesced_event.GetPredictedEventsPointers(),
               coalesced_event.latency_info());
       widget_->client()->HandleInputEvent(coalesced_pointer_event);
+      if (!weak_self) {
+        return WebInputEventResult::kNotHandled;
+      }
     }
   }
   return widget_->client()->DispatchBufferedTouchEvents();
@@ -566,6 +570,7 @@
       ui::INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT, &original_timestamp);
   DCHECK(found_original_component);
 
+  auto weak_self = weak_ptr_factory_.GetWeakPtr();
   gfx::PointF position = PositionInWidgetFromInputEvent(input_event);
   for (const InjectScrollGestureParams& params : injected_scroll_params) {
     // Set up a new `LatencyInfo` for the injected scroll - this is the original
@@ -658,6 +663,9 @@
               std::move(done_callback));
       widget_->client()->HandleInputEvent(
           WebCoalescedInputEvent(*gesture_event, scrollbar_latency_info));
+      if (!weak_self) {
+        return;
+      }
     }
   }
 }
diff --git a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h
index cad2ffa..443fb0c 100644
--- a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h
+++ b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h
@@ -80,6 +80,9 @@
   // cursor.
   bool DidChangeCursor(const ui::Cursor& cursor);
 
+  WebInputEventResult HandleTouchEvent(
+      const WebCoalescedInputEvent& coalesced_event);
+
  private:
   class HandlingState;
   struct InjectScrollGestureParams {
@@ -89,9 +92,6 @@
     blink::WebInputEvent::Type type;
   };
 
-  WebInputEventResult HandleTouchEvent(
-      const WebCoalescedInputEvent& coalesced_event);
-
   // Creates and handles scroll gestures based on parameters from
   // `injected_scroll_params`. `input_event`, `original_latency_info`, and
   // `original_metrics` are the original event causing gesture scrolls, its
diff --git a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
new file mode 100644
index 0000000..69a07490
--- /dev/null
+++ b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
@@ -0,0 +1,88 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h"
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/input/web_coalesced_input_event.h"
+#include "third_party/blink/public/common/input/web_touch_event.h"
+#include "third_party/blink/public/mojom/widget/platform_widget.mojom-blink.h"
+#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
+#include "third_party/blink/renderer/platform/scheduler/test/fake_widget_scheduler.h"
+#include "third_party/blink/renderer/platform/widget/compositing/test/stub_widget_base_client.h"
+#include "third_party/blink/renderer/platform/widget/widget_base.h"
+
+namespace blink {
+
+class MockWidgetBaseClient : public StubWidgetBaseClient {
+ public:
+  MOCK_METHOD(WebInputEventResult,
+              HandleInputEvent,
+              (const WebCoalescedInputEvent&),
+              (override));
+  MOCK_METHOD(WebInputEventResult, DispatchBufferedTouchEvents, (), (override));
+};
+
+class WidgetBaseInputHandlerTest : public testing::Test {
+ public:
+  WidgetBaseInputHandlerTest()
+      : widget_scheduler_(
+            base::MakeRefCounted<scheduler::FakeWidgetScheduler>()) {}
+
+  void SetUp() override {
+    mojo::AssociatedRemote<mojom::blink::WidgetHost> widget_host_remote;
+    mojo::PendingAssociatedReceiver<mojom::blink::WidgetHost>
+        widget_host_receiver =
+            widget_host_remote.BindNewEndpointAndPassDedicatedReceiver();
+
+    mojo::AssociatedRemote<mojom::blink::Widget> widget_remote;
+    mojo::PendingAssociatedReceiver<mojom::blink::Widget> widget_receiver =
+        widget_remote.BindNewEndpointAndPassDedicatedReceiver();
+
+    widget_base_ = std::make_unique<WidgetBase>(
+        &client_, widget_host_remote.Unbind(), std::move(widget_receiver),
+        scheduler::GetSingleThreadTaskRunnerForTesting(),
+        /*hidden=*/false, /*never_composited=*/false,
+        /*is_embedded=*/false,
+        /*is_for_scalable_page=*/false);
+  }
+
+ protected:
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  MockWidgetBaseClient client_;
+  scoped_refptr<scheduler::FakeWidgetScheduler> widget_scheduler_;
+  std::unique_ptr<WidgetBase> widget_base_;
+};
+
+TEST_F(WidgetBaseInputHandlerTest, TouchEventDestroysWidget) {
+  WebTouchEvent touch_event(WebInputEvent::Type::kTouchStart,
+                            WebInputEvent::kNoModifiers,
+                            WebInputEvent::GetStaticTimeStampForTests());
+  touch_event.touches_length = 1;
+  touch_event.touches[0].state = WebTouchPoint::State::kStatePressed;
+  touch_event.touches[0].id = 0;
+
+  WebCoalescedInputEvent coalesced_event(touch_event, ui::LatencyInfo());
+
+  // When HandleInputEvent is called, destroy the widget_base_.
+  EXPECT_CALL(client_, HandleInputEvent(testing::_))
+      .WillOnce([&](const WebCoalescedInputEvent&) {
+        widget_base_->Shutdown(false);
+        widget_base_.reset();
+        return WebInputEventResult::kHandledApplication;
+      });
+
+  // This should not crash if the fix is applied. Without the fix, it will UAF.
+  // We call HandleTouchEvent directly to avoid the
+  // LatencyInfoSwapPromiseMonitor which requires a full LayerTreeHost setup.
+  widget_base_->input_handler().HandleTouchEvent(coalesced_event);
+}
+
+}  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
new file mode 100644
index 0000000..69a07490
--- /dev/null
+++ b/third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
@@ -0,0 +1,88 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/widget/input/widget_base_input_handler.h"
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/input/web_coalesced_input_event.h"
+#include "third_party/blink/public/common/input/web_touch_event.h"
+#include "third_party/blink/public/mojom/widget/platform_widget.mojom-blink.h"
+#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
+#include "third_party/blink/renderer/platform/scheduler/test/fake_widget_scheduler.h"
+#include "third_party/blink/renderer/platform/widget/compositing/test/stub_widget_base_client.h"
+#include "third_party/blink/renderer/platform/widget/widget_base.h"
+
+namespace blink {
+
+class MockWidgetBaseClient : public StubWidgetBaseClient {
+ public:
+  MOCK_METHOD(WebInputEventResult,
+              HandleInputEvent,
+              (const WebCoalescedInputEvent&),
+              (override));
+  MOCK_METHOD(WebInputEventResult, DispatchBufferedTouchEvents, (), (override));
+};
+
+class WidgetBaseInputHandlerTest : public testing::Test {
+ public:
+  WidgetBaseInputHandlerTest()
+      : widget_scheduler_(
+            base::MakeRefCounted<scheduler::FakeWidgetScheduler>()) {}
+
+  void SetUp() override {
+    mojo::AssociatedRemote<mojom::blink::WidgetHost> widget_host_remote;
+    mojo::PendingAssociatedReceiver<mojom::blink::WidgetHost>
+        widget_host_receiver =
+            widget_host_remote.BindNewEndpointAndPassDedicatedReceiver();
+
+    mojo::AssociatedRemote<mojom::blink::Widget> widget_remote;
+    mojo::PendingAssociatedReceiver<mojom::blink::Widget> widget_receiver =
+        widget_remote.BindNewEndpointAndPassDedicatedReceiver();
+
+    widget_base_ = std::make_unique<WidgetBase>(
+        &client_, widget_host_remote.Unbind(), std::move(widget_receiver),
+        scheduler::GetSingleThreadTaskRunnerForTesting(),
+        /*hidden=*/false, /*never_composited=*/false,
+        /*is_embedded=*/false,
+        /*is_for_scalable_page=*/false);
+  }
+
+ protected:
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  MockWidgetBaseClient client_;
+  scoped_refptr<scheduler::FakeWidgetScheduler> widget_scheduler_;
+  std::unique_ptr<WidgetBase> widget_base_;
+};
+
+TEST_F(WidgetBaseInputHandlerTest, TouchEventDestroysWidget) {
+  WebTouchEvent touch_event(WebInputEvent::Type::kTouchStart,
+                            WebInputEvent::kNoModifiers,
+                            WebInputEvent::GetStaticTimeStampForTests());
+  touch_event.touches_length = 1;
+  touch_event.touches[0].state = WebTouchPoint::State::kStatePressed;
+  touch_event.touches[0].id = 0;
+
+  WebCoalescedInputEvent coalesced_event(touch_event, ui::LatencyInfo());
+
+  // When HandleInputEvent is called, destroy the widget_base_.
+  EXPECT_CALL(client_, HandleInputEvent(testing::_))
+      .WillOnce([&](const WebCoalescedInputEvent&) {
+        widget_base_->Shutdown(false);
+        widget_base_.reset();
+        return WebInputEventResult::kHandledApplication;
+      });
+
+  // This should not crash if the fix is applied. Without the fix, it will UAF.
+  // We call HandleTouchEvent directly to avoid the
+  // LatencyInfoSwapPromiseMonitor which requires a full LayerTreeHost setup.
+  widget_base_->input_handler().HandleTouchEvent(coalesced_event);
+}
+
+}  // namespace blink
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF and BRP bypass in WidgetBaseInputHandler::HandleTouchEvent leading to Renderer RCE

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 security team.

Overview: A potential Use-After-Free (UAF) vulnerability exists in WidgetBaseInputHandler::HandleTouchEvent when synchronous touch events trigger a nested event loop in JavaScript. During this nested loop, an unpausable IPC can destroy the WidgetBase, bypassing MiraclePtr protections. Upon returning from the loop, the dangling this pointer is accessed, potentially allowing Remote Code Execution (RCE) in the renderer.

Affected files:

  • third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc
  • third_party/blink/renderer/platform/widget/widget_base.h

Estimated timestamp from git blame: 2024-02-20

Summary

A potential Use-After-Free (UAF) vulnerability exists in WidgetBaseInputHandler::HandleTouchEvent that can lead to Remote Code Execution (RCE) in the renderer process. The issue occurs because touch events can trigger synchronous JavaScript execution, which may start a nested event loop. During this loop, specific unpausable IPCs can destroy the WidgetBase and its inline WidgetBaseInputHandler. When the nested loop exits, the function continues executing using a dangling this pointer.

Technical Details

In third_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc, the HandleTouchEvent function iterates through touch points and synchronously dispatches them via widget_->client()->HandleInputEvent(). This call chain eventually reaches PointerEventManager::DispatchPointerEvent, which synchronously fires JavaScript event listeners (e.g., pointerdown).

If the JavaScript listener executes a function that spins a nested event loop (like window.print()), a ScopedPagePauser is instantiated to pause renderer tasks. However, the mojom::Frame::Delete IPC is bound to the kInternalNavigationAssociated task queue. This queue uses FreezableTaskQueueTraits where can_be_paused defaults to false. Consequently, if a parent frame removes the target iframe during the print() dialog, the Delete IPC is processed immediately inside the nested loop.

Processing Delete triggers RenderFrameImpl::Delete, which propagates down to WebFrameWidgetImpl::Close and calls widget_base_.reset(). This destroys the WidgetBase object.

MiraclePtr (BRP) Bypass

This UAF cleanly bypasses MiraclePtr (BackupRefPtr) protections. WidgetBaseInputHandler is defined as an inline value member of WidgetBase (WidgetBaseInputHandler input_handler_{this};). When WidgetBase is destroyed, input_handler_ is synchronously destroyed. The only raw_ptr<WidgetBase> protecting the allocation is widget_, which resides inside the input_handler_. Because the raw_ptr is destroyed, the BRP reference count for the WidgetBase allocation drops to 0, and the memory is freed directly to PartitionAlloc without quarantine.

When the nested loop exits, execution returns to HandleTouchEvent, which attempts to execute return widget_->client()->DispatchBufferedTouchEvents();. At this point, this is a dangling pointer, and reading this->widget_ accesses the freed (and potentially reallocated) memory.

Potential Attack Steps

Note: Our tooling agent cannot run code, so these are suggested, theoretical steps to trigger the vulnerability based on code analysis.

  1. An attacker creates a page with a cross-origin out-of-process iframe (OOPIF).
  2. Inside the OOPIF, the attacker registers a pointerdown JavaScript listener.
  3. The attacker induces a touch event directed at the OOPIF.
  4. The pointerdown listener executes synchronously and calls window.print(), spinning a nested event loop.
  5. Concurrently, the attacker’s parent page removes the OOPIF from the DOM (iframe.remove()).
  6. The renderer processes the unpausable mojom::Frame::Delete IPC, freeing the WidgetBase.
  7. The attacker performs a PartitionAlloc heap spray matching the size of WidgetBase (e.g., using Web Workers or unpaused IPCs) to overwrite the freed memory with a fake vtable and spoofed client_ pointer.
  8. The print dialog is closed, returning execution to HandleTouchEvent.
  9. The code calls widget_->client()->DispatchBufferedTouchEvents(), hijacking control flow via the sprayed fake vtable to achieve renderer RCE.

Suggested Fix

The WidgetBaseInputHandler::HandleTouchEvent method needs to verify that it has not been destroyed during the synchronous event dispatch before it accesses this or widget_ again.

Similar to how HandleInputEvent manages its lifetime, HandleTouchEvent should capture a base::WeakPtr to itself before dispatching events. Before the final call to DispatchBufferedTouchEvents(), it must check if the WeakPtr is still valid:

base::WeakPtr<WidgetBaseInputHandler> weak_this = weak_ptr_factory_.GetWeakPtr();
// ... loop dispatching events ...
if (!weak_this) {
  return WebInputEventResult::kNotHandled;
}
return widget_->client()->DispatchBufferedTouchEvents();

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker