CVE-2026-13899
Overview
Files Changed
third_party/blink/renderer/core/exported/web_view_impl.ccthird_party/blink/renderer/core/html/forms/color_input_type.ccthird_party/blink/renderer/core/html/forms/file_input_type.ccthird_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/multiple-pickers-crash.html
Patch
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>
Regression Test / PoC
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>
Original Bug Report
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.ccthird_party/blink/renderer/core/exported/web_page_popup_impl.ccthird_party/blink/renderer/core/html/forms/internal_popup_menu.ccthird_party/blink/renderer/core/html/forms/color_chooser_popup_ui_controller.ccthird_party/blink/renderer/core/html/forms/select_type.ccthird_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.
- An attacker serves an HTML page containing
<select id="s1">,<input type="color" id="c1">, and<select id="s2">. - The user interacts with
s1, opening the popup and provisionally selecting an option. - A user gesture (e.g., keypress) triggers an event listener that calls
c1.click(). Because of the user gesture, this passes transient activation checks. WebViewImpl::OpenPagePopupis called forc1(the “outer” call). It callsCancelPagePopup()to closes1.- Closing
s1commits the provisional selection, firing a synchronouschangeevent. - The attacker’s
changeevent listener callss2.showPicker(), triggering the “inner” re-entrant call toWebViewImpl::OpenPagePopup. - The inner call creates Popup C, assigns it to
WebViewImpl::page_popup_, ands2’sInternalPopupMenustores a raw pointer to Popup C. - The inner call completes, the event listener finishes, and the outer
OpenPagePopupresumes. - The outer call creates Popup B and overwrites
WebViewImpl::page_popup_with it. - The browser destroys the orphaned Popup C widget, invoking
WebPagePopupImpl::Close(). WebViewImpl::ClosePagePopupbails out early becausepage_popup_points to Popup B. TheInternalPopupMenuis not notified, leaving itspopup_pointer dangling.- Popup C is freed from memory.
- The attacker script mutates
s2’s DOM (e.g., adding an<option>). This triggersInternalPopupMenu::Update(), which attempts to send the new data to the UI by callingpopup_->PostMessageToPopup(). - 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
- 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 anEventDispatchForbiddenScopeorEventQueueScopearound the popup teardown/creation logic to defer synchronous JavaScript execution. - Memory safety fix (Defense in depth): Migrate the raw
PagePopup* popup_pointer in clients (such asInternalPopupMenuandColorChooserPopupUIController) tobase::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.