diff --git a/ui/views/widget/widget_unittest.cc b/ui/views/widget/widget_unittest.cc index 4e0cf1e66c750..da9d1e923e7a2 100644 --- a/ui/views/widget/widget_unittest.cc +++ b/ui/views/widget/widget_unittest.cc @@ -3470,6 +3470,81 @@ TEST_F(WidgetTest, DestroyedWithCaptureViaEventMonitor) { EXPECT_TRUE(observer.widget_closed()); } +namespace { + +// A view that synchronously destroys |target_widget_| when it loses mouse +// capture. This models production paths such as +// MenuHost::OnMouseCaptureLost -> MenuController::Cancel(kAll), which can +// synchronously close arbitrary widgets including the one currently acquiring +// capture. +class CaptureLostDestroysWidgetView : public View { + METADATA_HEADER(CaptureLostDestroysWidgetView, View) + + public: + explicit CaptureLostDestroysWidgetView(Widget* target_widget) + : target_widget_(target_widget) {} + + CaptureLostDestroysWidgetView(const CaptureLostDestroysWidgetView&) = delete; + CaptureLostDestroysWidgetView& operator=( + const CaptureLostDestroysWidgetView&) = delete; + + // View: + void OnMouseCaptureLost() override { + if (target_widget_) { + Widget* w = target_widget_; + target_widget_ = nullptr; + // Synchronously destroys the Widget under NATIVE_WIDGET_OWNS_WIDGET. + w->CloseNow(); + } + } + + private: + raw_ptr target_widget_; +}; + +BEGIN_METADATA(CaptureLostDestroysWidgetView) +END_METADATA + +} // namespace + +// Widget::SetCapture() invokes native_widget_->SetCapture(), which reaches the +// platform CaptureClient. The CaptureClient synchronously dispatches +// OnCaptureLost to the previously-capturing window; that handler may close +// arbitrary widgets including |this|. After the reentrant call returns, +// Widget::SetCapture() continues to dereference |this| (loads native_widget_, +// writes is_mouse_button_pressed_, loads root_view_ and issues a virtual +// call) without checking for self-deletion -> heap-use-after-free. +TEST_F(WidgetTest, SetCaptureSurvivesSelfDeletionDuringCaptureChange) { + // Widget B currently holds capture; its capture-lost handler will destroy A. + Widget* widget_b = CreateTopLevelNativeWidget(); + widget_b->Show(); + widget_b->SetSize(gfx::Size(100, 100)); + + // Widget A will call SetCapture and be destroyed reentrantly inside it. + Widget* widget_a = CreateTopLevelNativeWidget(); + widget_a->Show(); + widget_a->SetSize(gfx::Size(100, 100)); + + // Install a view in B that destroys A when B loses capture, then give B + // capture so |destroyer| becomes B's mouse_pressed_handler_. + auto* destroyer = widget_b->GetRootView()->AddChildView( + std::make_unique(widget_a)); + widget_b->SetCapture(destroyer); + ASSERT_TRUE(widget_b->HasCapture()); + + WidgetDeletionObserver a_observer(widget_a); + View* view_in_a = + widget_a->GetRootView()->AddChildView(std::make_unique()); + + // Enters CaptureClient::SetCapture which dispatches OnCaptureLost to B. + // B's handler synchronously destroys A. Widget::SetCapture then continues + // to dereference |this| (= freed widget_a). ASAN: heap-use-after-free. + widget_a->SetCapture(view_in_a); + + EXPECT_FALSE(a_observer.IsWidgetAlive()); + widget_b->CloseNow(); +} + TEST_F(WidgetTest, LockPaintAsActive) { WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); widget->ShowInactive();