diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_test.cc b/third_party/blink/renderer/core/frame/web_frame_widget_test.cc index 51947fc6a4869..d30a6e502c059 100644 --- a/third_party/blink/renderer/core/frame/web_frame_widget_test.cc +++ b/third_party/blink/renderer/core/frame/web_frame_widget_test.cc @@ -3119,4 +3119,104 @@ TEST_F(WebFrameWidgetAdditionalWindowingControlsTest, SetResizableTimeout) { #endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +// Widget that destroys its WidgetBase inside WillHandleGestureEvent. +// +// This simulates the production scenario for cursor_control gestures +// (swipe-to-move-cursor on Android/ChromeOS): WillHandleGestureEvent calls +// MoveCaretSelection(), which can synchronously dispatch focus/blur events via +// FrameSelection::SetFocusedNodeIfNeeded. A blur handler can spin a nested +// event loop (e.g. window.print()), during which the browser may deliver a +// Frame::Delete IPC for this OOPIF local root. That IPC detaches the frame and +// calls WebFrameWidgetImpl::Close(), freeing the WidgetBase (and its inline +// WidgetBaseInputHandler) while HandleInputEvent is still on the stack. +class DestroyOnGestureWebFrameWidget + : public frame_test_helpers::TestWebFrameWidget { + public: + template + explicit DestroyOnGestureWebFrameWidget(Args&&... args) + : frame_test_helpers::TestWebFrameWidget(std::forward(args)...) {} + + void SetDestroyCallback(base::OnceClosure cb) { + destroy_callback_ = std::move(cb); + } + + void WillHandleGestureEvent(const WebGestureEvent& event, + bool* suppress) override { + if (destroy_callback_) { + std::move(destroy_callback_).Run(); + } + // The cursor_control path sets *suppress = true so the event is not + // re-dispatched to HandleInputEvent. + *suppress = true; + } + + private: + base::OnceClosure destroy_callback_; +}; + +class WidgetBaseInputHandlerGestureUAFTest : public SimTest { + public: + frame_test_helpers::TestWebFrameWidget* CreateWebFrameWidget( + base::PassKey pass_key, + CrossVariantMojoAssociatedRemote< + mojom::blink::FrameWidgetHostInterfaceBase> frame_widget_host, + CrossVariantMojoAssociatedReceiver + frame_widget, + CrossVariantMojoAssociatedRemote + widget_host, + CrossVariantMojoAssociatedReceiver + widget, + scoped_refptr task_runner, + const viz::FrameSinkId& frame_sink_id, + bool hidden, + bool never_composited, + bool is_for_child_local_root, + bool is_for_nested_main_frame, + bool is_for_scalable_page) override { + return MakeGarbageCollected( + pass_key, std::move(frame_widget_host), std::move(frame_widget), + std::move(widget_host), std::move(widget), std::move(task_runner), + frame_sink_id, hidden, never_composited, is_for_child_local_root, + is_for_nested_main_frame, is_for_scalable_page); + } +}; + +// Regression test: WidgetBaseInputHandler::HandleInputEvent must not access +// |this| after WillHandleGestureEvent returns, because that call can +// synchronously destroy the WidgetBase via re-entrant frame deletion. +// +// On a vulnerable build this test triggers an ASAN heap-use-after-free at +// |suppress_next_char_events_ = false| in HandleInputEvent. +TEST_F(WidgetBaseInputHandlerGestureUAFTest, + WillHandleGestureEventDestroysWidget) { + LoadURL("about:blank"); + Compositor().BeginFrame(); + + auto* widget = static_cast( + WebView().MainFrameViewWidget()); + ASSERT_TRUE(widget); + + widget->SetDestroyCallback(base::BindLambdaForTesting([this]() { + // Tear down the WebView. This detaches the main frame and calls + // WebFrameWidgetImpl::Close(), which resets widget_base_ and frees the + // WidgetBase + inline WidgetBaseInputHandler. In production this is the + // result of a mojom::Frame::Delete IPC processed during a nested event + // loop started from a blur handler. + WebViewHelper().Reset(); + })); + + WebGestureEvent gesture(WebInputEvent::Type::kGestureScrollBegin, + WebInputEvent::kNoModifiers, base::TimeTicks::Now(), + WebGestureDevice::kTouchscreen); + gesture.data.scroll_begin.cursor_control = true; + + // WidgetBaseInputHandler::HandleInputEvent calls WillHandleGestureEvent (via + // widget_->client()), which destroys the WidgetBase. Execution then resumes + // on a dangling |this| and writes |suppress_next_char_events_ = false| + // before reaching the existing |if (!weak_self)| guard. + widget->ProcessInputEventSynchronouslyForTesting( + WebCoalescedInputEvent(gesture.Clone(), {}, {}, ui::LatencyInfo()), + base::DoNothing()); +} + } // namespace blink 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 3ad3e9f40ec73..cfb5bd208d71b 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 @@ -407,6 +407,21 @@ void WidgetBaseInputHandler::HandleInputEvent( prevent_default = prevent_default || suppress; } + // WillHandleMouseEvent / WillHandleGestureEvent above can run script (e.g. + // cursor_control gestures call MoveCaretSelection which can synchronously + // dispatch focus/blur events) which may destroy this WidgetBase via a nested + // event loop. Bail out before touching any members. + if (!weak_self) { + if (callback) { + std::move(callback).Run( + prevent_default ? mojom::blink::InputEventResultState::kConsumed + : mojom::blink::InputEventResultState::kNotConsumed, + swap_latency_info, std::move(handling_state.event_overscroll()), + std::move(handling_state.touch_action())); + } + return; + } + WebInputEventResult processed = prevent_default ? WebInputEventResult::kHandledSuppressed : WebInputEventResult::kNotHandled;