Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in HTML
DescriptionMemory corruption in HTML
ComponentHTML
Bug ClassMemory Corruption
Tracker553335083
Fix commitba1db4b30658 (chromium/src) +355/-55
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
StyleScopeData
third_party/blink/renderer/core/dom/element.h
modified
TextVisitor
third_party/blink/renderer/core/dom/element.h
modified
TrustedHTMLParserOptions
third_party/blink/renderer/core/dom/element.h
modified
UIEvent
third_party/blink/renderer/core/dom/element.h
modified
V8UnionBooleanOrScrollIntoViewOptions
third_party/blink/renderer/core/dom/element.h
modified
V8UnionCSSPseudoElementOrDocumentOrElementOrText
third_party/blink/renderer/core/dom/element.h
modified
V8UnionKeyframeAnimationOptionsOrUnrestrictedDouble
third_party/blink/renderer/core/dom/element.h
modified
ListAttributeTargetObserver
third_party/blink/renderer/core/html/forms/html_input_element.h
modified
RadioButtonGroupScope
third_party/blink/renderer/core/html/forms/html_input_element.h
modified
ScriptObject
third_party/blink/renderer/core/html/forms/html_input_element.h
modified
UIEvent
third_party/blink/renderer/core/html/forms/html_input_element.h
modified
CORE_EXPORT
third_party/blink/renderer/core/html/forms/html_input_element.h
modified
V8UnionHTMLOptGroupElementOrHTMLOptionElement
third_party/blink/renderer/core/html/forms/html_select_element.h
modified
HTMLSelectedContentElement
third_party/blink/renderer/core/html/forms/html_select_element.h
modified
SelectMutationObserver
third_party/blink/renderer/core/html/forms/html_select_element.h
modified
UIEvent
third_party/blink/renderer/core/html/forms/html_select_element.h
modified
UIEvent
third_party/blink/renderer/core/html/html_details_element.h
modified
HTMLDetailsElement
third_party/blink/renderer/core/html/html_details_element.h
modified
Document
third_party/blink/renderer/core/html/html_dialog_element.h
modified
ExceptionState
third_party/blink/renderer/core/html/html_dialog_element.h
modified
PointerEvent
third_party/blink/renderer/core/html/html_dialog_element.h
modified
UIEvent
third_party/blink/renderer/core/html/html_dialog_element.h
modified
KeyboardEvent
third_party/blink/renderer/core/html/html_element.h
modified
PointerEvent
third_party/blink/renderer/core/html/html_element.h
modified
TextControlElement
third_party/blink/renderer/core/html/html_element.h
modified
UIEvent
third_party/blink/renderer/core/html/html_element.h
modified
V8UnionStringLegacyNullToEmptyStringOrTrustedScript
third_party/blink/renderer/core/html/html_element.h
modified
V8UnionBooleanOrTogglePopoverOptions
third_party/blink/renderer/core/html/html_element.h
modified
ShowPopoverOptions
third_party/blink/renderer/core/html/html_element.h
modified
if
third_party/blink/renderer/core/html/html_menu_item_element.cc
modified

Files Changed

  • third_party/blink/renderer/core/html/media/html_media_element.h
  • third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
  • third_party/blink/renderer/core/html/html_menu_item_element.h
  • third_party/blink/renderer/core/dom/element.h
  • third_party/blink/renderer/core/html/html_element.h
  • third_party/blink/renderer/core/html/html_details_element.h
  • third_party/blink/renderer/core/html/forms/html_form_control_element.cc
  • third_party/blink/renderer/core/html/html_menu_list_element.cc
  • third_party/blink/renderer/core/html/forms/html_select_element.cc
  • third_party/blink/renderer/core/html/media/html_media_element.cc
  • third_party/blink/renderer/core/html/html_element.cc
  • third_party/blink/renderer/core/html/forms/html_select_element.h
  • third_party/blink/renderer/core/html/forms/html_input_element.cc
  • third_party/blink/renderer/core/html/html_details_element.cc
  • third_party/blink/renderer/core/html/html_menu_list_element.h
  • third_party/blink/renderer/core/html/html_dialog_element.cc
  • third_party/blink/renderer/core/html/forms/html_input_element.h
  • third_party/blink/renderer/core/html/html_menu_item_element.cc
  • third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
  • third_party/blink/renderer/core/html/forms/html_button_element.cc
  • third_party/blink/renderer/core/html/html_dialog_element.h
