CVE-2026-106278
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/exported/web_page_popup_impl.cc |
modified | |
ifthird_party/blink/renderer/core/exported/web_view_impl.cc |
modified | |
DisconnectOnChangeListenerthird_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc |
modified | |
ifthird_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/exported/web_page_popup_impl.ccthird_party/blink/renderer/core/html/forms/internal_popup_menu_test.ccthird_party/blink/renderer/core/exported/web_page_popup_impl.hthird_party/blink/renderer/core/exported/web_view_impl.cc
Patch
From f9cea35d3e88ecc0b2edd526bd67ed2d56457bf0 Mon Sep 17 00:00:00 2001 From: David Baron <[email protected]> Date: Fri, 25 Sep 2026 16:47:07 -0700 Subject: [PATCH] Ensure sufficient lifetime of WebViewImpl::page_popup_. This is a straightforward fix that I think should be suitable for backporting to branches. In the longer term we might want to make WebPagePopupImpl garbage collected, but that's a bit more involved. I verified that the added unittest (built by Gemini from the original testcase) does exercise the need for both the scoped_refptr in WebViewImpl::UpdatePagePopup and the null-check in WebPagePopupImpl::Update, both of which are also needed to fix the original testcase. Fixed: 565774991 Change-Id: I828fe321e04a74bf9f6ba25222c507197082cbc3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8464185 Commit-Queue: David Baron <[email protected]> Reviewed-by: Joey Arhar <[email protected]> Cr-Commit-Position: refs/heads/main@{#1705319} --- diff --git a/third_party/blink/renderer/core/exported/web_page_popup_impl.cc b/third_party/blink/renderer/core/exported/web_page_popup_impl.cc index 372119a..4c2bc01 100644 --- a/third_party/blink/renderer/core/exported/web_page_popup_impl.cc +++ b/third_party/blink/renderer/core/exported/web_page_popup_impl.cc @@ -660,6 +660,10 @@ } popup_client_->Update(forced_update); + if (!widget_base_) { + // Something inside Update() closed this popup. + return; + } if (forced_update) { SetWindowRect(WindowRectInScreen()); } @@ -1066,6 +1070,10 @@ // Careful, this is now destroyed. } +void WebPagePopupImpl::WidgetHostDisconnectedForTesting() { + WidgetHostDisconnected(); +} + void WebPagePopupImpl::Close() { // If the popup is closed from the renderer via Cancel(), then ClosePopup() // has already run on another stack, and destroyed |page_|. If the popup is diff --git a/third_party/blink/renderer/core/exported/web_page_popup_impl.h b/third_party/blink/renderer/core/exported/web_page_popup_impl.h index d453230..c57d9eb 100644 --- a/third_party/blink/renderer/core/exported/web_page_popup_impl.h +++ b/third_party/blink/renderer/core/exported/web_page_popup_impl.h @@ -121,6 +121,7 @@ // Return the LayerTreeHost backing this popup widget. cc::LayerTreeHost* LayerTreeHostForTesting(); + void WidgetHostDisconnectedForTesting(); // Called when the browser has shown the popup. void DidShowPopup(); diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc index 5aef3bbc..cff1b407 100644 --- a/third_party/blink/renderer/core/exported/web_view_impl.cc +++ b/third_party/blink/renderer/core/exported/web_view_impl.cc @@ -1128,6 +1128,10 @@ void WebViewImpl::CancelPagePopup() { if (page_popup_) { + // Ensure page_popup_ lifetime lasts through the call even if the member + // variable is cleared by a callback during it. + scoped_refptr<WebPagePopupImpl> owning_reference(page_popup_); + page_popup_->Cancel(); } } @@ -1139,6 +1143,11 @@ if (page_popup_.get() != popup_impl) { return; } + + // Ensure page_popup_ lifetime lasts through the call even if the member + // variable is cleared by a callback during it. + scoped_refptr<WebPagePopupImpl> owning_reference(page_popup_); + page_popup_->ClosePopup(); } @@ -1149,6 +1158,10 @@ void WebViewImpl::UpdatePagePopup() { if (page_popup_) { + // Ensure page_popup_ lifetime lasts through the call even if the member + // variable is cleared by a callback during it. + scoped_refptr<WebPagePopupImpl> owning_reference(page_popup_); + page_popup_->Update(); } } 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 b1499f96..defc7d5 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 @@ -11,6 +11,8 @@ #include "third_party/blink/renderer/core/css/media_feature_overrides.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" +#include "third_party/blink/renderer/core/dom/events/native_event_listener.h" +#include "third_party/blink/renderer/core/event_type_names.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" @@ -192,6 +194,84 @@ EXPECT_TRUE(select->PopupIsVisible()); } +namespace { +class DisconnectOnChangeListener final : public NativeEventListener { + public: + explicit DisconnectOnChangeListener(WebPagePopupImpl* popup) + : popup_(popup) {} + void Invoke(ExecutionContext*, Event*) override { + if (popup_) { + popup_->WidgetHostDisconnectedForTesting(); + popup_ = nullptr; + } + } + + private: + raw_ptr<WebPagePopupImpl> popup_; +}; +} // namespace + +// See crbug.com/565774991. +TEST_F(InternalPopupMenuTest, DisconnectDuringPopupUpdate) { + if (!RuntimeEnabledFeatures::PagePopupEnabled()) { + return; + } + + frame_test_helpers::WebViewHelper web_view_helper; + WebViewImpl* web_view = web_view_helper.Initialize(); + if (web_view->GetChromeClient().UseExternalPopupMenus()) { + // <select> doesn't use a WebPagePopupImpl on this platform. + return; + } + web_view->MainFrameViewWidget()->Resize(gfx::Size(800, 600)); + + WebURL base_url = url_test_helpers::ToKURL("http://example.com/"); + frame_test_helpers::LoadHTMLString(web_view->MainFrameImpl(), R"HTML( + <!DOCTYPE html> + <style> + body { margin: 0; } + select { + position: absolute; + left: 0; + top: 0; + width: 100px; + height: 50px; + } + </style> + <select id="select"> + <option>1</option> + <option>2</option> + </select> + )HTML", + base_url); + + Document& document = + *web_view->MainFrameImpl()->GetDocument().Unwrap<Document>(); + document.View()->UpdateAllLifecyclePhasesForTest(); + + auto* select = + To<HTMLSelectElement>(document.getElementById(AtomicString("select"))); + ASSERT_TRUE(select); + + select->ShowPopup(); + EXPECT_TRUE(select->PopupIsVisible()); + + WebPagePopupImpl* popup = web_view->GetPagePopup(); + ASSERT_TRUE(popup); + popup->DidShowPopup(); + + select->ProvisionalSelectionChanged(1); + select->addEventListener( + event_type_names::kChange, + MakeGarbageCollected<DisconnectOnChangeListener>(popup)); + + select->SetInlineStyleProperty(CSSPropertyID::kPosition, "absolute"); + select->SetInlineStyleProperty(CSSPropertyID::kTop, "5000px"); + document.View()->UpdateAllLifecyclePhasesForTest(); + + web_view->UpdatePagePopup(); +} + #endif // !BUILDFLAG(IS_ANDROID) } // namespace blink
Regression Test / PoC
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 b1499f96..defc7d5 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
@@ -11,6 +11,8 @@
#include "third_party/blink/renderer/core/css/media_feature_overrides.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
+#include "third_party/blink/renderer/core/event_type_names.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"
@@ -192,6 +194,84 @@
EXPECT_TRUE(select->PopupIsVisible());
}
+namespace {
+class DisconnectOnChangeListener final : public NativeEventListener {
+ public:
+ explicit DisconnectOnChangeListener(WebPagePopupImpl* popup)
+ : popup_(popup) {}
+ void Invoke(ExecutionContext*, Event*) override {
+ if (popup_) {
+ popup_->WidgetHostDisconnectedForTesting();
+ popup_ = nullptr;
+ }
+ }
+
+ private:
+ raw_ptr<WebPagePopupImpl> popup_;
+};
+} // namespace
+
+// See crbug.com/565774991.
+TEST_F(InternalPopupMenuTest, DisconnectDuringPopupUpdate) {
+ if (!RuntimeEnabledFeatures::PagePopupEnabled()) {
+ return;
+ }
+
+ frame_test_helpers::WebViewHelper web_view_helper;
+ WebViewImpl* web_view = web_view_helper.Initialize();
+ if (web_view->GetChromeClient().UseExternalPopupMenus()) {
+ // <select> doesn't use a WebPagePopupImpl on this platform.
+ return;
+ }
+ web_view->MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+
+ WebURL base_url = url_test_helpers::ToKURL("http://example.com/");
+ frame_test_helpers::LoadHTMLString(web_view->MainFrameImpl(), R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body { margin: 0; }
+ select {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 50px;
+ }
+ </style>
+ <select id="select">
+ <option>1</option>
+ <option>2</option>
+ </select>
+ )HTML",
+ base_url);
+
+ Document& document =
+ *web_view->MainFrameImpl()->GetDocument().Unwrap<Document>();
+ document.View()->UpdateAllLifecyclePhasesForTest();
+
+ auto* select =
+ To<HTMLSelectElement>(document.getElementById(AtomicString("select")));
+ ASSERT_TRUE(select);
+
+ select->ShowPopup();
+ EXPECT_TRUE(select->PopupIsVisible());
+
+ WebPagePopupImpl* popup = web_view->GetPagePopup();
+ ASSERT_TRUE(popup);
+ popup->DidShowPopup();
+
+ select->ProvisionalSelectionChanged(1);
+ select->addEventListener(
+ event_type_names::kChange,
+ MakeGarbageCollected<DisconnectOnChangeListener>(popup));
+
+ select->SetInlineStyleProperty(CSSPropertyID::kPosition, "absolute");
+ select->SetInlineStyleProperty(CSSPropertyID::kTop, "5000px");
+ document.View()->UpdateAllLifecyclePhasesForTest();
+
+ web_view->UpdatePagePopup();
+}
+
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace blink
Original Bug Report
Heap-use-after-free in blink::WebPagePopupImpl::Update
Summary
WebPagePopupImpl::Update() calls popup_client_->Update(). When the <select> that owns an open drop-down has moved out of view, that call cancels the popup and dispatches change synchronously. If the page calls print() from that listener, the nested RunLoop delivers the popup widget’s disconnect and frees the WebPagePopupImpl. When print() returns, Update() reads through the freed object. Renderer process.
Environment
- Chrome for Testing 154.0.8037.57 (Stable), 155.0.8059.12 (Beta), 156.0.8072.0 (Canary); code unchanged on main b7c07f35773a
- Ubuntu 24.04, x86-64 (in-renderer
<select>popups: Linux, Windows, ChromeOS) - Evidence build: ASan, Chromium 155.0.8038.0
Reproduction
python3 poc.py 8000
chrome --user-data-dir=/tmp/p --no-first-run http://a.localhost:8000/poc.html
- Click the drop-down once to open it.
- Within 3 s, press Down-arrow once.
- Wait. The page moves the
<select>out of view,print()opens the preview, and the frame’s renderer crashes about 4.5 s after the click.
poc.py serves the page at two sites so the frame gets its own renderer. Rate: Stable 3/3, Beta 2/2, Canary 3/3.
Evidence
Renderer, main thread. Full reports: asan.txt, asan-dcheck.txt (DCHECK build, same bug, no DCHECK fires).
heap-use-after-free READ of size 8, 272 bytes inside 312-byte region
#0 WebPagePopupImpl::Update (WindowRectInScreen inlined, :664)
freed: WebPagePopupImpl close callback <- InterfaceEndpointClient::NotifyError
<- RunLoop::Run <- PrintRenderFrameHelper::RequestPrintPreview <- window.print
<- HTMLSelectElement::SelectOptionByPopup <- InternalPopupMenu::Update
<- WebPagePopupImpl::Update :662 (same call)
MiraclePtr Status: NOT PROTECTED
Vulnerable code
third_party/blink/renderer/core/exported/web_page_popup_impl.cc
662 popup_client_->Update(forced_update); [1]
663 if (forced_update) {
664 SetWindowRect(WindowRectInScreen()); [2]
[1] Can run script that frees this. [2] Reads widget_base_ from the freed object.
Impact
Web content plus two ordinary actions (open a drop-down, press an arrow key). Renderer use-after-free. Reporter’s estimate: High (S1); S2 if the interaction counts as mitigation.
Discovery and validation
Found by AI-assisted source review and dynamic testing (Claude).
Credit
Xinyang Ge (Anthropic), assisted by Claude
Attachments: poc.py, asan.txt, asan-dcheck.txt, report.md (full write-up)