Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Input
DescriptionUse after free in Input
ComponentInput
Bug ClassUAF
Tracker523238265
Fix commita621298c354b (chromium/src) +115/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/widget/input/widget_base_input_handler.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/widget/input/widget_base_input_handler.cc
  • third_party/blink/renderer/platform/widget/input/widget_base_input_handler_unittest.cc
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());
+
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
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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

  1. Gesture Handling: In WidgetBaseInputHandler::HandleInputEvent, gesture events are pre-processed via widget_->client()->WillHandleGestureEvent(...).
  2. Synchronous Selection: For certain gestures (e.g., kGestureScrollBegin with cursor_control active), WebFrameWidgetImpl::WillHandleGestureEvent calls focused_frame->MoveCaretSelection(...).
  3. Synchronous Event Dispatch: Updating the selection can synchronously dispatch blur and focus events via SetFocusedNodeIfNeeded() if the focused element changes.
  4. Nested Event Loop & Destruction: An attacker can register a JavaScript event listener for these blur or focus events that spins a nested event loop (e.g., using window.print()). During this loop, the frame can be detached via an IPC message (e.g., mojom::Frame::Delete), destroying the WebFrameWidgetImpl, its WidgetBase, and the inline member WidgetBaseInputHandler.
  5. UAF Write: When the stack unwinds back to HandleInputEvent, execution continues using the now-dangling this pointer:
  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).

  1. Create an attacker-controlled cross-site iframe.
  2. Inside the iframe, create a focusable text input and attach a blur event listener.
  3. 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.
  4. In the blur event listener, call window.print() to spin a nested event loop.
  5. From the parent frame, remove the iframe from the DOM, triggering the destruction of the iframe’s WidgetBase via IPC.
  6. During the nested event loop (e.g., using a Web Worker), spray the PartitionAlloc heap with objects matching the size of WidgetBase (~1056 bytes).
  7. 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.

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.

View on issue tracker