Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Select
DescriptionIncorrect security UI in Select
ComponentSelect
Bug ClassLogic Error
Tracker511869411
Fix commit9dd7d49061ff (chromium/src) +137/-5
CISA KEVNot listed
Creditedpwn.ai
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
Element
third_party/blink/renderer/core/html/forms/external_popup_menu.h
modified
HTMLSelectElement
third_party/blink/renderer/core/html/forms/external_popup_menu.h
modified
LocalFrame
third_party/blink/renderer/core/html/forms/external_popup_menu.h
modified
WebMouseEvent
third_party/blink/renderer/core/html/forms/external_popup_menu.h
modified
TEST_F
third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
modified
for
third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
modified
if
third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/html/forms/external_popup_menu.cc
  • third_party/blink/renderer/core/html/forms/external_popup_menu.h
  • third_party/blink/renderer/core/html/forms/external_popup_menu_test.cc
From 9dd7d49061ff6271c74f4dba9d90e11ae2a3dafc Mon Sep 17 00:00:00 2001
From: Joey Arhar <[email protected]>
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 <[email protected]>
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
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.