Medium watch CVSS 5.3 chrome Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactObservable discrepancy in CSS
DescriptionObservable discrepancy in CSS
ComponentCSS
Sub-componentAutofill
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker518032534
Fix commitb12935e03a81 (chromium/src) +429/-58
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-08-10 (public git)
Disclosed2026-09-15
Patch gapNot computed: several commits cite this bug and it is not yet established which one is the fix.

Background

Autofill preview state
a transient rendering mode where a form control (here an HTMLSelectElement) displays a browser-suggested value the user has not yet accepted, tracked by HTMLFormControlElement::IsPreviewed() and set via SetSuggestedValue().
`MenuListSelectType` vs `ListBoxSelectType`
the two SelectType subclasses backing a <select>: a collapsed dropdown (UsesMenuList() true) versus an expanded, scrollable list (size attribute set), each with its own shadow DOM and preview behavior.
Autofill preview popover
a top-layer manual popover (autofill_popover_ holding autofill_popover_text_) that overlays the suggested value on top of the control without mutating the control’s own scroll offset or selection.
Observable discrepancy
a side effect (scroll offset, ::scroll-marker selection, rendered geometry) that a script or CSS on the hosting page can read, leaking state the page should not see — here, the not-yet-accepted suggested value.

Root Cause Analysis

For an expanded listbox <select>, ListBoxSelectType::GetAutofillPreviewElement() returned nullptr (an unimplemented TODO(crbug.com/357649033)), so there was no overlay to show the suggested value; instead ListBoxSelectType::DidSetSuggestedOption() previewed the value by scrolling the listbox to bring the previewed option into view and adjusting its displayed selection. Because that scroll and selection change are properties of the control’s own layout box, they were observable from the page, so the code tried to paper over the leak by masking the scroll offset to 0 in the scroll accessor in element.cc and by pinning selected_marker_ in scroll_marker_group_data.cc when IsPreviewed() was true. That masking is incomplete and fragile — it only covers the two specific channels the authors anticipated, leaving other CSS-observable consequences of the real scroll/selection mutation exposed, which is the observable discrepancy.

The fix introduces a unified SelectType::CreateAutofillPopover() / SelectType::DidSetSuggestedOption() that renders the preview in a top-layer popover overlay for both control types, so the underlying listbox is never scrolled and its selection is never changed while previewing. With the preview no longer causing any state mutation, the ad-hoc masks in element.cc and scroll_marker_group_data.cc are gated off behind the now-stable SelectAutofillPopoverPreview flag, and MenuListSelectType::OptionToBeShown() is updated to stop returning the suggested option inline.

The fix works because it removes the disclosure at the source rather than trying to enumerate and mask every observable consequence.

Key insight
The single mistake was previewing a suggested value for listbox <select>s by mutating the control’s real scroll position and selection and then trying to hide those mutations with targeted masks, which inevitably leaves observable side channels; the fix renders the preview in a non-mutating top-layer popover overlay so there is nothing observable to mask.

Attack Path

  1. Set up an observable listbox An attacker page renders a <select size=N> styled with CSS whose rendering depends on scroll position or ::scroll-marker state (or that reads scrollTop/scroll markers via script), for a field the browser will offer to autofill.
  2. Trigger autofill preview The user hovers a browser autofill suggestion (or the page induces a suggested value), driving SetSuggestedValue() and ListBoxSelectType::DidSetSuggestedOption() into preview state without the user accepting the value.
  3. Value-dependent scroll occurs In the pre-fix path the listbox is scrolled to bring the previewed option into view, so the control’s scroll offset and internal selection now encode which option was suggested.
  4. Read the leaked state Although the scroll accessor returns 0 and the selected marker is pinned, other CSS-observable consequences of the real scroll/selection change remain readable, letting the page infer the previewed value the user never consented to reveal (inferred: precise readout channel beyond scrollTop/scroll-marker is not enumerated in the diff).

Impact Assessment

An attacker-controlled page can observe a not-yet-accepted autofill suggested value in an expanded <select> control, disclosing potentially sensitive user data (e.g., profile-derived option selections) within the renderer, without user consent. Preconditions are that the user has autofill data applicable to the page’s <select> and that an autofill preview is triggered for it. This is an information-disclosure / observable-discrepancy logic error in CSS/forms rendering; no memory-safety, sandbox-escape, or code-execution capability is demonstrated by the patch (inferred: no escalation beyond disclosure), and the advisory reports it as an observable discrepancy in CSS with no in-the-wild exploitation stated.