From ba1db4b3065871bdabd2822b6980df4ea122cac6 Mon Sep 17 00:00:00 2001
From: David Baron <[email protected]>
Date: Mon, 21 Sep 2026 11:34:33 -0700
Subject: [PATCH] Don't use FocusTrigger::kUserGesture when handling untrusted key events.

This makes menu elements stop using EventHandler::IsHandlingKeyEvent()
and instead make decisions based on the event we're actually handling.

Fixed: 553335083
Change-Id: Ie574a5595c9d476387ef3bcfa77a7244b00c83c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427363
Reviewed-by: Joey Arhar <[email protected]>
Commit-Queue: David Baron <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1701919}
---
diff --git a/third_party/blink/renderer/core/dom/element.h b/third_party/blink/renderer/core/dom/element.h
index eaa37b0..5c5e8e77 100644
--- a/third_party/blink/renderer/core/dom/element.h
+++ b/third_party/blink/renderer/core/dom/element.h
@@ -174,6 +174,7 @@
 class StyleScopeData;
 class TextVisitor;
 class TrustedHTMLParserOptions;
+class UIEvent;
 class V8UnionBooleanOrScrollIntoViewOptions;
 class V8UnionCSSPseudoElementOrDocumentOrElementOrText;
 class V8UnionKeyframeAnimationOptionsOrUnrestrictedDouble;
@@ -1400,7 +1401,8 @@
     return false;
   }
   virtual bool HandleCommandInternal(HTMLElement& invoker,
-                                     CommandEventType command) {
+                                     CommandEventType command,
+                                     UIEvent* activate_event) {
     CHECK(command != CommandEventType::kCustom &&
           command != CommandEventType::kNone);
 
diff --git a/third_party/blink/renderer/core/html/forms/html_button_element.cc b/third_party/blink/renderer/core/html/forms/html_button_element.cc
index 243c838..4511cc9 100644
--- a/third_party/blink/renderer/core/html/forms/html_button_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_button_element.cc
@@ -35,6 +35,7 @@
 #include "third_party/blink/renderer/core/dom/focus_params.h"
 #include "third_party/blink/renderer/core/dom/qualified_name.h"
 #include "third_party/blink/renderer/core/events/command_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/web_feature.h"
 #include "third_party/blink/renderer/core/html/forms/form_data.h"
@@ -261,7 +262,7 @@
       }
     }
 
-    if (HandleCommandForActivation()) {
+    if (HandleCommandForActivation(To<UIEvent>(&event))) {
       return;
     }
   }
diff --git a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
index 45302f6..46bdaa6 100644
--- a/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_form_control_element.cc
@@ -38,6 +38,7 @@
 #include "third_party/blink/renderer/core/event_type_names.h"
 #include "third_party/blink/renderer/core/events/command_event.h"
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/web_feature.h"
 #include "third_party/blink/renderer/core/html/forms/html_button_element.h"
@@ -484,7 +485,8 @@
       }
 
       CHECK(popover.popover->IsValidBuiltinCommand(*this, action));
-      popover.popover->HandleCommandInternal(*this, action);
+      popover.popover->HandleCommandInternal(*this, action,
+                                             To<UIEvent>(&event));
     }
   }
 }
diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.cc b/third_party/blink/renderer/core/html/forms/html_input_element.cc
index 76d2a460..fc27aad 100644
--- a/third_party/blink/renderer/core/html/forms/html_input_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_input_element.cc
@@ -2629,10 +2629,11 @@
 }
 
 bool HTMLInputElement::HandleCommandInternal(HTMLElement& invoker,
-                                             CommandEventType command) {
+                                             CommandEventType command,
+                                             UIEvent* activate_event) {
   CHECK(IsValidBuiltinCommand(invoker, command));
 
-  if (HTMLElement::HandleCommandInternal(invoker, command)) {
+  if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
     return true;
   }
 
diff --git a/third_party/blink/renderer/core/html/forms/html_input_element.h b/third_party/blink/renderer/core/html/forms/html_input_element.h
index 408e755c..10cb096 100644
--- a/third_party/blink/renderer/core/html/forms/html_input_element.h
+++ b/third_party/blink/renderer/core/html/forms/html_input_element.h
@@ -53,6 +53,7 @@
 class ListAttributeTargetObserver;
 class RadioButtonGroupScope;
 class ScriptObject;
+class UIEvent;
 struct DateTimeChooserParameters;
 
 class CORE_EXPORT HTMLInputElement
@@ -426,7 +427,8 @@
   bool IsValidBuiltinCommand(HTMLElement& invoker,
                              CommandEventType command) override;
   bool HandleCommandInternal(HTMLElement& invoker,
-                             CommandEventType command) override;
+                             CommandEventType command,
+                             UIEvent* activate_event) override;
 
   void SetFocused(bool is_focused,
                   mojom::blink::FocusType,
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.cc b/third_party/blink/renderer/core/html/forms/html_select_element.cc
index a8c8c231..ec67bfe 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_select_element.cc
@@ -1860,10 +1860,11 @@
 }
 
 bool HTMLSelectElement::HandleCommandInternal(HTMLElement& invoker,
-                                              CommandEventType command) {
+                                              CommandEventType command,
+                                              UIEvent* activate_event) {
   CHECK(IsValidBuiltinCommand(invoker, command));
 
-  if (HTMLElement::HandleCommandInternal(invoker, command)) {
+  if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
     return true;
   }
 
diff --git a/third_party/blink/renderer/core/html/forms/html_select_element.h b/third_party/blink/renderer/core/html/forms/html_select_element.h
index 7826c9ea..86b48598 100644
--- a/third_party/blink/renderer/core/html/forms/html_select_element.h
+++ b/third_party/blink/renderer/core/html/forms/html_select_element.h
@@ -54,6 +54,7 @@
 class V8UnionHTMLOptGroupElementOrHTMLOptionElement;
 class HTMLSelectedContentElement;
 class SelectMutationObserver;
+class UIEvent;
 
 enum class SelectPopupHideBehavior {
   kNormal,
@@ -295,7 +296,8 @@
   bool IsValidBuiltinCommand(HTMLElement& invoker,
                              CommandEventType command) override;
   bool HandleCommandInternal(HTMLElement& invoker,
-                             CommandEventType command) override;
+                             CommandEventType command,
+                             UIEvent* activate_event) override;
 
   // SlottedButton returns the first child <button> in the light dom tree. If
   // this select is in a state where the <button> can't be rendered, such as a
diff --git a/third_party/blink/renderer/core/html/html_details_element.cc b/third_party/blink/renderer/core/html/html_details_element.cc
index 2ad521f9..edc0262 100644
--- a/third_party/blink/renderer/core/html/html_details_element.cc
+++ b/third_party/blink/renderer/core/html/html_details_element.cc
@@ -305,11 +305,12 @@
 }
 
 bool HTMLDetailsElement::HandleCommandInternal(HTMLElement& invoker,
-                                               CommandEventType command) {
+                                               CommandEventType command,
+                                               UIEvent* activate_event) {
   if (!IsValidBuiltinCommand(invoker, command)) {
     return false;
   }
-  if (HTMLElement::HandleCommandInternal(invoker, command)) {
+  if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
     return true;
   }
 
diff --git a/third_party/blink/renderer/core/html/html_details_element.h b/third_party/blink/renderer/core/html/html_details_element.h
index fee7abb1..51c389b2 100644
--- a/third_party/blink/renderer/core/html/html_details_element.h
+++ b/third_party/blink/renderer/core/html/html_details_element.h
@@ -27,6 +27,8 @@
 
 namespace blink {
 
+class UIEvent;
+
 class HTMLDetailsElement final : public HTMLElement {
   DEFINE_WRAPPERTYPEINFO();
 
@@ -49,7 +51,8 @@
   bool IsValidBuiltinCommand(HTMLElement& invoker,
                              CommandEventType command) override;
   bool HandleCommandInternal(HTMLElement& invoker,
-                             CommandEventType command) override;
+                             CommandEventType command,
+                             UIEvent* activate_event) override;
 
   // The name attribute for grouping of related details; empty string
   // means no grouping.
diff --git a/third_party/blink/renderer/core/html/html_dialog_element.cc b/third_party/blink/renderer/core/html/html_dialog_element.cc
index 1f0d86e..5718379 100644
--- a/third_party/blink/renderer/core/html/html_dialog_element.cc
+++ b/third_party/blink/renderer/core/html/html_dialog_element.cc
@@ -383,11 +383,12 @@
 }
 
 bool HTMLDialogElement::HandleCommandInternal(HTMLElement& invoker,
-                                              CommandEventType command) {
+                                              CommandEventType command,
+                                              UIEvent* activate_event) {
   if (!IsValidBuiltinCommand(invoker, command)) {
     return false;
   }
-  if (HTMLElement::HandleCommandInternal(invoker, command)) {
+  if (HTMLElement::HandleCommandInternal(invoker, command, activate_event)) {
     return true;
   }
 
diff --git a/third_party/blink/renderer/core/html/html_dialog_element.h b/third_party/blink/renderer/core/html/html_dialog_element.h
index 7ad67e4..38d56bf 100644
--- a/third_party/blink/renderer/core/html/html_dialog_element.h
+++ b/third_party/blink/renderer/core/html/html_dialog_element.h
@@ -39,6 +39,7 @@
 class Document;
 class ExceptionState;
 class PointerEvent;
+class UIEvent;
 
 enum class ClosedByState {
   kAny,
@@ -130,7 +131,8 @@
   bool IsValidBuiltinCommand(HTMLElement& invoker,
                              CommandEventType command) override;
   bool HandleCommandInternal(HTMLElement& invoker,
-                             CommandEventType command) override;
+                             CommandEventType command,
+                             UIEvent* activate_event) override;
 
   void AttributeChanged(const AttributeModificationParams&) override;
   void ParseAttribute(const AttributeModificationParams&) override;
diff --git a/third_party/blink/renderer/core/html/html_element.cc b/third_party/blink/renderer/core/html/html_element.cc
index 86599e7..4e92bed 100644
--- a/third_party/blink/renderer/core/html/html_element.cc
+++ b/third_party/blink/renderer/core/html/html_element.cc
@@ -89,6 +89,7 @@
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
 #include "third_party/blink/renderer/core/events/pointer_event.h"
 #include "third_party/blink/renderer/core/events/toggle_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
 #include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
@@ -3254,11 +3255,12 @@
 }
 
 bool HTMLElement::HandleCommandInternal(HTMLElement& invoker,
-                                        CommandEventType command) {
+                                        CommandEventType command,
+                                        UIEvent* activate_event) {
   if (!IsValidBuiltinCommand(invoker, command)) {
     return false;
   }
-  if (Element::HandleCommandInternal(invoker, command)) {
+  if (Element::HandleCommandInternal(invoker, command, activate_event)) {
     return true;
   }
 
@@ -3389,7 +3391,7 @@
   return false;
 }
 
-bool HTMLElement::HandleCommandForActivation() {
+bool HTMLElement::HandleCommandForActivation(UIEvent* activate_event) {
   if (!CanBeCommandInvoker()) {
     return false;
   }
@@ -3429,7 +3431,8 @@
   command_target->DispatchEvent(*command_event);
   if (!command_event->defaultPrevented() &&
       command_event_type != CommandEventType::kCustom) {
-    command_target->HandleCommandInternal(*this, command_event_type);
+    command_target->HandleCommandInternal(*this, command_event_type,
+                                          activate_event);
   }
   return true;
 }
@@ -4205,7 +4208,7 @@
       return;
     }
 
-    if (HandleCommandForActivation()) {
+    if (HandleCommandForActivation(To<UIEvent>(&event))) {
       return;
     }
   }
diff --git a/third_party/blink/renderer/core/html/html_element.h b/third_party/blink/renderer/core/html/html_element.h
index cd12035..0a1a7ee 100644
--- a/third_party/blink/renderer/core/html/html_element.h
+++ b/third_party/blink/renderer/core/html/html_element.h
@@ -49,6 +49,7 @@
 class KeyboardEvent;
 class PointerEvent;
 class TextControlElement;
+class UIEvent;
 class V8UnionStringLegacyNullToEmptyStringOrTrustedScript;
 class V8UnionBooleanOrTogglePopoverOptions;
 class ShowPopoverOptions;
@@ -392,7 +393,8 @@
                              CommandEventType command) override;
   bool IsValidBuiltinPopoverCommand(CommandEventType command);
   bool HandleCommandInternal(HTMLElement& invoker,
-                             CommandEventType command) override;
+                             CommandEventType command,
+                             UIEvent* activate_event) override;
   // This is true if this element *can* be a command invoker: it is an element
   // type that supports command invokers (e.g. buttons and menuitems), and the
   // element isn't in a state that disqualifies it (e.g. a disabled state). This
@@ -401,7 +403,7 @@
   virtual bool CanBeCommandInvoker() const;
   static CommandEventType GetCommandEventType(const AtomicString& type,
                                               ExecutionContext*);
-  virtual bool HandleCommandForActivation();
+  virtual bool HandleCommandForActivation(UIEvent* activate_event);
   Element* commandForElement() const;
   AtomicString command() const;
   void setCommand(const AtomicString& type);
diff --git a/third_party/blink/renderer/core/html/html_menu_item_element.cc b/third_party/blink/renderer/core/html/html_menu_item_element.cc
index ed65bcc8..625cde4 100644
--- a/third_party/blink/renderer/core/html/html_menu_item_element.cc
+++ b/third_party/blink/renderer/core/html/html_menu_item_element.cc
@@ -15,6 +15,7 @@
 #include "third_party/blink/renderer/core/events/command_event.h"
 #include "third_party/blink/renderer/core/events/keyboard_event.h"
 #include "third_party/blink/renderer/core/events/mouse_event.h"
+#include "third_party/blink/renderer/core/events/ui_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/web_feature.h"
@@ -26,7 +27,6 @@
 #include "third_party/blink/renderer/core/html/html_sub_menu_element.h"
 #include "third_party/blink/renderer/core/html_names.h"
 #include "third_party/blink/renderer/core/input/event_handler.h"
-#include "third_party/blink/renderer/core/input/keyboard_event_manager.h"
 #include "third_party/blink/renderer/core/keywords.h"
 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
@@ -231,7 +231,8 @@
   }
 }
 
-void HTMLMenuItemElement::ActivateMenuItem() {
+void HTMLMenuItemElement::ActivateMenuItem(
+    ActivationKeyboardEventType activation_type) {
   // A menu item's checkability and ability to invoke a command are
   // exclusive. If the item is checkable, that takes precedence, and the sub-
   // menu invoker will NOT be respected.
@@ -248,13 +249,13 @@
 
     submenu->InvokePopover(*this, PopoverInvokedVia::kCommand);
 
-    bool handling_keyboard_event = false;
-    if (LocalFrame* frame = GetDocument().GetFrame()) {
-      handling_keyboard_event = frame->GetEventHandler().IsHandlingKeyEvent();
-    }
     if (submenu->popoverOpen()) {
-      if (handling_keyboard_event) {
-        submenu->FocusFirstItem();
+      if (activation_type != ActivationKeyboardEventType::kNotKeyboard) {
+        FocusTrigger trigger =
+            activation_type == ActivationKeyboardEventType::kTrustedKeyboard
+                ? FocusTrigger::kUserGesture
+                : FocusTrigger::kScript;
+        submenu->FocusFirstItem(trigger);
       } else {
         // If this is resulting from a click, this is happening *before* that
         // click has had a chance to focus this menuitem.  However, we should
@@ -311,7 +312,10 @@
     return;
   }
 
-  FocusParams focus_params(FocusTrigger::kUserGesture);
+  CHECK(event.IsFullyTrusted())
+      << "FocusTrigger::kUserGesture depends on trusted events";
+  FocusTrigger key_focus_trigger = FocusTrigger::kUserGesture;
+  FocusParams focus_params(key_focus_trigger);
   const AtomicString key(keyboard_event->key());
 
   // Nothing else below does anything if we're not inside an owner menu that has
@@ -530,12 +534,12 @@
       if (auto* invoked_menulist = GetInvokedSubmenu()) {
         invoked_menulist->InvokePopover(*this, PopoverInvokedVia::kCommand);
         if (key == keywords::kArrowDown) {
-          if (invoked_menulist->FocusFirstItem()) {
+          if (invoked_menulist->FocusFirstItem(key_focus_trigger)) {
             event.SetDefaultHandled();
             return;
           }
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
new file mode 100644
index 0000000..3ff214a1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/focus-without-user-activation.html
@@ -0,0 +1,178 @@
+<!doctype html>
+<title>focusing first item in menu respects focus-without-user-activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<body>
+  <input type="text" id="top-input" />
+  <script>
+    const tabKey = "\uE004";
+    const enterKey = "\uE007";
+    const shiftKey = "\uE008";
+
+    const waitForMessage = (expectedAction) => {
+      return new Promise((resolve) => {
+        const handler = (e) => {
+          if (e.data && e.data.action === expectedAction) {
+            window.removeEventListener("message", handler);
+            resolve(e.data);
+          }
+        };
+        window.addEventListener("message", handler);
+      });
+    };
+
+    function createTestIframe(t, target) {
+      const iframe = document.createElement("iframe");
+      iframe.allow = "focus-without-user-activation 'none'";
+      iframe.src = `/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html?target=${target}`;
+      document.body.appendChild(iframe);
+      t.add_cleanup(() => iframe.remove());
+      return iframe;
+    }
+
+    // Test 1: Script click during key event on button commandfor menulist
+    promise_test(async (t) => {
+      const topInput = document.querySelector("#top-input");
+      const iframe = createTestIframe(t, "button");
+      await waitForMessage("ready");
+
+      const trigger = iframe.contentDocument.getElementById("trigger");
+      trigger.focus();
+      assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+      const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+      // Press Shift key on the trigger in iframe.
+      // The Shift key handler will park focus on topInput and call trigger.click().
+      await new test_driver.Actions()
+        .keyDown(shiftKey)
+        .keyUp(shiftKey)
+        .send();
+
+      const msg = await shiftCompletePromise;
+
+      assert_true(msg.isPopoverOpen, "Popover should have opened");
+      assert_equals(
+        document.activeElement,
+        topInput,
+        "Top-level document should not lose focus on script click during key event",
+      );
+      assert_not_equals(
+        msg.activeElementId,
+        "first-item",
+        "First item should not have gained focus without user activation",
+      );
+    }, "Script click during key event on button commandfor menulist does not bypass focus-without-user-activation");
+
+    // Test 2: Script click during key event on menuitem activating submenu
+    promise_test(async (t) => {
+      const topInput = document.querySelector("#top-input");
+      const iframe = createTestIframe(t, "menuitem");
+      await waitForMessage("ready");
+
+      const trigger = iframe.contentDocument.getElementById("trigger");
+      trigger.focus();
+      assert_equals(iframe.contentDocument.activeElement, trigger, "Trigger should have initial focus");
+
+      const shiftCompletePromise = waitForMessage("shift-click-complete");
+
+      // Press Shift key on the trigger in iframe.
+      // The Shift key handler will park focus on topInput and call trigger.click().
+      await new test_driver.Actions()
+        .keyDown(shiftKey)
+        .keyUp(shiftKey)
+        .send();
+
+      const msg = await shiftCompletePromise;
+
+      assert_true(msg.isPopoverOpen, "Submenu should have opened");
+      assert_equals(
+        document.activeElement,
+        topInput,
+        "Top-level document should not lose focus on script click during key event",
+      );
+      assert_not_equals(
+        msg.activeElementId,
+        "first-item",
+        "Submenu first item should not have gained focus without user activation",
+      );
+    }, "Script click during key event on menuitem activating submenu does not bypass focus-without-user-activation");
+
+    // Test 3: Trusted key event on button commandfor menulist
+    promise_test(async (t) => {
+      const topInput = document.querySelector("#top-input");
+      const iframe = createTestIframe(t, "button");
+      await waitForMessage("ready");
+
+      topInput.focus();
+      assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+      const itemFocusedPromise = waitForMessage("item-focused");
+
+      // Press Tab to navigate focus into the iframe's trigger button
+      await new test_driver.Actions()
+        .keyDown(tabKey)
+        .keyUp(tabKey)
+        .send();
+
+      // Press Enter on the focused trigger button
+      await new test_driver.Actions()
+        .keyDown(enterKey)
+        .keyUp(enterKey)
+        .send();
+
+      const msg = await itemFocusedPromise;
+      assert_true(msg.isPopoverOpen, "Popover should have opened");
+      assert_equals(
+        msg.activeElementId,
+        "first-item",
+        "First item in menulist should receive focus from trusted key event",
+      );
+      assert_equals(
+        document.activeElement,
+        iframe,
+        "Iframe should have focus after trusted key event",
+      );
+    }, "Trusted key event on button commandfor menulist focuses first item");
+
+    // Test 4: Trusted key event on menuitem activating submenu
+    promise_test(async (t) => {
+      const topInput = document.querySelector("#top-input");
+      const iframe = createTestIframe(t, "menuitem");
+      await waitForMessage("ready");
+
+      topInput.focus();
+      assert_equals(document.activeElement, topInput, "Top input should have focus");
+
+      const itemFocusedPromise = waitForMessage("item-focused");
+
+      // Press Tab to navigate focus into the iframe's trigger menuitem
+      await new test_driver.Actions()
+        .keyDown(tabKey)
+        .keyUp(tabKey)
+        .send();
+
+      // Press Enter on the focused trigger menuitem
+      await new test_driver.Actions()
+        .keyDown(enterKey)
+        .keyUp(enterKey)
+        .send();
+
+      const msg = await itemFocusedPromise;
+      assert_true(msg.isPopoverOpen, "Submenu should have opened");
+      assert_equals(
+        msg.activeElementId,
+        "first-item",
+        "First item in submenu should receive focus from trusted key event",
+      );
+      assert_equals(
+        document.activeElement,
+        iframe,
+        "Iframe should have focus after trusted key event",
+      );
+    }, "Trusted key event on menuitem activating submenu focuses first item");
+  </script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
new file mode 100644
index 0000000..9e182362
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/menu/tentative/resources/focus-without-user-activation-helper.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<style>
+  body { margin: 0; padding: 0; }
+</style>
+<body>
+<script>
+const params = new URLSearchParams(location.search);
+const target = params.get("target") || "button";
+
+let trigger;
+let menulist;
+let firstItem;
+
+if (target === "button") {
+  document.body.innerHTML = `
+    <button id="trigger" commandfor="menulist" command="show-popover">Trigger</button>
+    <menulist id="menulist">
+      <menuitem id="first-item">First Item</menuitem>
+      <menuitem id="second-item">Second Item</menuitem>
+    </menulist>
+  `;
+} else {
+  document.body.innerHTML = `
+    <menubar>
+      <submenu>
+        <menuitem id="trigger">Trigger</menuitem>
+        <menulist id="menulist">
+          <menuitem id="first-item">First Item</menuitem>
+          <menuitem id="second-item">Second Item</menuitem>
+        </menulist>
+      </submenu>
+    </menubar>
+  `;
+}
+
+trigger = document.getElementById("trigger");
+menulist = document.getElementById("menulist");
+firstItem = document.getElementById("first-item");
+
+firstItem.addEventListener("focus", () => {
+  window.parent.postMessage({
+    action: "item-focused",
+    activeElementId: document.activeElement ? document.activeElement.id : null,
+    isPopoverOpen: menulist.matches(":popover-open")
+  }, "*");
+});
+
+trigger.addEventListener("keydown", (e) => {
+  if (e.key === "Shift") {
+    const parentInput = window.parent.document.getElementById("top-input");
+    if (parentInput) {
+      parentInput.focus();
+    }
+    trigger.click();
+    window.parent.postMessage({
+      action: "shift-click-complete",
+      activeElementId: document.activeElement ? document.activeElement.id : null,
+      isPopoverOpen: menulist.matches(":popover-open")
+    }, "*");
+  }
+});
+
+window.addEventListener("load", () => {
+  window.parent.postMessage({ action: "ready" }, "*");
+});
+</script>
+</body>
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.