Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Select
DescriptionUse after free in Select
ComponentSelect
Sub-componentBlink>Forms>Select
Bug ClassUAF
Tracker565774991
Fix commitf9cea35d3e88 (chromium/src) +102/-0
CISA KEVNot listed
CreditedXinyang Ge (Anthropic), assisted by Claude
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/exported/web_page_popup_impl.cc
modified
if
third_party/blink/renderer/core/exported/web_view_impl.cc
modified
DisconnectOnChangeListener
third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc
modified
if
third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc
modified
TEST_F
third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/exported/web_page_popup_impl.cc
  • third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc
  • third_party/blink/renderer/core/exported/web_page_popup_impl.h
  • third_party/blink/renderer/core/exported/web_view_impl.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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
  1. Click the drop-down once to open it.
  2. Within 3 s, press Down-arrow once.
  3. 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)

View on issue tracker