Changed Functions

FunctionChangeNotes
GetAutofillPreviewElement
third_party/blink/renderer/core/html/forms/select_type.cc
modified
if
third_party/blink/renderer/core/html/forms/select_type.cc
modified
TEST_F
third_party/blink/renderer/core/html/forms/html_select_element_test.cc
modified

Files Changed

  • third_party/blink/renderer/platform/runtime_enabled_features.json5
  • third_party/blink/web_tests/fast/forms/suggested-value-after-empty-suggested-value-expected.txt
  • third_party/blink/renderer/core/html/forms/select_type.cc
  • third_party/blink/web_tests/fast/forms/select/select-autofill-popover-preview.html
  • third_party/blink/renderer/core/html/resources/html.css
  • third_party/blink/renderer/core/html/forms/select_type.h
  • third_party/blink/web_tests/fast/forms/select/select-autofill-scroll-oracle.html
  • third_party/blink/renderer/core/dom/scroll_marker_group_data.cc
  • third_party/blink/renderer/core/html/forms/html_select_element_test.cc
  • third_party/blink/renderer/core/dom/element.cc

Audit Directions

  • Preview/masking band-aids
    Audit any feature that masks a getter (returning 0, pinning a marker, suppressing an event) to hide a real state mutation performed during preview/suggestion states; the mutation itself typically leaks through other CSS/layout channels, so prefer eliminating the mutation.
  • Unimplemented preview surfaces
    Look for GetAutofillPreviewElement()-style hooks that return nullptr with a TODO, where the caller then falls back to mutating the real control; these fallbacks are prime observable-discrepancy sources.
  • Suggested-value read paths
    Review every place IsPreviewed()/suggested_option_ influences observable output (OptionToBeShown(), scroll offsets, ::scroll-marker, :checked, intrinsic size) to ensure the not-yet-accepted value cannot be reconstructed by the page.
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/select_type.cc b/third_party/blink/renderer/core/html/forms/select_type.cc
index 05d7f972..5886114 100644
--- a/third_party/blink/renderer/core/html/forms/select_type.cc
+++ b/third_party/blink/renderer/core/html/forms/select_type.cc
@@ -294,8 +294,6 @@
   bool IsAppearanceBasePicker() const override;
   bool PickerIsPopover() const override;
   void SetIsAppearanceBasePickerForDisplayNone(bool) override;
-  HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
-      const override;
   Element& InnerElement() const override;
   void ShowPopup(PopupMenu::ShowEventType type) override;
   void HidePopup(SelectPopupHideBehavior) override;
@@ -325,8 +323,6 @@
   Member<const ComputedStyle> option_style_;
   Member<HTMLSlotElement> button_slot_;
   Member<PopoverElementForAppearanceBase> popover_;
-  Member<HTMLSelectElement::SelectAutofillPreviewElement> autofill_popover_;
-  Member<HTMLDivElement> autofill_popover_text_;
   Member<HTMLSlotElement> popover_input_slot_;
   Member<HTMLSlotElement> popover_options_slot_;
   // TODO(40146374): The option_slot_ can be removed when the CustomizableSelect
@@ -354,8 +350,6 @@
   visitor->Trace(option_style_);
   visitor->Trace(button_slot_);
   visitor->Trace(popover_);
-  visitor->Trace(autofill_popover_);
-  visitor->Trace(autofill_popover_text_);
   visitor->Trace(popover_input_slot_);
   visitor->Trace(popover_options_slot_);
   visitor->Trace(option_slot_);
@@ -620,18 +614,9 @@
   popover_options_slot_ = MakeGarbageCollected<HTMLSlotElement>(doc);
   popover_options_slot_->SetIdAttribute(shadow_element_names::kSelectOptions);
 
-  autofill_popover_ =
-      MakeGarbageCollected<HTMLSelectElement::SelectAutofillPreviewElement>(
-          doc, select_);
-  autofill_popover_->SetShadowPseudoId(
-      shadow_element_names::kSelectAutofillPreview);
-  root.appendChild(autofill_popover_);
-  autofill_popover_->setAttribute(html_names::kPopoverAttr, keywords::kManual);
-
-  autofill_popover_text_ = MakeGarbageCollected<HTMLDivElement>(doc);
-  autofill_popover_text_->SetShadowPseudoId(
-      shadow_element_names::kSelectAutofillPreviewText);
-  autofill_popover_->appendChild(autofill_popover_text_);
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    CreateAutofillPopover(root);
+  }
 
   if (RuntimeEnabledFeatures::FilterableSelectEnabled() &&
       select_->NumDescendantInputs()) {
@@ -662,6 +647,10 @@
     popover_input_slot_ = nullptr;
     popover_->AppendChild(popover_options_slot_);
   }
