Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DOM
DescriptionUse after free in DOM
ComponentDOM
Bug ClassUAF
Tracker516936863
Fix commit9800c213a58d (chromium/src) +62/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/exported/web_page_popup_impl.cc
modified
TEST_F
third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc
modified
if
third_party/blink/renderer/core/page/page_popup_controller.cc
modified
PagePopup
third_party/blink/renderer/core/page/page_popup_controller.h
modified
PagePopupClient
third_party/blink/renderer/core/page/page_popup_controller.h
modified
PagePopupController
third_party/blink/renderer/core/page/page_popup_controller.h
modified
CORE_EXPORT
third_party/blink/renderer/core/page/page_popup_controller.h
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/page/page_popup_controller.cc
  • third_party/blink/renderer/core/page/page_popup_controller.h
From 9800c213a58d1eedefae6b64aebf98a5bda62c81 Mon Sep 17 00:00:00 2001
From: Mason Freed <masonf@chromium.org>
Date: Tue, 09 Jun 2026 10:30:48 -0700
Subject: [PATCH] Fix UAF in PagePopupController via orphaned popup destruction teardown

When PagePopup cancellation occurs, re-entrant page popup creation can
overwrite or clear WebViewImpl's tracking reference, leaving an
orphaned popup that bypasses standard teardown in ClosePopup(). If a
pending DOM timer or asynchronous script task subsequently invokes
setWindowRect() on the orphaned frame, it attempts a virtual method
call on the freed WebPagePopupImpl referent.

The fix includes:
1. Guard PagePopupController::setWindowRect() with an explicit check on
popup_client_.
2. In WebPagePopupImpl::Close(), if page_ remains non-null after
Cancel() returns, explicitly invoke ClearPagePopupClient() and
DestroyPage() so the frame and scheduler are detached before freeing.

Fixed: 516936863
Change-Id: I218c94955949e8a07535239eecf05350c56ab24a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7905043
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644092}
---

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 4e26d684..a327509 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
@@ -1032,6 +1032,15 @@
     closing_ = true;
     // This should end up running ClosePopup() though the PopupClient.
     Cancel();
+    // Cancel() may have synchronously triggered ClosePopup() and destroyed
+    // page_.
+    if (page_) {
+      EventDispatchForbiddenScope::AllowUserAgentEvents allow_events;
+      if (auto* controller = PagePopupController::From(*page_)) {
+        controller->ClearPagePopupClient();
+      }
+      DestroyPage();
+    }
   }
 
   // TODO(dtapuska): WidgetBase shutdown should happen before Page is
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 2350e26..e3b105e 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
@@ -12,14 +12,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 +112,48 @@
   popup2->ClosePopup();
 }
 
