CVE-2026-11059
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc |
modified | |
ifthird_party/blink/renderer/platform/widget/input/widget_base_input_handler.cc |
modified | |
HandlingStatethird_party/blink/renderer/platform/widget/input/widget_base_input_handler.h |
modified | |
MockWidgetBaseClientthird_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc |
modified | |
WidgetBaseInputHandlerTestthird_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc |
modified | |
TEST_Fthird_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc |
modified |
Files Changed
third_party/blink/renderer/platform/BUILD.gnthird_party/blink/renderer/platform/widget/input/widget_base_input_handler.ccthird_party/blink/renderer/platform/widget/input/widget_base_input_handler.hthird_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
Patch
From d3038a70cb83faaf5b16c0847a4d8412ae995650 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> 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 <[email protected]> Reviewed-by: Philip Rogers <[email protected]> Commit-Queue: Jonathan Ross <[email protected]> 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
Regression Test / PoC
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
Original Bug Report
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.ccthird_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.
- An attacker creates a page with a cross-origin out-of-process iframe (OOPIF).
- Inside the OOPIF, the attacker registers a
pointerdownJavaScript listener. - The attacker induces a touch event directed at the OOPIF.
- The
pointerdownlistener executes synchronously and callswindow.print(), spinning a nested event loop. - Concurrently, the attacker’s parent page removes the OOPIF from the DOM (
iframe.remove()). - The renderer processes the unpausable
mojom::Frame::DeleteIPC, freeing theWidgetBase. - 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 spoofedclient_pointer. - The print dialog is closed, returning execution to
HandleTouchEvent. - 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.