+
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    CreateAutofillPopover(root);
+  }
 }
 
 void MenuListSelectType::ManuallyAssignSlots() {
@@ -779,11 +768,6 @@
   is_appearance_base_picker_for_display_none_ = value;
 }
 
-HTMLSelectElement::SelectAutofillPreviewElement*
-MenuListSelectType::GetAutofillPreviewElement() const {
-  return autofill_popover_;
-}
-
 Element& MenuListSelectType::InnerElement() const {
   return *inner_element_;
 }
@@ -1001,17 +985,9 @@
   if (native_popup_is_visible_) {
     popup_->UpdateFromElement(PopupMenu::kBySelectionChange);
   }
-  if (select_->IsAppearanceBase()) {
-    if (option) {
-      autofill_popover_->ShowPopoverInternal(select_, &ASSERT_NO_EXCEPTION);
-      autofill_popover_text_->setInnerText(option->label());
-    } else {
-      autofill_popover_text_->setInnerText(g_empty_string);
-      autofill_popover_->HidePopoverInternal(
-          /*invoker=*/nullptr, HidePopoverFocusBehavior::kNone,
-          HidePopoverTransitionBehavior::kNoEventsNoWaiting,
-          /*exception_state=*/nullptr);
-    }
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() ||
+      select_->IsAppearanceBase()) {
+    SelectType::DidSetSuggestedOption(option);
   }
 }
 
@@ -1175,7 +1151,8 @@
     return option;
   // In appearance:base-select mode, we don't want to reveal the suggested
   // option anywhere except in autofill_popover_.
-  if (select_->suggested_option_ && !select_->IsAppearanceBase()) {
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() &&
+      select_->suggested_option_ && !select_->IsAppearanceBase()) {
     return select_->suggested_option_.Get();
   }
   // TODO(tkent): We should not call OptionToBeShown() in IsMultiple() case.
@@ -1303,8 +1280,6 @@
   bool IsAppearanceBasePicker() const override;
   bool PickerIsPopover() const override;
   void SetIsAppearanceBasePickerForDisplayNone(bool) override;
-  HTMLSelectElement::SelectAutofillPreviewElement* GetAutofillPreviewElement()
-      const override;
   void DidRecalcStyle(const StyleRecalcChange) override;
 
  private:
@@ -1664,6 +1639,10 @@
 void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
   if (!select_->GetLayoutObject())
     return;
+  if (RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled()) {
+    SelectType::DidSetSuggestedOption(option);
+    return;
+  }
   // When ending preview state, don't leave the scroll position at the
   // previewed element but return to the active selection end if it is
   // defined or to the first selectable option. See crbug.com/1261689.
@@ -1996,6 +1975,8 @@
     root.appendChild(option_slot_);
     listbox_ = nullptr;
   }
+
+  CreateAutofillPopover(root);
 }
 
 void ListBoxSelectType::ManuallyAssignSlots() {
@@ -2052,12 +2033,6 @@
                   "UsesMenuList(). ListBoxSelectType does not have a picker.";
 }
 
-HTMLSelectElement::SelectAutofillPreviewElement*
-ListBoxSelectType::GetAutofillPreviewElement() const {
-  // TODO(crbug.com/357649033): Implement this
-  return nullptr;
-}
-
 void ListBoxSelectType::DidRecalcStyle(const StyleRecalcChange) {
   UpdateOptionMutationObservers(/*in_style_recalc=*/true);
 }
