Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in HTML
DescriptionUse after free in HTML
ComponentHTML
Bug ClassUAF
Tracker502109002
Fix commit89091966d023 (chromium/src) +73/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • third_party/blink/renderer/core/exported/web_view_impl.cc
  • third_party/blink/renderer/core/html/forms/color_input_type.cc
  • third_party/blink/renderer/core/html/forms/file_input_type.cc
  • third_party/blink/renderer/platform/runtime_enabled_features.json5
  • third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
From 89091966d023d524c27d97fa98fe752be8304b0c Mon Sep 17 00:00:00 2001
From: Joey Arhar <jarhar@chromium.org>
Date: Tue, 26 May 2026 12:16:28 -0700
Subject: [PATCH] Consume user activation in input.click() to show picker

Certain input types, like <input type=color> and <input type=file>, can
show their picker when calling input.click(). This already checks for
user activation, but does not actually consume it. input.showPicker(),
on the other hand, checks for and consumes user activation.

Not consuming user activation can lead to issues where multiple pickers
can be opened at the same time, which leads to issues since there is
code which assumes that only one can be open at a time.

This patch also upgrades a DCHECK for only one picker being open at a
time to a CHECK.

Fixed: 502109002
Change-Id: I9842095f417cb470412bbbe5a7241600ce45e5eb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852999
Reviewed-by: David Baron <dbaron@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1636387}
---

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 45a11bc1..eb8ca629 100644
--- a/third_party/blink/renderer/core/exported/web_view_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_view_impl.cc
@@ -1050,11 +1050,19 @@
 }
 
 WebPagePopupImpl* WebViewImpl::OpenPagePopup(PagePopupClient* client) {
-  DCHECK(client);
+  CHECK(client);
 
   // This guarantees there is never more than 1 PagePopup active at a time.
+  // CancelPagePopup may fire a synchronous change event when a select element
+  // is closed, but that event handler shouldn't be able to open another picker
+  // because it would require multiple user activations to be created and
+  // consumed within the same task.
   CancelPagePopup();
-  DCHECK(!page_popup_);
+  if (RuntimeEnabledFeatures::FileColorPickerConsumeActivationEnabled()) {
+    CHECK(!page_popup_);
+  } else {
+    DCHECK(!page_popup_);
+  }
 
   LocalFrame* opener_frame = client->OwnerElement().GetDocument().GetFrame();
   WebLocalFrameImpl* web_opener_frame =
diff --git a/third_party/blink/renderer/core/html/forms/color_input_type.cc b/third_party/blink/renderer/core/html/forms/color_input_type.cc
index 03e54549..490cdfa 100644
--- a/third_party/blink/renderer/core/html/forms/color_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/color_input_type.cc
@@ -178,6 +178,9 @@
         "A user gesture is required to show the color picker."));
     return;
   }
+  if (RuntimeEnabledFeatures::FileColorPickerConsumeActivationEnabled()) {
+    LocalFrame::ConsumeTransientUserActivation(document.GetFrame());
+  }
 
   ChromeClient* chrome_client = GetChromeClient();
   if (chrome_client && !HasOpenedPopup()) {
diff --git a/third_party/blink/renderer/core/html/forms/file_input_type.cc b/third_party/blink/renderer/core/html/forms/file_input_type.cc
index 794b0d1..8382530 100644
--- a/third_party/blink/renderer/core/html/forms/file_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/file_input_type.cc
@@ -183,6 +183,9 @@
         mojom::ConsoleMessageLevel::kWarning, message));
     return;
   }