+// See crbug.com/516936863.
+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(
+    <select id=sel>
+      <option>1</option>
+      <option>2</option>
+    </select>
+  )HTML",
+                                     base_url);
+  Document& document =
+      *web_view->MainFrameImpl()->GetDocument().Unwrap<Document>();
+  document.View()->UpdateAllLifecyclePhasesForTest();
+
+  auto* sel =
+      To<HTMLSelectElement>(document.getElementById(AtomicString("sel")));
+  ASSERT_TRUE(sel);
+  auto* menu = MakeGarbageCollected<InternalPopupMenu>(
+      MakeGarbageCollected<EmptyChromeClient>(), *sel);
+
+  WebPagePopupImpl* popup = web_view->OpenPagePopup(menu);
+  popup->DidShowPopup();
+
+  Page* popup_page = popup->GetDocument().Unwrap<Document>()->GetPage();
+  ASSERT_TRUE(popup_page);
+  Persistent<PagePopupController> controller =
+      PagePopupController::From(*popup_page);
+  ASSERT_TRUE(controller);
+
+  web_view->CleanupPagePopup();
+
+  test::RunPendingTasks();
+
+  controller->setWindowRect(0, 0, 100, 100);
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/page/page_popup_controller.cc b/third_party/blink/renderer/core/page/page_popup_controller.cc
index ee99845..20a878d3 100644
--- a/third_party/blink/renderer/core/page/page_popup_controller.cc
+++ b/third_party/blink/renderer/core/page/page_popup_controller.cc
@@ -117,6 +117,10 @@
 }
 
 void PagePopupController::setWindowRect(int x, int y, int width, int height) {
+  if (!popup_client_) {
+    return;
+  }
+
   popup_.SetWindowRect(gfx::Rect(x, y, width, height));
 
   popup_origin_ = gfx::Point(x, y);
diff --git a/third_party/blink/renderer/core/page/page_popup_controller.h b/third_party/blink/renderer/core/page/page_popup_controller.h
index 2e96310..583dee4 100644
--- a/third_party/blink/renderer/core/page/page_popup_controller.h
+++ b/third_party/blink/renderer/core/page/page_popup_controller.h
@@ -33,6 +33,7 @@
 
 #include <optional>
 
+#include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
@@ -49,7 +50,8 @@
 class PagePopup;
 class PagePopupClient;
 
-class PagePopupController : public ScriptWrappable, public Supplement<Page> {
+class CORE_EXPORT PagePopupController : public ScriptWrappable,
+                                        public Supplement<Page> {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
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 2350e26..e3b105e 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
@@ -12,14 +12,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 +112,48 @@
   popup2->ClosePopup();
 }
 
+// See crbug.com/516936863.
+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(
+    <select id=sel>
+      <option>1</option>
+      <option>2</option>
+    </select>
+  )HTML",
+                                     base_url);
+  Document& document =
+      *web_view->MainFrameImpl()->GetDocument().Unwrap<Document>();
+  document.View()->UpdateAllLifecyclePhasesForTest();
+
+  auto* sel =
+      To<HTMLSelectElement>(document.getElementById(AtomicString("sel")));
+  ASSERT_TRUE(sel);
+  auto* menu = MakeGarbageCollected<InternalPopupMenu>(
+      MakeGarbageCollected<EmptyChromeClient>(), *sel);
+
+  WebPagePopupImpl* popup = web_view->OpenPagePopup(menu);
+  popup->DidShowPopup();
+
+  Page* popup_page = popup->GetDocument().Unwrap<Document>()->GetPage();
+  ASSERT_TRUE(popup_page);
+  Persistent<PagePopupController> controller =
+      PagePopupController::From(*popup_page);
+  ASSERT_TRUE(controller);
+
+  web_view->CleanupPagePopup();
+
+  test::RunPendingTasks();
+
+  controller->setWindowRect(0, 0, 100, 100);
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace blink
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in PagePopupController::setWindowRect via Re-entrant Popup Creation

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in PagePopupController because it holds a raw C++ reference to an off-heap WebPagePopupImpl. Re-entrant page popup creation during popup cancellation can overwrite or clear WebViewImpl’s tracking reference, leaving an orphaned popup that is destructed without full teardown. If a pending JavaScript task subsequently invokes setWindowRect in the orphaned context, it performs a virtual function call on the freed WebPagePopupImpl object.

Affected files:

  • third_party/blink/renderer/core/page/page_popup_controller.h
  • third_party/blink/renderer/core/page/page_popup_controller.cc
  • third_party/blink/renderer/core/exported/web_page_popup_impl.cc
  • third_party/blink/renderer/core/exported/web_view_impl.cc

Estimated timestamp from git blame: 2015-04-15

Potential Use-After-Free in PagePopupController

Analysis

PagePopupController stores a raw, unmanaged C++ reference to PagePopup:

// third_party/blink/renderer/core/page/page_popup_controller.h
PagePopup& popup_;

This reference points to a WebPagePopupImpl instance, which is allocated off-heap using PartitionAlloc and does not benefit from MiraclePtr protection.

During normal operation, WebViewImpl manages the active popup via scoped_refptr<WebPagePopupImpl> page_popup_. However, during a re-entrant OpenPagePopup sequence (for example, if a synchronous JS event is dispatched while a popup is being cancelled or closed), a new popup can be initialized before the previous popup’s teardown completes. Because DCHECK assertions are compiled out in production builds, this re-entrant flow can overwrite the active page_popup_ pointer or cause CleanupPagePopup() to set page_popup_ to nullptr prematurely.

When a popup is orphaned this way:

  1. It retains a reference count of 1 (held by its self-reference for the visible widget).
  2. When the browser process disconnects the widget host for the orphaned popup, WebPagePopupImpl::WidgetHostDisconnected() calls Close(), which initiates Cancel().
  3. Cancel() routes back to WebViewImpl::ClosePagePopup(), which checks:
    if (page_popup_.get() != popup_impl)
      return;
    
    Since page_popup_ no longer points to the orphaned popup (or is nullptr), this check fails and early-returns, completely bypassing ClosePopup() and DestroyPage().
  4. Consequently, the orphaned popup is immediately destructed when its refcount drops to 0, but its associated LocalFrame and DOM window are never properly detached or shut down.
  5. Any pending asynchronous tasks (e.g., DOM timers) still queued in the orphaned frame’s context can execute on subsequent event loop ticks and call window.pagePopupController.setWindowRect().
  6. This invokes PagePopupController::setWindowRect(), which performs a pure virtual method call (popup_.SetWindowRect(...)) on the deleted WebPagePopupImpl object, leading to a potential use-after-free and renderer remote code execution.

Potential Trigger Scenario (Conceptual)

Note: These are potential steps that need verification as our current tooling does not run code.

  1. A page opens a form-associated popup (Popup A).
  2. The attacker triggers a re-entrant popup request (Popup B) via synchronous event listeners (e.g., during events dispatched when Popup A is cancelled or closed).
  3. In release builds, this overwrites/invalidates WebViewImpl::page_popup_, orphaning Popup A.
  4. When the browser process disconnects the orphaned popup’s host widget, its destructor runs but its frame and page scheduler are bypassed during teardown and remain active.
  5. An asynchronous timer or event listener in Popup A’s context executes after the object is freed and invokes setWindowRect, triggering the virtual call UAF.

Suggested Remediation

  1. Avoid storing raw C++ references to PagePopup in PagePopupController. Instead, use a nullable pointer or a weak reference pattern that can be safely invalidated during teardown.
  2. Harden WebViewImpl::OpenPagePopup against re-entrancy by ensuring that popup creation requests are safely deferred or rejected if a page popup cancellation/closure sequence is currently active on the stack.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker