Chrome · Select
CVE-2026-14077
Logic Error in Select
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
Elementthird_party/blink/renderer/core/html/forms/external_popup_menu.h |
modified | |
HTMLSelectElementthird_party/blink/renderer/core/html/forms/external_popup_menu.h |
modified | |
LocalFramethird_party/blink/renderer/core/html/forms/external_popup_menu.h |
modified | |
WebMouseEventthird_party/blink/renderer/core/html/forms/external_popup_menu.h |
modified | |
TEST_Fthird_party/blink/renderer/core/html/forms/external_popup_menu_test.cc |
modified | |
forthird_party/blink/renderer/core/html/forms/external_popup_menu_test.cc |
modified | |
ifthird_party/blink/renderer/core/html/forms/external_popup_menu_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/html/forms/external_popup_menu.ccthird_party/blink/renderer/core/html/forms/external_popup_menu.hthird_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
Patch
From 9dd7d49061ff6271c74f4dba9d90e11ae2a3dafc Mon Sep 17 00:00:00 2001
From: Joey Arhar <jarhar@chromium.org>
Date: Mon, 01 Jun 2026 13:20:11 -0700
Subject: [PATCH] Make select popup anchor stay within web contents
This prevents the popup from being rendered entirely outside of the web
contents which could overlap the browser ui.
Fixed: 511869411
Change-Id: I239e1ff4d7212666153d432e3c10e18ba860dbec
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876426
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Mason Freed <masonf@chromium.org>
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639621}
---
diff --git a/third_party/blink/renderer/core/html/forms/external_popup_menu.cc b/third_party/blink/renderer/core/html/forms/external_popup_menu.cc
index 63acc60..211da4a 100644
--- a/third_party/blink/renderer/core/html/forms/external_popup_menu.cc
+++ b/third_party/blink/renderer/core/html/forms/external_popup_menu.cc
@@ -42,6 +42,7 @@
#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/frame/web_frame_widget_impl.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h"
@@ -59,9 +60,8 @@
namespace blink {
-namespace {
-
-float GetDprForSizeAdjustment(const Element& owner_element) {
+// static
+float ExternalPopupMenu::GetDprForSizeAdjustment(const Element& owner_element) {
float dpr = 1.0f;
// Android doesn't need these adjustments and it makes tests fail.
#ifndef OS_ANDROID
@@ -83,8 +83,6 @@
return dpr;
}
-} // namespace
-
ExternalPopupMenu::ExternalPopupMenu(LocalFrame& frame,
HTMLSelectElement& owner_element)
: owner_element_(owner_element),
@@ -142,6 +140,22 @@
rect_in_viewport = gfx::ScaleToRoundedRect(rect_in_viewport, 1 / dpr);
}
+ // Adjust anchor position to stay within web contents, otherwise the popup
+ // could be rendered entirely outside of the web contents. If this select
+ // is in a cross-origin iframe, then the anchor will be confined to the
+ // bounds of the iframe rather than the entire web contents.
+ if (RuntimeEnabledFeatures::SelectAnchorInViewportEnabled() &&
+ local_frame_->GetPage()) {
+ gfx::Rect viewport_rect(
+ local_frame_->GetPage()->GetVisualViewport().Size());
+ // rect_in_viewport should always overlap with viewport_rect. If the
+ // select element is positioned outside of the viewport, then
+ // MenuListSelectType::ShowPopup has an early return which prevents us
+ // from getting here.
+ CHECK(viewport_rect.Intersects(rect_in_viewport));
+ rect_in_viewport.Intersect(viewport_rect);
+ }
+
gfx::Rect bounds =
gfx::Rect(rect_in_viewport.x() * scale_for_emulation,
rect_in_viewport.y() * scale_for_emulation,
diff --git a/third_party/blink/renderer/core/html/forms/external_popup_menu.h b/third_party/blink/renderer/core/html/forms/external_popup_menu.h
index b74da76..e520025a 100644
--- a/third_party/blink/renderer/core/html/forms/external_popup_menu.h
+++ b/third_party/blink/renderer/core/html/forms/external_popup_menu.h
@@ -42,6 +42,7 @@
namespace blink {
+class Element;
class HTMLSelectElement;
class LocalFrame;
class WebMouseEvent;
@@ -67,6 +68,7 @@
bool* allow_multiple_selection);
static int ToPopupMenuItemIndex(int index, HTMLSelectElement&);
static int ToExternalPopupMenuItemIndex(int index, HTMLSelectElement&);
+ static float GetDprForSizeAdjustment(const Element& owner_element);
void Trace(Visitor*) const override;
diff --git a/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc b/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
index c74a437..ea628c2d 100644
--- a/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
+++ b/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
@@ -6,6 +6,7 @@
#include <memory>
+#include "base/strings/stringprintf.h"
#include "content/test/test_blink_web_unit_test_support.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/choosers/popup_menu.mojom-blink.h"
@@ -348,6 +349,104 @@
EXPECT_EQ(expected_y, ShownBounds().y());
}
+TEST_F(ExternalPopupMenuTest, PopupClippedToViewportVariations) {
+ struct TestCase {
+ int left;
+ int top;
+ int width;
+ int height;
+ float dpr;
+ bool expect_shown;
+ int expected_x;
+ int expected_y;
+ int expected_width;
+ int expected_height;
+ } test_cases[] = {
+ // Top left partial
+ {-50, -50, 200, 200, 1.0f, true, 0, 0, 150, 150},
+ // Top left partial with DPR
+// The android differences here and below correspond to the OS_ANDROID check in
+// ExternalPopupMenu::GetDprForSizeAdjustment.
+#ifdef OS_ANDROID
+ {-50, -50, 200, 200, 2.0f, true, 0, 0, 300, 300},
+#else
+ {-50, -50, 200, 200, 2.0f, true, 0, 0, 150, 150},
+#endif
+ // Top left complete
+ {-250, -250, 200, 200, 1.0f, false, 0, 0, 0, 0},
+ // Top left complete with DPR
+ {-250, -250, 200, 200, 2.0f, false, 0, 0, 0, 0},
+ // Bottom right partial
+ {750, 550, 200, 200, 1.0f, true, 750, 550, 50, 50},
+ // Bottom right partial with DPR
+#ifdef OS_ANDROID
+ {750, 550, 200, 200, 2.0f, true, 1500, 1100, 100, 100},
+#else
+ {750, 550, 200, 200, 2.0f, true, 750, 550, 200, 200},
+#endif
+ // Bottom right complete
+ {850, 650, 200, 200, 1.0f, false, 0, 0, 0, 0},
+ // Bottom right complete with DPR
+ {850, 650, 200, 200, 2.0f, false, 0, 0, 0, 0},
+ };
+
+ for (const auto& test_case : test_cases) {
+ SCOPED_TRACE(testing::Message()
+ << "left: " << test_case.left << ", top: " << test_case.top
+ << ", dpr: " << test_case.dpr);
+
+ WebView()->MainFrameWidget()->SetDeviceScaleFactorForTesting(test_case.dpr);
+
+ frame_test_helpers::LoadHTMLString(
+ MainFrame(),
+ base::StringPrintf(R"HTML(
+ <!DOCTYPE html>
+ <html>
+ <style>
+ body { margin: 0; }
+ select {
+ position: fixed;
+ left: %dpx;
+ top: %dpx;
+ width: %dpx;
+ height: %dpx;
+ }
+ </style>
+ <body>
+ <select id=select>
+ <option>option</option>
+ </select>
+ </body>
+ </html>
+ )HTML",
+ test_case.left, test_case.top, test_case.width, test_case.height),
+ url_test_helpers::ToKURL("http://www.test.com/"));
+
+ WebView()->MainFrameViewWidget()->Resize(
+ gfx::Size(800 * test_case.dpr, 600 * test_case.dpr));
+ WebView()->MainFrameWidget()->UpdateAllLifecyclePhases(
+ DocumentUpdateReason::kTest);
+
+ auto* select = To<HTMLSelectElement>(
+ MainFrame()->GetFrame()->GetDocument()->getElementById(
+ AtomicString("select")));
+ auto* layout_object = select->GetLayoutObject();
+ ASSERT_TRUE(layout_object);
+
+ select->ShowPopup();
+
+ if (test_case.expect_shown) {
+ WaitUntilShowedPopup();
+ EXPECT_EQ(test_case.expected_x, ShownBounds().x());
+ EXPECT_EQ(test_case.expected_y, ShownBounds().y());
+ EXPECT_EQ(test_case.expected_width, ShownBounds().width());
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc b/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
index c74a437..ea628c2d 100644
--- a/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
+++ b/third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
@@ -6,6 +6,7 @@
#include <memory>
+#include "base/strings/stringprintf.h"
#include "content/test/test_blink_web_unit_test_support.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/choosers/popup_menu.mojom-blink.h"
@@ -348,6 +349,104 @@
EXPECT_EQ(expected_y, ShownBounds().y());
}
+TEST_F(ExternalPopupMenuTest, PopupClippedToViewportVariations) {
+ struct TestCase {
+ int left;
+ int top;
+ int width;
+ int height;
+ float dpr;
+ bool expect_shown;
+ int expected_x;
+ int expected_y;
+ int expected_width;
+ int expected_height;
+ } test_cases[] = {
+ // Top left partial
+ {-50, -50, 200, 200, 1.0f, true, 0, 0, 150, 150},
+ // Top left partial with DPR
+// The android differences here and below correspond to the OS_ANDROID check in
+// ExternalPopupMenu::GetDprForSizeAdjustment.
+#ifdef OS_ANDROID
+ {-50, -50, 200, 200, 2.0f, true, 0, 0, 300, 300},
+#else
+ {-50, -50, 200, 200, 2.0f, true, 0, 0, 150, 150},
+#endif
+ // Top left complete
+ {-250, -250, 200, 200, 1.0f, false, 0, 0, 0, 0},
+ // Top left complete with DPR
+ {-250, -250, 200, 200, 2.0f, false, 0, 0, 0, 0},
+ // Bottom right partial
+ {750, 550, 200, 200, 1.0f, true, 750, 550, 50, 50},
+ // Bottom right partial with DPR
+#ifdef OS_ANDROID
+ {750, 550, 200, 200, 2.0f, true, 1500, 1100, 100, 100},
+#else
+ {750, 550, 200, 200, 2.0f, true, 750, 550, 200, 200},
+#endif
+ // Bottom right complete
+ {850, 650, 200, 200, 1.0f, false, 0, 0, 0, 0},
+ // Bottom right complete with DPR
+ {850, 650, 200, 200, 2.0f, false, 0, 0, 0, 0},
+ };
+
+ for (const auto& test_case : test_cases) {
+ SCOPED_TRACE(testing::Message()
+ << "left: " << test_case.left << ", top: " << test_case.top
+ << ", dpr: " << test_case.dpr);
+
+ WebView()->MainFrameWidget()->SetDeviceScaleFactorForTesting(test_case.dpr);
+
+ frame_test_helpers::LoadHTMLString(
+ MainFrame(),
+ base::StringPrintf(R"HTML(
+ <!DOCTYPE html>
+ <html>
+ <style>
+ body { margin: 0; }
+ select {
+ position: fixed;
+ left: %dpx;
+ top: %dpx;
+ width: %dpx;
+ height: %dpx;
+ }
+ </style>
+ <body>
+ <select id=select>
+ <option>option</option>
+ </select>
+ </body>
+ </html>
+ )HTML",
+ test_case.left, test_case.top, test_case.width, test_case.height),
+ url_test_helpers::ToKURL("http://www.test.com/"));
+
+ WebView()->MainFrameViewWidget()->Resize(
+ gfx::Size(800 * test_case.dpr, 600 * test_case.dpr));
+ WebView()->MainFrameWidget()->UpdateAllLifecyclePhases(
+ DocumentUpdateReason::kTest);
+
+ auto* select = To<HTMLSelectElement>(
+ MainFrame()->GetFrame()->GetDocument()->getElementById(
+ AtomicString("select")));
+ auto* layout_object = select->GetLayoutObject();
+ ASSERT_TRUE(layout_object);
+
+ select->ShowPopup();
+
+ if (test_case.expect_shown) {
+ WaitUntilShowedPopup();
+ EXPECT_EQ(test_case.expected_x, ShownBounds().x());
+ EXPECT_EQ(test_case.expected_y, ShownBounds().y());
+ EXPECT_EQ(test_case.expected_width, ShownBounds().width());
+ EXPECT_EQ(test_case.expected_height, ShownBounds().height());
+ } else {
+ EXPECT_FALSE(select->PopupIsVisible());
+ }
+ }
+}
+
// Android doesn't use this position data and we don't adjust it for DPR there..
#ifdef OS_ANDROID
#define MAYBE_PopupAccountsForDeviceScaleFactor \
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