+  if (RuntimeEnabledFeatures::FileColorPickerConsumeActivationEnabled()) {
+    LocalFrame::ConsumeTransientUserActivation(document.GetFrame());
+  }
 
   OpenPopupView();
   event.SetDefaultHandled();
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 79c18ae8..2dda0d0 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -2767,6 +2767,13 @@
       status: "stable",
     },
     {
+      // Consumes user activation when showing a picker via input.click() for
+      // file and color inputs to match input.showPicker() and avoid issues
+      // with multiple pickers trying to open at the same time.
+      name: "FileColorPickerConsumeActivation",
+      status: "stable",
+    },
+    {
       // Also enabled when blink::features::kFileHandlingAPI is overridden
       // on the command line (or via chrome://flags).
       name: "FileHandling",
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
new file mode 100644
index 0000000..ff9e019
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html class=test-wait>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://issues.chromium.org/issues/502109002">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+
+<select id=s1>
+  <option>one</option>
+  <option>two</option>
+</select>
+
+<input type=color>
+
+<select id=s2>
+  <option>one</option>
+  <option>two</option>
+</select>
+
+<script>
+const enterKey = '\uE007';
+const arrowDown = '\uE015';
+
+function pressKey(key) {
+  return (new test_driver.Actions()
+    .keyDown(key)
+    .keyUp(key))
+    .send();
+}
+
+const s1 = document.getElementById('s1');
+const colorInput = document.querySelector('input');
+const s2 = document.getElementById('s2');
+
+(async () => {
+  s1.focus();
+  await pressKey(enterKey);
+  await pressKey(arrowDown);
+
+  s1.addEventListener('change', () => {
+    s2.showPicker();
+  });
+  colorInput.click();
+
+  await new Promise(requestAnimationFrame);
+  await new Promise(setTimeout);
+  document.documentElement.classList.remove('test-wait');
+})();
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
new file mode 100644
index 0000000..ff9e019
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html class=test-wait>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://issues.chromium.org/issues/502109002">
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+
+<select id=s1>
+  <option>one</option>
+  <option>two</option>
+</select>
+
+<input type=color>
+
+<select id=s2>
+  <option>one</option>
+  <option>two</option>
+</select>
+
+<script>
+const enterKey = '\uE007';
+const arrowDown = '\uE015';
+
+function pressKey(key) {
+  return (new test_driver.Actions()
+    .keyDown(key)
+    .keyUp(key))
+    .send();
+}
+
+const s1 = document.getElementById('s1');
+const colorInput = document.querySelector('input');
+const s2 = document.getElementById('s2');
+
+(async () => {
+  s1.focus();
+  await pressKey(enterKey);
+  await pressKey(arrowDown);
+
+  s1.addEventListener('change', () => {
+    s2.showPicker();
+  });
+  colorInput.click();
+
+  await new Promise(requestAnimationFrame);
+  await new Promise(setTimeout);
+  document.documentElement.classList.remove('test-wait');
+})();
+</script>
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in WebViewImpl via re-entrant OpenPagePopup

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 without the Chrome Security team.

Overview: A Use-After-Free vulnerability exists in Blink’s handling of page popups due to a re-entrancy flaw in WebViewImpl::OpenPagePopup. Synchronous JavaScript execution during popup cancellation can create a secondary popup whose reference is later overwritten by the outer call. When the orphaned popup is eventually freed, its client retains a dangling raw pointer that can be dereferenced, potentially leading to arbitrary code execution.

Affected files:

  • third_party/blink/renderer/core/exported/web_view_impl.cc
  • third_party/blink/renderer/core/exported/web_page_popup_impl.cc
  • third_party/blink/renderer/core/html/forms/internal_popup_menu.cc
  • third_party/blink/renderer/core/html/forms/color_chooser_popup_ui_controller.cc
  • third_party/blink/renderer/core/html/forms/select_type.cc
  • third_party/blink/renderer/core/html/forms/html_select_element.cc

Estimated timestamp from git blame: 2022-06-29

Description

A potential Use-After-Free (UAF) vulnerability exists in the Blink renderer’s handling of page popups (e.g., <select> menus and color pickers). The issue arises from re-entrant calls to WebViewImpl::OpenPagePopup, which can be triggered by synchronous JavaScript execution during the cancellation of an existing popup.

When WebViewImpl::OpenPagePopup is called, it first calls CancelPagePopup() to maintain a single-popup invariant. CancelPagePopup() systematically closes the active popup, which invokes HTMLSelectElement::PopupDidCancel(). If there is a provisional selection, this method synchronously dispatches a change event. An attacker can use a change event listener to immediately open another popup, causing a re-entrant call to OpenPagePopup.

The inner OpenPagePopup successfully creates a WebPagePopupImpl instance, assigns it to the WebViewImpl::page_popup_ scoped_refptr, and returns the raw pointer to the client (e.g., InternalPopupMenu), which stores it in a PagePopup* member variable.

Once the synchronous script finishes, the outer OpenPagePopup resumes execution. It creates its own WebPagePopupImpl instance and overwrites WebViewImpl::page_popup_. The inner popup is now orphaned from WebViewImpl but remains alive due to a self-reference.

When the browser process detects the inner widget is orphaned, it initiates closure, triggering WebPagePopupImpl::Close(). This calls WebViewImpl::ClosePagePopup(popup). However, ClosePagePopup checks if page_popup_.get() == popup. Since page_popup_ now points to the outer popup, the check fails, and the method early-returns. Consequently, the client’s DidClosePopup() is never called, and its raw pointer is never nulled. The WebPagePopupImpl then drops its self-reference and is freed, leaving the client with a dangling pointer.

Because WebPagePopupImpl is allocated via PartitionAlloc (USING_FAST_MALLOC) and the client holds a bare pointer (PagePopup*) rather than raw_ptr<T>, MiraclePtr does not mitigate this vulnerability.

Potential Steps to Trigger

Note: These are suggested steps to trigger the vulnerability based on code analysis; our tooling has not executed this as a live proof-of-concept.

  1. An attacker serves an HTML page containing <select id="s1">, <input type="color" id="c1">, and <select id="s2">.
  2. The user interacts with s1, opening the popup and provisionally selecting an option.
  3. A user gesture (e.g., keypress) triggers an event listener that calls c1.click(). Because of the user gesture, this passes transient activation checks.
  4. WebViewImpl::OpenPagePopup is called for c1 (the “outer” call). It calls CancelPagePopup() to close s1.
  5. Closing s1 commits the provisional selection, firing a synchronous change event.
  6. The attacker’s change event listener calls s2.showPicker(), triggering the “inner” re-entrant call to WebViewImpl::OpenPagePopup.
  7. The inner call creates Popup C, assigns it to WebViewImpl::page_popup_, and s2’s InternalPopupMenu stores a raw pointer to Popup C.
  8. The inner call completes, the event listener finishes, and the outer OpenPagePopup resumes.
  9. The outer call creates Popup B and overwrites WebViewImpl::page_popup_ with it.
  10. The browser destroys the orphaned Popup C widget, invoking WebPagePopupImpl::Close().
  11. WebViewImpl::ClosePagePopup bails out early because page_popup_ points to Popup B. The InternalPopupMenu is not notified, leaving its popup_ pointer dangling.
  12. Popup C is freed from memory.
  13. The attacker script mutates s2’s DOM (e.g., adding an <option>). This triggers InternalPopupMenu::Update(), which attempts to send the new data to the UI by calling popup_->PostMessageToPopup().
  14. The virtual function call dereferences the freed memory. If the attacker has used heap spraying, they can hijack the vtable to achieve arbitrary code execution.

Suggested Fix

  1. Lifecycle fix (Primary): Prevent re-entrancy in WebViewImpl::OpenPagePopup. This could be done by using a boolean flag (is_opening_popup_) and ignoring/crashing on nested calls, or by establishing an EventDispatchForbiddenScope or EventQueueScope around the popup teardown/creation logic to defer synchronous JavaScript execution.
  2. Memory safety fix (Defense in depth): Migrate the raw PagePopup* popup_ pointer in clients (such as InternalPopupMenu and ColorChooserPopupUIController) to base::raw_ptr<PagePopup>. This ensures that even if the lifecycle logic fails, the vulnerability is mitigated to a safe crash via MiraclePtr.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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