← Chrome — 2026-W39

f9cea35d3e88ecc0b2edd526bd67ed2d56457bf0  Ensure sufficient lifetime of WebViewImpl::page_popup_.

severity high class UAF CVSS 8.8 signal score 9.45 third_party/blink/renderer/core/ patch-now AI review CONFIRMED
Structural signals: smartptr_member_added (+3.0), null_guard (+1.95), msg_medium (+2.0), area:third_party/blink/renderer/core/ (+2.5)

The bug at a glance

Renderer heap use-after-free of WebPagePopupImpl: a callback during page_popup_->Update()/Cancel()/ClosePopup() can clear WebViewImpl::page_popup_, dropping the last reference while the method is still executing on the object. Renderer memory corruption from web content is conventionally high; it needs a popup (e.g. picker) to be open, which typically requires a user gesture (inferred).

Script-run event handlers (e.g. a ‘change’ listener) fired from inside the popup update can close/disconnect the popup, freeing the object under its own call frame. This is a classic reentrancy-frees-this pattern useful for renderer code execution in a full chain.

Root cause

WebViewImpl held the popup only through the member page_popup_ and called page_popup_->Update()/Cancel()/ClosePopup() directly. Inside WebPagePopupImpl::Update, popup_client_->Update(forced_update) can run author script (the test fires a change event whose listener calls WidgetHostDisconnected()), which closes the popup and clears page_popup_, freeing the WebPagePopupImpl; the code then continued with SetWindowRect(…) on freed/closed state. The fix takes a local scoped_refptr owning reference in all three WebViewImpl entry points and adds a !widget_base_ early return after popup_client_->Update().

Key code

Keep popup alive across reentrant Update and bail if closed

+    scoped_refptr<WebPagePopupImpl> owning_reference(page_popup_);
+
     page_popup_->Update();
...
   popup_client_->Update(forced_update);
+  if (!widget_base_) {
+    // Something inside Update() closed this popup.
+    return;
+  }

Patch walkthrough

  • third_party/blink/renderer/core/exported/web_view_impl.cc — CancelPagePopup, ClosePagePopup and UpdatePagePopup now hold a scoped_refptr to page_popup_ for the duration of the call so a callback clearing the member cannot free the object mid-call.
  • third_party/blink/renderer/core/exported/web_page_popup_impl.cc — WebPagePopupImpl::Update returns if widget_base_ is null after popup_client_->Update(), since the popup may have been closed by reentrant script; also adds a test hook WidgetHostDisconnectedForTesting.
  • third_party/blink/renderer/core/exported/web_page_popup_impl.h — Declares the test-only WidgetHostDisconnectedForTesting().
  • third_party/blink/renderer/core/html/forms/internal_popup_menu_test.cc — Adds DisconnectDuringPopupUpdate: opens a popup, registers a change listener that disconnects the popup, moves the select, and calls UpdatePagePopup to exercise the reentrant close.

Background

WebPagePopupImpl — Ref-counted (not GC’d) renderer-side widget hosting popups like the picker and date/color choosers.

PagePopupClient::Update — Refreshes the popup’s content/position from its owner element and can dispatch DOM events such as ‘change’, running page script.

Reentrancy — Script run from inside a C++ method can tear down the object whose method is executing unless a strong reference is held.

Vulnerability window

  1. Reachability — Ordinary web content in a stock desktop build with an internal page popup open (e.g. picker, typically opened via user gesture (inferred)); Android uses external popup menus per the test.
  2. Trigger — A popup update (e.g. owner element moved/restyled) dispatches an event whose handler causes the popup to close, clearing page_popup_ during Update().
  3. Primitive — Use-after-free of the WebPagePopupImpl (and its widget state) in the renderer as Update continues after the last reference is dropped.
  4. Escalation — Reclaiming the freed object could yield renderer code execution, needing a separate sandbox escape (inferred).

Proof of concept

Conceptually: open a popup, register a handler that will run during the popup’s Update and causes the popup to be closed/disconnected, then force an update (e.g. move the select) so Update() continues on the freed popup. The added unit test models the close via a test-only WidgetHostDisconnected hook rather than a pure-web path.

Exploitation

  1. Setup — Open an internal popup (select picker) with a user gesture (inferred).
  2. Reentrancy — Use a DOM event fired from the popup update to close the popup, dropping the last reference (inferred web-exposed close path).
  3. Reclaim — Heap-groom to occupy the freed WebPagePopupImpl before subsequent member access (inferred).

Detection & hunting

For defenders and detection engineers:

  • weggli: ‘{ if ($m) { $m->$f(); } }’ where $m is a scoped_refptr/raw member and $f can run script — Find calls through a ref-counted member without a local owning reference when the callee can reenter script.
  • ASan heap-use-after-free in WebPagePopupImpl::Update / SetWindowRect — Fuzzer crash signature indicating the unpatched path.

Audit directions

  • WebViewImpl / WebPagePopupImpl other entry points (e.g. popup input handling, DidShowPopup) — Verify every call through page_popup_ that can run script holds an owning reference.
  • PagePopupClient implementations (InternalPopupMenu, color/date choosers) — Check whether their Update paths dispatch events and whether callers re-check widget liveness afterwards.
  • Other ref-counted, non-GC Blink widgets held as members — Look for the same self-destruction-during-callback pattern; the commit notes converting to GC as the long-term fix.

Before / after

Loading diff…