diff --git a/third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc b/third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc index 2350e2634b02f3..faac1dcf87b20e 100644 --- a/third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc +++ b/third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/core/html/forms/internal_popup_menu.h" +#include "base/run_loop.h" #include "build/build_config.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/web/web_document.h" @@ -12,14 +13,18 @@ #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/exported/web_page_popup_impl.h" +#include "third_party/blink/renderer/core/exported/web_view_impl.h" #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/html/forms/html_select_element.h" #include "third_party/blink/renderer/core/loader/empty_clients.h" +#include "third_party/blink/renderer/core/page/page.h" +#include "third_party/blink/renderer/core/page/page_popup_controller.h" #include "third_party/blink/renderer/core/testing/dummy_page_holder.h" #include "third_party/blink/renderer/core/testing/page_test_base.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/testing/task_environment.h" +#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" #include "third_party/blink/renderer/platform/wtf/shared_buffer.h" @@ -108,6 +113,90 @@ TEST_F(InternalPopupMenuTest, MediaFeatureOverridesPropagation) { popup2->ClosePopup(); } +// Demonstrates that PagePopupController::popup_ (a bare PagePopup& reference, +// page_popup_controller.h:92) dangles after the orphaned-popup destruction +// path frees its WebPagePopupImpl referent without DestroyPage(). +// +// In production this is reached when a re-entrant OpenPagePopup overwrites +// WebViewImpl::page_popup_ (so ClosePagePopup early-returns at +// web_view_impl.cc:1104) and the browser later disconnects the orphaned +// popup's PopupWidgetHost. We model that here by dropping the WebView's +// scoped_refptr via CleanupPagePopup() and letting the test PopupWidgetHost +// receiver (which the stub LocalFrameHost drops on the floor) disconnect. +// +// The picker JS in the orphaned popup's still-attached LocalFrame can then +// call window.pagePopupController.setWindowRect(), which performs a virtual +// call through the freed PagePopup vtable. +TEST_F(InternalPopupMenuTest, PagePopupControllerUAFAfterOrphanedFree) { + if (!RuntimeEnabledFeatures::PagePopupEnabled()) { + return; + } + + frame_test_helpers::WebViewHelper web_view_helper; + WebViewImpl* web_view = web_view_helper.Initialize(); + WebURL base_url = url_test_helpers::ToKURL("http://example.com/"); + frame_test_helpers::LoadHTMLString(web_view->MainFrameImpl(), R"HTML( + + )HTML", + base_url); + Document& document = + *web_view->MainFrameImpl()->GetDocument().Unwrap(); + document.View()->UpdateAllLifecyclePhasesForTest(); + + auto* sel = + To(document.getElementById(AtomicString("sel"))); + ASSERT_TRUE(sel); + auto* menu = MakeGarbageCollected( + MakeGarbageCollected(), *sel); + + // Open popup_B. WebViewImpl::page_popup_ = B; B refcount == 2. + WebPagePopupImpl* popup = web_view->OpenPagePopup(menu); + popup->DidShowPopup(); + + // Grab the PagePopupController bound to B's internal Page. Its `popup_` + // member is a bare C++ reference to B's PagePopup subobject. + Page* popup_page = popup->GetDocument().Unwrap()->GetPage(); + ASSERT_TRUE(popup_page); + Persistent controller = + PagePopupController::From(*popup_page); + ASSERT_TRUE(controller); + + // Model the re-entrant-OpenPagePopup orphaning step: drop the WebView's + // owning reference *without* running ClosePopup()/DestroyPage() on B. + // (In the wild, this happens when page_popup_ is overwritten by popup_C + // while popup_B was created inside a synchronous change-event handler.) + // After this, B has only its self-reference (refcount == 1) and page_ is + // still non-null. + web_view->CleanupPagePopup(); + + // The PopupWidgetHost receiver was passed to LocalFrameHost:: + // CreateNewPopupWidget; the test stub drops it. Pump tasks so the + // associated remote observes the disconnection and runs + // WebPagePopupImpl::WidgetHostDisconnected() -> Close(). + // + // Inside Close(): page_ non-null -> Cancel() -> InternalPopupMenu:: + // CancelPopup() -> ClosePagePopup(B) goes nowhere (page_popup_ != B), so + // ClosePopup()/DestroyPage()/ClearPagePopupClient() are *bypassed*. Then + // widget_base_->Shutdown(); Release() drops refcount 1->0. + // ~WebPagePopupImpl runs with page_ non-null (release: DCHECK stripped) + // and PartitionAlloc frees B. + base::RunLoop().RunUntilIdle(); + test::RunPendingTasks(); + + // The popup's LocalFrame was never detached (DestroyPage skipped), so in + // production its picker JS DOMTimers can still fire and reach + // setWindowRect() on the very next main-thread turn. We model that JS + // call directly: + // + // page_popup_controller.cc:120 -> popup_.SetWindowRect(...) is a *virtual* + // call (PagePopup::SetWindowRect is pure virtual, page_popup.h:50) through + // the freed PartitionAlloc allocation. ASAN: heap-use-after-free. + controller->setWindowRect(0, 0, 100, 100); +} + #endif // !BUILDFLAG(IS_ANDROID) } // namespace blink