f9cea35d3e88ecc0b2edd526bd67ed2d56457bf0 Ensure sufficient lifetime of WebViewImpl::page_popup_.
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
- 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.
- 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().
- Primitive — Use-after-free of the WebPagePopupImpl (and its widget state) in the renderer as Update continues after the last reference is dropped.
- 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
- Setup — Open an internal popup (select picker) with a user gesture (inferred).
- Reentrancy — Use a DOM event fired from the popup update to close the popup, dropping the last reference (inferred web-exposed close path).
- 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.