CVE-2026-15118
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/widget/input/widget_base_input_handler.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/widget/input/widget_base_input_handler.ccthird_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
Patch
From a621298c354b64bb95932e6d76c9133b5f69c988 Mon Sep 17 00:00:00 2001 From: Vladimir Levin <[email protected]> Date: Tue, 16 Jun 2026 07:16:09 -0700 Subject: [PATCH] Extend weak self checks to more places in HandleInputEvent Various calls can delete the "this" object, so extend the checks to more spots. [email protected] Bug: 523238265 Change-Id: Ifb5b22fbc09bdbcb3a33be65b935d6633c9d03fa Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7941475 Reviewed-by: Dave Tapuska <[email protected]> Commit-Queue: Vladimir Levin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1647525} --- 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 3ad3e9f..f94c271 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 @@ -350,6 +350,20 @@ std::move(done_callback)); bool prevent_default = false; + WebInputEventResult processed = WebInputEventResult::kNotHandled; + + auto check_was_destroyed = [&]() { + if (!weak_self) { + if (callback) { + std::move(callback).Run(GetAckResult(processed), swap_latency_info, + std::move(handling_state.event_overscroll()), + std::move(handling_state.touch_action())); + } + return true; + } + return false; + }; + bool show_virtual_keyboard_for_mouse = false; if (WebInputEvent::IsMouseEventType(input_event.GetType())) { const WebMouseEvent& mouse_event = @@ -359,6 +373,9 @@ mouse_event.PositionInWidget().y()); widget_->client()->WillHandleMouseEvent(mouse_event); + if (check_was_destroyed()) { + return; + } // Reset the last known cursor if mouse has left this widget. So next // time that the mouse enters we always set the cursor accordingly. @@ -395,21 +412,28 @@ // through to the web app would cause compatibility problems since // DPAD_CENTER is also used as a "confirm" button). prevent_default = true; + processed = WebInputEventResult::kHandledSuppressed; } } #endif + if (check_was_destroyed()) { + return; + } if (WebInputEvent::IsGestureEventType(input_event.GetType())) { const WebGestureEvent& gesture_event = static_cast<const WebGestureEvent&>(input_event); bool suppress = false; widget_->client()->WillHandleGestureEvent(gesture_event, &suppress); - prevent_default = prevent_default || suppress; + if (suppress) { + prevent_default = true; + processed = WebInputEventResult::kHandledSuppressed; + } + if (check_was_destroyed()) { + return; + } } - WebInputEventResult processed = prevent_default - ? WebInputEventResult::kHandledSuppressed - : WebInputEventResult::kNotHandled; if (input_event.GetType() != WebInputEvent::Type::kChar || !suppress_next_char_events_) { suppress_next_char_events_ = false; @@ -421,15 +445,7 @@ processed = widget_->client()->HandleInputEvent(coalesced_event); } - // The associated WidgetBase (and this WidgetBaseInputHandler) could - // have been destroyed. If it was return early before accessing any more of - // this class. - if (!weak_self) { - if (callback) { - std::move(callback).Run(GetAckResult(processed), swap_latency_info, - std::move(handling_state.event_overscroll()), - std::move(handling_state.touch_action())); - } + if (check_was_destroyed()) { return; } } @@ -458,6 +474,9 @@ HandleInjectedScrollGestures( std::move(handling_state.injected_scroll_params()), input_event, coalesced_event.latency_info(), cloned_metrics.get()); + if (check_was_destroyed()) { + return; + } } // Send gesture scroll events and their dispositions to the compositor thread, @@ -483,6 +502,10 @@ } } + if (check_was_destroyed()) { + return; + } + if (callback) { std::move(callback).Run(GetAckResult(processed), swap_latency_info, std::move(handling_state.event_overscroll()), @@ -492,6 +515,10 @@ << "Unexpected overscroll for un-acked event"; } + if (!weak_self) { + return; + } + // Show the virtual keyboard if enabled and a user gesture triggers a focus // change. if ((processed != WebInputEventResult::kNotHandled && 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 index 69a07490..046b4e6 100644 --- 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 @@ -15,9 +15,12 @@ #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/public/dummy_schedulers.h" +#include "third_party/blink/renderer/platform/scheduler/public/page_scheduler.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" +#include "ui/display/screen_infos.h" namespace blink { @@ -28,6 +31,15 @@ (const WebCoalescedInputEvent&), (override)); MOCK_METHOD(WebInputEventResult, DispatchBufferedTouchEvents, (), (override)); + MOCK_METHOD(void, + WillHandleGestureEvent, + (const WebGestureEvent&, bool*), + (override)); + MOCK_METHOD(void, WillHandleMouseEvent, (const WebMouseEvent&), (override)); + const display::ScreenInfos& GetOriginalScreenInfos() override { + return screen_infos_; + } + display::ScreenInfos screen_infos_{display::ScreenInfo()}; }; class WidgetBaseInputHandlerTest : public testing::Test { @@ -52,12 +64,19 @@ /*hidden=*/false, /*never_composited=*/false, /*is_embedded=*/false, /*is_for_scalable_page=*/false); + + page_scheduler_ = scheduler::CreateDummyPageScheduler(); + display::ScreenInfo screen_info; + display::ScreenInfos screen_infos(screen_info); + widget_base_->InitializeCompositing(*page_scheduler_, screen_infos, nullptr, + nullptr, nullptr); } protected: - base::test::SingleThreadTaskEnvironment task_environment_; + base::test::TaskEnvironment task_environment_; MockWidgetBaseClient client_; scoped_refptr<scheduler::FakeWidgetScheduler> widget_scheduler_; + std::unique_ptr<PageScheduler> page_scheduler_; std::unique_ptr<WidgetBase> widget_base_; }; @@ -85,4 +104,59 @@ widget_base_->input_handler().HandleTouchEvent(coalesced_event); } +TEST_F(WidgetBaseInputHandlerTest, GestureEventDestroysWidget) { + WebGestureEvent gesture_event(WebInputEvent::Type::kGestureScrollBegin, + WebInputEvent::kNoModifiers, + WebInputEvent::GetStaticTimeStampForTests(), + WebGestureDevice::kTouchscreen); + WebCoalescedInputEvent coalesced_event(gesture_event, ui::LatencyInfo()); +
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
index 69a07490..046b4e6 100644
--- 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
@@ -15,9 +15,12 @@
#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/public/dummy_schedulers.h"
+#include "third_party/blink/renderer/platform/scheduler/public/page_scheduler.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"
+#include "ui/display/screen_infos.h"
namespace blink {
@@ -28,6 +31,15 @@
(const WebCoalescedInputEvent&),
(override));
MOCK_METHOD(WebInputEventResult, DispatchBufferedTouchEvents, (), (override));
+ MOCK_METHOD(void,
+ WillHandleGestureEvent,
+ (const WebGestureEvent&, bool*),
+ (override));
+ MOCK_METHOD(void, WillHandleMouseEvent, (const WebMouseEvent&), (override));
+ const display::ScreenInfos& GetOriginalScreenInfos() override {
+ return screen_infos_;
+ }
+ display::ScreenInfos screen_infos_{display::ScreenInfo()};
};
class WidgetBaseInputHandlerTest : public testing::Test {
@@ -52,12 +64,19 @@
/*hidden=*/false, /*never_composited=*/false,
/*is_embedded=*/false,
/*is_for_scalable_page=*/false);
+
+ page_scheduler_ = scheduler::CreateDummyPageScheduler();
+ display::ScreenInfo screen_info;
+ display::ScreenInfos screen_infos(screen_info);
+ widget_base_->InitializeCompositing(*page_scheduler_, screen_infos, nullptr,
+ nullptr, nullptr);
}
protected:
- base::test::SingleThreadTaskEnvironment task_environment_;
+ base::test::TaskEnvironment task_environment_;
MockWidgetBaseClient client_;
scoped_refptr<scheduler::FakeWidgetScheduler> widget_scheduler_;
+ std::unique_ptr<PageScheduler> page_scheduler_;
std::unique_ptr<WidgetBase> widget_base_;
};
@@ -85,4 +104,59 @@
widget_base_->input_handler().HandleTouchEvent(coalesced_event);
}
+TEST_F(WidgetBaseInputHandlerTest, GestureEventDestroysWidget) {
+ WebGestureEvent gesture_event(WebInputEvent::Type::kGestureScrollBegin,
+ WebInputEvent::kNoModifiers,
+ WebInputEvent::GetStaticTimeStampForTests(),
+ WebGestureDevice::kTouchscreen);
+ WebCoalescedInputEvent coalesced_event(gesture_event, ui::LatencyInfo());
+
+ EXPECT_CALL(client_, WillHandleGestureEvent(testing::_, testing::_))
+ .WillOnce([&](const WebGestureEvent&, bool*) {
+ widget_base_->Shutdown(false);
+ widget_base_.reset();
+ });
+
+ bool callback_run = false;
+ widget_base_->input_handler().HandleInputEvent(
+ coalesced_event, nullptr,
+ base::BindOnce(
+ [](bool* callback_run, mojom::InputEventResultState ack_state,
+ const ui::LatencyInfo& latency_info,
+ std::unique_ptr<InputHandlerProxy::DidOverscrollParams> overscroll,
+ std::optional<WebTouchAction> touch_action) {
+ *callback_run = true;
+ },
+ &callback_run));
+
+ EXPECT_TRUE(callback_run);
+}
+
+TEST_F(WidgetBaseInputHandlerTest, MouseEventDestroysWidget) {
+ WebMouseEvent mouse_event(WebInputEvent::Type::kMouseMove,
+ WebInputEvent::kNoModifiers,
+ WebInputEvent::GetStaticTimeStampForTests());
+ WebCoalescedInputEvent coalesced_event(mouse_event, ui::LatencyInfo());
+
+ EXPECT_CALL(client_, WillHandleMouseEvent(testing::_))
+ .WillOnce([&](const WebMouseEvent&) {
+ widget_base_->Shutdown(false);
+ widget_base_.reset();
+ });
+
+ bool callback_run = false;
+ widget_base_->input_handler().HandleInputEvent(
+ coalesced_event, nullptr,
+ base::BindOnce(
+ [](bool* callback_run, mojom::InputEventResultState ack_state,
+ const ui::LatencyInfo& latency_info,
+ std::unique_ptr<InputHandlerProxy::DidOverscrollParams> overscroll,
+ std::optional<WebTouchAction> touch_action) {
+ *callback_run = true;
+ },
+ &callback_run));
+
+ EXPECT_TRUE(callback_run);
+}
+
} // namespace blink
Original Bug Report
Potential Use-After-Free in WidgetBaseInputHandler::HandleInputEvent
Flapjack, 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. 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 WidgetBaseInputHandler::HandleInputEvent. Handling specific gesture events can synchronously trigger DOM blur/focus events, allowing a nested event loop to destroy the WidgetBase. Returning to the unwound stack results in a reliable 1-byte write of zero to freed memory, bypassing MiraclePtr protections.
Affected files:
third_party/blink/renderer/platform/widget/input/widget_base_input_handler.ccthird_party/blink/renderer/core/frame/web_frame_widget_impl.cc
Estimated timestamp from git blame: 2020-10-31
Description
A potential Use-After-Free (UAF) vulnerability exists in WidgetBaseInputHandler::HandleInputEvent. When processing gesture events, the code can trigger synchronous DOM selection updates, leading to a nested event loop. If the widget is destroyed during this loop, execution unwinds to a dangling this pointer, resulting in a reliable memory corruption primitive.
Vulnerability Details
- Gesture Handling: In
WidgetBaseInputHandler::HandleInputEvent, gesture events are pre-processed viawidget_->client()->WillHandleGestureEvent(...). - Synchronous Selection: For certain gestures (e.g.,
kGestureScrollBeginwithcursor_controlactive),WebFrameWidgetImpl::WillHandleGestureEventcallsfocused_frame->MoveCaretSelection(...). - Synchronous Event Dispatch: Updating the selection can synchronously dispatch
blurandfocusevents viaSetFocusedNodeIfNeeded()if the focused element changes. - Nested Event Loop & Destruction: An attacker can register a JavaScript event listener for these
blurorfocusevents that spins a nested event loop (e.g., usingwindow.print()). During this loop, the frame can be detached via an IPC message (e.g.,mojom::Frame::Delete), destroying theWebFrameWidgetImpl, itsWidgetBase, and the inline memberWidgetBaseInputHandler. - UAF Write: When the stack unwinds back to
HandleInputEvent, execution continues using the now-danglingthispointer:
if (input_event.GetType() != WebInputEvent::Type::kChar ||
!suppress_next_char_events_) {
suppress_next_char_events_ = false; // <--- UAF Write
Because the event is not a kChar event, the first half of the logical OR evaluates to true. C++ short-circuit evaluation prevents the read of !suppress_next_char_events_ (which might otherwise crash if the memory was poisoned). The code unconditionally executes the assignment, resulting in a reliable 1-byte write of 0x00 (false) to the offset of suppress_next_char_events_.
MiraclePtr Bypass
This vulnerability bypasses MiraclePtr (BackupRefPtr). WidgetBaseInputHandler is an inline member of WidgetBase, and its internal raw_ptr<WidgetBase> widget_ is destroyed alongside the widget. Because this is the only raw_ptr to the allocation, the refcount drops to zero, and the memory is fully freed to PartitionAlloc. The subsequent access in HandleInputEvent uses the raw implicit this pointer on the stack, which BRP does not protect.
Potential Exploitation Steps
(Note: These are suggested steps based on static analysis; a working PoC has not been executed).
- Create an attacker-controlled cross-site iframe.
- Inside the iframe, create a focusable text input and attach a
blurevent listener. - Induce the user to perform a cursor movement gesture (e.g., swiping the spacebar on the Android virtual keyboard) while the text input is focused.
- In the
blurevent listener, callwindow.print()to spin a nested event loop. - From the parent frame, remove the iframe from the DOM, triggering the destruction of the iframe’s
WidgetBasevia IPC. - During the nested event loop (e.g., using a Web Worker), spray the PartitionAlloc heap with objects matching the size of
WidgetBase(~1056 bytes). - When the event loop finishes and the stack unwinds, the UAF write will overwrite a 1-byte value in the attacker-controlled sprayed object, potentially corrupting a length field or pointer to achieve arbitrary read/write and Remote Code Execution in the renderer process.
Recommended Fix
Move the existing WeakPtr check (!weak_self) earlier in HandleInputEvent. It currently resides at line 427, after the UAF write occurs at line 415. The check should be placed immediately following the calls to WillHandleMouseEvent and WillHandleGestureEvent (around line 409) to ensure the handler is still valid before accessing any member variables.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.