@@ -2108,6 +2083,50 @@
 
 void SelectType::Trace(Visitor* visitor) const {
   visitor->Trace(select_);
+  visitor->Trace(autofill_popover_);
+  visitor->Trace(autofill_popover_text_);
+}
+
+void SelectType::CreateAutofillPopover(ShadowRoot& root) {
+  if (!RuntimeEnabledFeatures::SelectAutofillPopoverPreviewEnabled() &&
+      !select_->IsAppearanceBase()) {
+    return;
+  }
+  select_->SetMayBeImplicitAnchor();
+  Document& doc = select_->GetDocument();
+  autofill_popover_ =
+      MakeGarbageCollected<HTMLSelectElement::SelectAutofillPreviewElement>(
+          doc, select_);
+  autofill_popover_->SetShadowPseudoId(
+      shadow_element_names::kSelectAutofillPreview);
+  root.appendChild(autofill_popover_);
+  autofill_popover_->setAttribute(html_names::kPopoverAttr, keywords::kManual);
+
+  autofill_popover_text_ = MakeGarbageCollected<HTMLDivElement>(doc);
+  autofill_popover_text_->SetShadowPseudoId(
+      shadow_element_names::kSelectAutofillPreviewText);
+  autofill_popover_->appendChild(autofill_popover_text_);
+}
+
+HTMLSelectElement::SelectAutofillPreviewElement*
+SelectType::GetAutofillPreviewElement() const {
+  return autofill_popover_;
+}
+
+void SelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
+  if (!autofill_popover_) {
+    return;
+  }
+  if (option) {
+    autofill_popover_->ShowPopoverInternal(select_, &ASSERT_NO_EXCEPTION);
+    autofill_popover_text_->setInnerText(option->label());
+  } else {
+    autofill_popover_text_->setInnerText(g_empty_string);
+    autofill_popover_->HidePopoverInternal(
+        /*invoker=*/nullptr, HidePopoverFocusBehavior::kNone,
+        HidePopoverTransitionBehavior::kNoEventsNoWaiting,
+        /*exception_state=*/nullptr);
+  }
 }
 
 void SelectType::OptionRemoved(HTMLOptionElement& option) {}
diff --git a/third_party/blink/renderer/core/html/forms/select_type.h b/third_party/blink/renderer/core/html/forms/select_type.h
index aba5b06..3c04fd057 100644
--- a/third_party/blink/renderer/core/html/forms/select_type.h
+++ b/third_party/blink/renderer/core/html/forms/select_type.h
@@ -35,7 +35,7 @@
   virtual void DidBlur() = 0;
   virtual void DidDetachLayoutTree();
   virtual void DidRecalcStyle(const StyleRecalcChange change);
-  virtual void DidSetSuggestedOption(HTMLOptionElement* option) = 0;
+  virtual void DidSetSuggestedOption(HTMLOptionElement* option);
   virtual void SaveLastSelection() = 0;
 
   // Update style of text in the CSS box on style or selected OPTION change.
@@ -72,7 +72,7 @@
   virtual bool PickerIsPopover() const = 0;
   virtual void SetIsAppearanceBasePickerForDisplayNone(bool) = 0;
   virtual HTMLSelectElement::SelectAutofillPreviewElement*
-  GetAutofillPreviewElement() const = 0;
+  GetAutofillPreviewElement() const;
   virtual Element& InnerElement() const;
   virtual void ShowPopup(PopupMenu::ShowEventType type);
   virtual void HidePopup(SelectPopupHideBehavior);
@@ -95,7 +95,11 @@
                                      SkipDirection direction,
                                      int skip) const;
 
+  void CreateAutofillPopover(ShadowRoot& root);
+
   const Member<HTMLSelectElement> select_;
+  Member<HTMLSelectElement::SelectAutofillPreviewElement> autofill_popover_;
+  Member<HTMLDivElement> autofill_popover_text_;
   bool will_be_destroyed_ = false;
 
  private:
diff --git a/third_party/blink/renderer/core/html/resources/html.css b/third_party/blink/renderer/core/html/resources/html.css
index 789de46..9bd97f6e 100644
--- a/third_party/blink/renderer/core/html/resources/html.css
+++ b/third_party/blink/renderer/core/html/resources/html.css
@@ -2355,7 +2355,8 @@
 select::-internal-select-autofill-preview-text {
   overflow: hidden;
   text-overflow: ellipsis;
-  text-align: center;
+  text-align: start;
+  padding-inline: -internal-auto-base(2px, 0.5em);
   white-space: nowrap;
   font: -webkit-small-control !important;
   /* See input::-internal-input-suggested: pin a generic family so an author
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index fa184ab7..9a56eac8 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -5473,6 +5473,10 @@
       },
     },
     {
+      name: "SelectAutofillPopoverPreview",
+      status: "stable",
+    },
+    {
       // The selectedcontentelement attribute is for connecting select elements
       // to arbitrary <selectedcontent> elements.
       // https://github.com/openui/open-ui/issues/1063
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);
 
... (file diff truncated)
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.