Chrome · Autofill
CVE-2026-87478
Logic Error in Autofill
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/html/forms/html_select_element_test.cc |
modified | |
forthird_party/blink/renderer/core/html/forms/html_select_element_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/dom/element.ccthird_party/blink/renderer/core/dom/scroll_marker_group_data.ccthird_party/blink/renderer/core/html/forms/html_select_element_test.cc
Patch
From b12935e03a81ffbfcbb68a792dc9d51ab6b968ea Mon Sep 17 00:00:00 2001 From: Jochen Eisinger <[email protected]> Date: Mon, 10 Aug 2026 03:52:21 -0700 Subject: [PATCH] [autofill] Implement unified popover preview for <select> controls Standardize <select> form controls (both MenuListSelectType and ListBoxSelectType) to render Autofill previews using a top-layer popover overlay (autofill_popover_ and autofill_popover_text_). In MenuListSelectType, remove the appearance:base-select condition so that any suggested option triggers the popover overlay, and update OptionToBeShown() to never return suggested options for standard menulists. In ListBoxSelectType, instantiate autofill_popover_ and autofill_popover_text_ during shadow DOM initialization and implement GetAutofillPreviewElement() to return the popover instance. Update html.css to apply full-control anchor sizing, start text alignment with inline padding, and enforce -webkit-small-control font pinning on the popover overlay. TAG=agy CONV=4259a896-ab67-4bff-bd6c-afa15ec4ad96 Bug: 40060525, 40057398, 518032534, 540015493, 517219513, 540021850 Change-Id: Ic0e2d79fd518479afeb36bf32870f889d32591f1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175874 Commit-Queue: Jochen Eisinger <[email protected]> Reviewed-by: Joey Arhar <[email protected]> Reviewed-by: Christoph Schwering <[email protected]> Cr-Commit-Position: refs/heads/main@{#1676341} --- diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc index 938dd8e7..cdfe1c4 100644 --- a/third_party/blink/renderer/core/dom/element.cc +++ b/third_party/blink/renderer/core/dom/element.cc @@ -2679,10 +2679,12 @@ } // Don't disclose scroll position in preview state. See crbug.com/1261689. - auto* select_element = DynamicTo<HTMLSelectElement>(this); - if (select_element && !select_element->UsesMenuList() && - select_element->IsPreviewed()) { - return 0; + if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) { + auto* select_element = DynamicTo<HTMLSelectElement>(this); + if (select_element && !select_element->UsesMenuList() && + select_element->IsPreviewed()) { + return 0; + } } LayoutBox* box = GetLayoutBoxForScrolling(); diff --git a/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc b/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc index feea272..3678350 100644 --- a/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc +++ b/third_party/blink/renderer/core/dom/scroll_marker_group_data.cc @@ -14,6 +14,7 @@ #include "third_party/blink/renderer/core/layout/layout_box.h" #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" #include "third_party/blink/renderer/core/scroll/scroll_into_view_util.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/wtf/wtf_size_t.h" namespace blink { @@ -590,9 +591,11 @@ // Form controls in autofill preview state may have been scrolled to bring // the previewed value into view. Keep the current selection so that the // suggested value cannot be observed via the selected scroll marker. - if (auto* form_control = DynamicTo<HTMLFormControlElement>(scroller); - form_control && form_control->IsPreviewed()) { - return selected_marker_; + if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) { + if (auto* form_control = DynamicTo<HTMLFormControlElement>(scroller); + form_control && form_control->IsPreviewed()) { + return selected_marker_; + } } LayoutBox* scroller_box = scroller->GetLayoutBox(); DCHECK(scroller_box); diff --git a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc index b2c7f16..67dbac7 100644 --- a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc +++ b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc @@ -110,6 +110,36 @@ EXPECT_EQ(select->UserHasEditedTheField(), true); } +TEST_F(HTMLSelectElementTest, MenuListAutofillPreviewDisabledFallback) { + ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false); + SetHtmlInnerHTML( + "<!DOCTYPE HTML><select id='sel'>" + "<option value='111' selected>111</option>" + "<option value='222'>222</option></select>"); + auto* select = To<HTMLSelectElement>(GetElementById("sel")); + + // MenuList always supports implicit anchor for the ::picker popover. + EXPECT_TRUE(select->MayBeImplicitAnchor()); + + // When SelectAutofillPopoverPreview is disabled, the shadow DOM popover + // preview element is omitted. + EXPECT_EQ(nullptr, select->GetAutofillPreviewElement()); + EXPECT_EQ("111", select->InnerElement().textContent()); + + // Setting the suggested value mutates the menulist inner text node directly + // via OptionToBeShown(). + select->SetSuggestedValue("222"); + ASSERT_TRUE(select->IsPreviewed()); + EXPECT_EQ("222", select->InnerElement().textContent()); + EXPECT_EQ("111", select->SelectedOption()->value()); + EXPECT_EQ(nullptr, select->GetAutofillPreviewElement()); + + // Clearing the preview restores the original selection's inner text. + select->SetSuggestedValue(""); + ASSERT_FALSE(select->IsPreviewed()); + EXPECT_EQ("111", select->InnerElement().textContent()); +} + TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) { StringBuilder html; html.Append( @@ -135,9 +165,8 @@ ASSERT_TRUE(group); ASSERT_EQ(group->Selected(), first_anchor); - // Setting the suggested option scrolls the listbox to bring it into view, - // but the selected scroll marker should not follow that scroll while the - // suggestion has not been accepted. + // Setting the suggested option displays a popover preview overlay without + // scrolling the listbox, so the selected scroll marker is unchanged. select->SetSuggestedValue("v15"); ASSERT_TRUE(select->IsPreviewed()); test::RunPendingTasks(); @@ -145,7 +174,7 @@ EXPECT_EQ(group->Selected(), first_anchor); // Once the suggestion is cleared and a value is committed, the selected - // scroll marker tracks the listbox scroll position again. + // option is scrolled into view and the scroll marker tracks it. select->setValueForBinding("v15"); ASSERT_FALSE(select->IsPreviewed()); test::RunPendingTasks(); @@ -153,6 +182,98 @@ EXPECT_NE(group->Selected(), first_anchor); } +TEST_F(HTMLSelectElementTest, + ListBoxAutofillPreviewDoesNotScrollOrResetScroll) { + StringBuilder html; + html.Append("<!DOCTYPE HTML><select id='sel' size='4'>"); + for (int i = 0; i < 20; ++i) { + html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i, + i); + } + html.Append("</select>"); + SetHtmlInnerHTML(html.ToString().Utf8()); + test::RunPendingTasks(); + UpdateAllLifecyclePhasesForTest(); + + auto* select = To<HTMLSelectElement>(GetElementById("sel")); + + // When enabled, listbox supports implicit anchor for autofill popover. + EXPECT_TRUE(select->MayBeImplicitAnchor()); + + // 1. Initial preview does not scroll the listbox. + EXPECT_EQ(0.0, select->scrollTop()); + select->SetSuggestedValue("v15"); + ASSERT_TRUE(select->IsPreviewed()); + test::RunPendingTasks(); + UpdateAllLifecyclePhasesForTest(); + EXPECT_EQ(0.0, select->scrollTop()); + + // 2. Clear preview. + select->SetSuggestedValue(""); + ASSERT_FALSE(select->IsPreviewed()); + test::RunPendingTasks(); + UpdateAllLifecyclePhasesForTest(); + EXPECT_EQ(0.0, select->scrollTop()); + + // 3. User scrolls the listbox. + select->setScrollTop(50); + test::RunPendingTasks(); + UpdateAllLifecyclePhasesForTest(); + double scrolled_top = select->scrollTop(); + EXPECT_GT(scrolled_top, 0.0); + + // 4. Setting a suggested value does not alter the listbox scroll position or + // mask scrollTop(). + select->SetSuggestedValue("v15"); + ASSERT_TRUE(select->IsPreviewed()); + test::RunPendingTasks(); + UpdateAllLifecyclePhasesForTest(); + EXPECT_EQ(scrolled_top, select->scrollTop()); + + // 5. Clearing the suggested value preserves the user's scroll position. + select->SetSuggestedValue(""); + ASSERT_FALSE(select->IsPreviewed()); + test::RunPendingTasks();
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
index b2c7f16..67dbac7 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element_test.cc
@@ -110,6 +110,36 @@
EXPECT_EQ(select->UserHasEditedTheField(), true);
}
+TEST_F(HTMLSelectElementTest, MenuListAutofillPreviewDisabledFallback) {
+ ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false);
+ SetHtmlInnerHTML(
+ "<!DOCTYPE HTML><select id='sel'>"
+ "<option value='111' selected>111</option>"
+ "<option value='222'>222</option></select>");
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // MenuList always supports implicit anchor for the ::picker popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // When SelectAutofillPopoverPreview is disabled, the shadow DOM popover
+ // preview element is omitted.
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+
+ // Setting the suggested value mutates the menulist inner text node directly
+ // via OptionToBeShown().
+ select->SetSuggestedValue("222");
+ ASSERT_TRUE(select->IsPreviewed());
+ EXPECT_EQ("222", select->InnerElement().textContent());
+ EXPECT_EQ("111", select->SelectedOption()->value());
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+
+ // Clearing the preview restores the original selection's inner text.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ EXPECT_EQ("111", select->InnerElement().textContent());
+}
+
TEST_F(HTMLSelectElementTest, ListBoxSuggestedOptionScrollTargetGroup) {
StringBuilder html;
html.Append(
@@ -135,9 +165,8 @@
ASSERT_TRUE(group);
ASSERT_EQ(group->Selected(), first_anchor);
- // Setting the suggested option scrolls the listbox to bring it into view,
- // but the selected scroll marker should not follow that scroll while the
- // suggestion has not been accepted.
+ // Setting the suggested option displays a popover preview overlay without
+ // scrolling the listbox, so the selected scroll marker is unchanged.
select->SetSuggestedValue("v15");
ASSERT_TRUE(select->IsPreviewed());
test::RunPendingTasks();
@@ -145,7 +174,7 @@
EXPECT_EQ(group->Selected(), first_anchor);
// Once the suggestion is cleared and a value is committed, the selected
- // scroll marker tracks the listbox scroll position again.
+ // option is scrolled into view and the scroll marker tracks it.
select->setValueForBinding("v15");
ASSERT_FALSE(select->IsPreviewed());
test::RunPendingTasks();
@@ -153,6 +182,98 @@
EXPECT_NE(group->Selected(), first_anchor);
}
+TEST_F(HTMLSelectElementTest,
+ ListBoxAutofillPreviewDoesNotScrollOrResetScroll) {
+ StringBuilder html;
+ html.Append("<!DOCTYPE HTML><select id='sel' size='4'>");
+ for (int i = 0; i < 20; ++i) {
+ html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i,
+ i);
+ }
+ html.Append("</select>");
+ SetHtmlInnerHTML(html.ToString().Utf8());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // When enabled, listbox supports implicit anchor for autofill popover.
+ EXPECT_TRUE(select->MayBeImplicitAnchor());
+
+ // 1. Initial preview does not scroll the listbox.
+ EXPECT_EQ(0.0, select->scrollTop());
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 2. Clear preview.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // 3. User scrolls the listbox.
+ select->setScrollTop(50);
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ double scrolled_top = select->scrollTop();
+ EXPECT_GT(scrolled_top, 0.0);
+
+ // 4. Setting a suggested value does not alter the listbox scroll position or
+ // mask scrollTop().
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(scrolled_top, select->scrollTop());
+
+ // 5. Clearing the suggested value preserves the user's scroll position.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(scrolled_top, select->scrollTop());
+}
+
+TEST_F(HTMLSelectElementTest, ListBoxAutofillPreviewDisabledFallback) {
+ ScopedSelectAutofillPopoverPreviewForTest disable_popover_preview(false);
+ StringBuilder html;
+ html.Append("<!DOCTYPE HTML><select id='sel' size='4'>");
+ for (int i = 0; i < 20; ++i) {
+ html.AppendFormat("<option id='o%d' value='v%d'>option %d</option>", i, i,
+ i);
+ }
+ html.Append("</select>");
+ SetHtmlInnerHTML(html.ToString().Utf8());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+
+ auto* select = To<HTMLSelectElement>(GetElementById("sel"));
+
+ // Popover preview element is omitted when feature is disabled.
+ EXPECT_EQ(nullptr, select->GetAutofillPreviewElement());
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // Setting the suggested value scrolls the listbox to the previewed option,
+ // but scrollTop() is masked to 0.0 to prevent scroll disclosure.
+ select->SetSuggestedValue("v15");
+ ASSERT_TRUE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+
+ // Clearing the preview resets the scroll position to the first selectable
+ // option.
+ select->SetSuggestedValue("");
+ ASSERT_FALSE(select->IsPreviewed());
+ test::RunPendingTasks();
+ UpdateAllLifecyclePhasesForTest();
+ EXPECT_EQ(0.0, select->scrollTop());
+}
+
TEST_F(HTMLSelectElementTest, SaveRestoreSelectSingleFormControlState) {
SetHtmlInnerHTML(
"<!DOCTYPE HTML><select id='sel'>"
diff --git a/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html b/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
new file mode 100644
index 0000000..6fdbab4
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:[email protected]">
+<link rel=help href="https://issues.chromium.org/issues/40060525">
+<link rel=help href="https://issues.chromium.org/issues/40057398">
+<script src="../../../resources/testharness.js"></script>
+<script src="../../../resources/testharnessreport.js"></script>
+<script src="../resources/common.js"></script>
+<style>
+ @font-face {
+ font-family: Ahem;
+ src: url(../../../resources/Ahem.ttf);
+ }
+ .use-ahem {
+ font-family: Ahem;
+ font-size: 40px;
+ font-feature-settings: "case", "ss01";
+ width: 200px;
+ height: 100px;
+ }
+</style>
+
+<select id="menulist" class="use-ahem">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+</select>
+
+<select id="listbox" class="use-ahem" size="4">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+</select>
+
+<select id="listbox-scrollable" size="4">
+ <option value="v1">Value 1</option>
+ <option value="v2">Value 2</option>
+ <option value="v3">Value 3</option>
+ <option value="v4">Value 4</option>
+ <option value="v5">Value 5</option>
+ <option value="v6">Value 6</option>
+ <option value="v7">Value 7</option>
+ <option value="v8">Value 8</option>
+</select>
+
+<script>
+function testAutofillPopoverPreview(id, description) {
+ promise_test(async () => {
+ await document.fonts.ready;
+ const select = document.getElementById(id);
+ assert_true(!!select, `Element ${id} should exist`);
+
+ const initialStyle = window.getComputedStyle(select);
+ assert_equals(initialStyle.fontFamily, 'Ahem', `${id} should use author font initially`);
+
+ internals.setSuggestedValue(select, 'v2');
+
+ const shadowRoot = internals.shadowRoot(select);
+ const popover = getElementByPseudoId(shadowRoot, '-internal-select-autofill-preview');
+ assert_true(!!popover, `${id} autofill_popover_ element should exist in shadow DOM`);
+ assert_true(popover.matches(':popover-open'), `${id} autofill_popover_ should be open in preview state`);
+ assert_equals(window.getComputedStyle(popover).display, 'grid', `${id} autofill_popover_ display should be grid when open`);
+
+ // Full-element anchor sizing dimensions
+ assert_equals(window.getComputedStyle(popover).positionArea, 'center', `${id} autofill_popover_ position-area should be center`);
+ assert_equals(popover.offsetWidth, select.offsetWidth, `${id} autofill_popover_ width should match anchor select width`);
+ assert_equals(popover.offsetHeight, select.offsetHeight, `${id} autofill_popover_ height should match anchor select height`);
+
+ // Text-align start and text content
+ const popoverText = getElementByPseudoId(shadowRoot, '-internal-select-autofill-preview-text');
+ assert_true(!!popoverText, `${id} autofill_popover_text_ should exist in shadow DOM`);
+ assert_equals(popoverText.textContent, 'Value 2', `${id} autofill_popover_text_ should contain the label of the previewed option`);
+ const textStyle = window.getComputedStyle(popoverText);
+ assert_equals(textStyle.textAlign, 'start', `${id} autofill_popover_text_ should have text-align: start`);
+
+ // -webkit-small-control font pinning
+ assert_not_equals(textStyle.fontFamily, 'Ahem', `${id} autofill_popover_text_ should not use author font family in preview state`);
+ assert_not_equals(textStyle.fontSize, '40px', `${id} autofill_popover_text_ should pin font size to -webkit-small-control`);
+ assert_equals(textStyle.fontFeatureSettings, 'normal', `${id} autofill_popover_text_ should pin font-feature-settings to normal`);
+
+ // End preview
+ internals.setSuggestedValue(select, '');
+ assert_false(popover.matches(':popover-open'), `${id} autofill_popover_ should close when preview ends`);
+ assert_equals(window.getComputedStyle(popover).display, 'none', `${id} autofill_popover_ display should be none when closed`);
+ }, description);
+}
+
+testAutofillPopoverPreview('menulist', 'Autofill popover preview on menulist <select>');
+testAutofillPopoverPreview('listbox', 'Autofill popover preview on listbox <select>');
+
+test(() => {
+ const select = document.getElementById('listbox-scrollable');
+ assert_equals(select.scrollTop, 0);
+
+ // Previewing an option beyond the visible items does not scroll the listbox.
+ internals.setSuggestedValue(select, 'v8');
+ assert_equals(select.scrollTop, 0, 'scrollTop should remain 0 when previewing off-screen option');
+
+ internals.setSuggestedValue(select, '');
+ assert_equals(select.scrollTop, 0, 'scrollTop should remain 0 after clearing preview');
+
+ // User scrolls the listbox.
+ select.scrollTop = 30;
+ const userScrollTop = select.scrollTop;
+ assert_greater_than(userScrollTop, 0, 'listbox should be scrolled');
+
+ // Previewing does not modify or mask the user scroll position.
+ internals.setSuggestedValue(select, 'v8');
+ assert_equals(select.scrollTop, userScrollTop, 'scrollTop should match user scroll position during preview');
+
+ // Ending preview preserves the user scroll position without resetting.
+ internals.setSuggestedValue(select, '');
+ assert_equals(select.scrollTop, userScrollTop, 'scrollTop should be preserved after clearing preview');
+}, 'Autofill popover preview does not scroll or reset listbox scroll position');
+</script>
+
diff --git a/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html b/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
new file mode 100644
index 0000000..6088baf7
--- /dev/null
+++ b/third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<link rel="author" href="mailto:[email protected]">
+<link rel="help" href="https://issues.chromium.org/issues/40060525">
+<script src="../../../resources/testharness.js"></script>
+<script src="../../../resources/testharnessreport.js"></script>
+<script src="../resources/common.js"></script>
+<style>
+ select {
+ width: 200px;
+ height: 100px;
+ }
+</style>
+
+<select id="listbox" size="4">
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page