Overview

High
Severity
8.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Sub-componentViews
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker516996291
Fix commit932e957b2cd9 (chromium/src) +715/-113
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-09-02 (public git)
Disclosed2026-09-08
Patch gap6 days — fix public in source before the advisory

Background

`MenuController`
the process-wide, active_instance_-tracked object that drives an open Views menu, dispatching every input event and owning selection state and teardown for the whole menu hierarchy.
`MenuRunnerImpl`
the per-menu object created by RunMenuAt; on Release/Cancel it tears down its menu and, for the top-level runner, the shared MenuController.
`ScopedDeletionGuard` / `stack_depth_` / `destroy_pending_`
the new RAII guard that increments a re-entrancy depth counter (stack_depth_) on entry to a MenuController method and, when destruction is requested while depth is nonzero, records it in destroy_pending_ and defers the real delete until the stack unwinds (guard internals live in the truncated menu_controller.h/.cc) (inferred).
`base::WeakPtr` liveness pattern
the removed idiom auto this_ref = AsWeakPtr(); ... if (!this_ref) return; that tried to detect whether a re-entrant callback had synchronously deleted this.

Root Cause Analysis

Many MenuController methods — showing a menu in OpenMenuImpl, cancelling in Cancel, and event handlers such as OnGestureEvent, OnTouchEvent, OnWidgetDestroying, and the mouse handlers — run code (widget display, accessibility notifications, nested-menu teardown) that can re-enter and synchronously destroy the MenuController and/or the owning MenuRunnerImpl. Several of those methods held no base::WeakPtr guard at all, so after the re-entrant deletion they continued to execute on a freed this, and even the methods that did guard only protected their own frame, never the outer stack frames still executing. The regression test NestedMenuRunnersDestroyedOutOfOrder makes this concrete: cancelling the outer runner destroys the shared controller, and then menu_runner2->Release() touches the already-freed MenuController.

The fix replaces synchronous deletion with deferred deletion: while any MenuController method is on the stack (stack_depth_ > 0), a destruction request only sets destroy_pending_, and the actual delete is performed by the outermost ScopedDeletionGuard destructor when depth returns to zero. Because this now stays valid for the full duration of every guarded method, the scattered if (!this_ref) checks become if (destroy_pending_) bail-outs on a still-live object, and OnWidgetDestroying additionally avoids a re-entrant ExitMenu() by calling it only when exit_type_ == ExitType::kNone.

Key insight
The code depended on incomplete, per-frame base::WeakPtr liveness checks to survive re-entrant self-destruction, but a WeakPtr in one frame cannot protect the outer (or sibling nested-runner) frames still dereferencing the same object; the fix guarantees this outlives the entire active call stack by deferring destruction until stack_depth_ reaches zero and signalling callers through destroy_pending_ instead.

Attack Path

  1. Open a Views menu Cause a menu to be shown so MenuController::OpenMenuImpl runs and displays the menu host widget, entering a code region that had no liveness guard.
  2. Re-enter during display/handling While that method (or an event handler like OnGestureEvent/OnTouchEvent) is on the stack, an accessibility notification, dispatched event, or nested-menu operation re-enters the controller.
  3. Force synchronous teardown Trigger a cancel/release that destroys the shared MenuController mid-stack — e.g. the out-of-order pattern from the regression test where the outer runner is cancelled and a nested MenuRunnerImpl is then released.
  4. Use the freed controller When control returns to the still-executing outer frame (or the sibling runner), it dereferences the now-freed MenuController, producing a use-after-free access. (inferred exact dereference sink — the patch defers deletion rather than pinpointing one sink)
  5. Exploit the freed allocation Groom the reclaimed MenuController memory to convert the UAF into controlled corruption. (inferred)

Impact Assessment

An attacker who can get a menu open and drive re-entrant teardown gains a use-after-free of the MenuController in the browser/UI process where Views runs, potentially leading to memory corruption and, from there, code execution in that process (inferred). Preconditions are the ability to trigger a menu (e.g. a context/app menu) and to induce the re-entrant cancellation/release, plausibly via crafted page/UI interaction or accessibility events. The advisory characterizes it only as a use-after-free in Views and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
if
ui/views/controls/menu/menu_controller.cc
modified
for
ui/views/controls/menu/menu_controller.cc
modified

Files Changed

  • ui/views/controls/menu/menu_host.h
  • ui/views/controls/menu/submenu_view.cc
  • ui/views/controls/menu/menu_controller.cc
  • ui/views/controls/menu/menu_runner_impl.h
  • ui/views/controls/menu/menu_controller.h
  • ui/views/controls/menu/menu_host.cc
  • ui/views/controls/menu/menu_runner_unittest.cc
  • ui/views/controls/menu/menu_controller_unittest.cc
  • ui/views/controls/menu/menu_runner_impl.cc
  • ui/views/controls/menu/menu_item_view.h
  • ui/views/controls/menu/menu_item_view.cc

Audit Directions

  • Re-entrant self-deletion via callbacks
    Audit other Views/UI controllers that dispatch events, fire accessibility notifications, or show widgets and may be destroyed by those callbacks; look for continued member access after such a call without a lifetime guarantee.
  • Incomplete `WeakPtr` liveness guards
    Flag the auto ref = AsWeakPtr(); if (!ref) return; idiom where only the immediate frame is protected while outer frames on the same stack (or sibling objects) keep using the same this.
  • Out-of-order nested-object teardown
    Review shared singletons owned by multiple runners/handles (like MenuRunnerImpl sharing one MenuController) for cancel/release orderings that free the shared object while another owner still references it.
From 932e957b2cd9e2a172e779ef44e6aa6ab6442bee Mon Sep 17 00:00:00 2001
From: Mitsuru Oshima <[email protected]>
Date: Tue, 01 Sep 2026 23:23:04 -0700
Subject: [PATCH] views: Fix UAF in MenuController and simplify teardown

This CL fixes a Use-After-Free (UAF) in MenuController::OpenMenuImpl
by deferring MenuController and MenuRunnerImpl destruction when the
execution stack is active (tracked via stack_depth_ > 0).

To support this:
- Fix UAF in MenuItemView by clearing parent pointers during
  destruction.
- Fix gesture transfer issue in MenuHost by returning early in
  HideMenuHost if we are destroying and it is already hidden.
- Fix dangling pointers in ClipboardHistory and BirchBar menus by
  clearing delegates/history pointers during menu teardown.

Bug: 516996291
AX-Relnotes: n/a.
TAG=agy
CONV=b4c86621-d1cd-478c-ae64-25a090f21efa

Change-Id: I4d4d4cf14c95bab5745653f1d0d59845dc3b9024
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8246240
Reviewed-by: Allen Bauer <[email protected]>
Reviewed-by: Keren Zhu <[email protected]>
Commit-Queue: Mitsuru Oshima <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1690483}
---
diff --git a/ui/views/controls/menu/menu_controller.cc b/ui/views/controls/menu/menu_controller.cc
index 6a8ca32..c0ac1441 100644
--- a/ui/views/controls/menu/menu_controller.cc
+++ b/ui/views/controls/menu/menu_controller.cc
@@ -11,7 +11,6 @@
 #include "base/callback_list.h"
 #include "base/check.h"
 #include "base/containers/flat_set.h"
-#include "base/debug/dump_without_crashing.h"
 #include "base/functional/bind.h"
 #include "base/i18n/case_conversion.h"
 #include "base/i18n/rtl.h"
@@ -47,6 +46,7 @@
 #include "ui/views/controls/menu/menu_host_root_view.h"
 #include "ui/views/controls/menu/menu_item_view.h"
 #include "ui/views/controls/menu/menu_pre_target_handler.h"
+#include "ui/views/controls/menu/menu_runner_impl.h"
 #include "ui/views/controls/menu/menu_scroll_view_container.h"
 #include "ui/views/controls/menu/menu_types.h"
 #include "ui/views/controls/menu/submenu_view.h"
@@ -685,6 +685,9 @@
                          MenuType menu_type,
                          bool is_nested_drag,
                          gfx::NativeView native_view_for_gestures) {
+  // Track stack depth to defer controller destruction during initial menu
+  // display.
+  ScopedDeletionGuard guard(AsWeakPtr());
   exit_type_ = ExitType::kNone;
   possible_drag_ = false;
   drag_in_progress_ = false;
@@ -816,6 +819,9 @@
 }
 
 void MenuController::Cancel(ExitType type, bool disable_animation) {
+  // Track stack depth to defer controller destruction during menu
+  // cancellation.
+  ScopedDeletionGuard guard(AsWeakPtr());
 #if BUILDFLAG(IS_MAC)
   menu_closure_animation_.reset();
 #endif
@@ -918,12 +924,12 @@
 
 void MenuController::SelectItemAndOpenSubmenu(MenuItemView* item) {
   DCHECK(item);
-  auto this_ref = AsWeakPtr();
+  ScopedDeletionGuard guard(AsWeakPtr());
   SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY);
 
   // Accessibility events fired as a result of the selection changing may have
-  // closed the menu and deleted `this`. Guard against that.
-  if (!this_ref) {
+  // closed the menu and destroyed `this`. Guard against that.
+  if (destroy_pending_) {
     return;
   }
 
@@ -937,6 +943,7 @@
 
 bool MenuController::OnMousePressed(SubmenuView* source,
                                     const ui::MouseEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   // We should either have no current_mouse_event_target_, or should have a
   // pressed state stored.
   DCHECK(!current_mouse_event_target_ || current_mouse_pressed_state_);
@@ -968,12 +975,11 @@
     View* view =
         forward_to_root->GetEventHandlerForPoint(event_for_root.location());
     Button* button = Button::AsButton(view);
-    auto this_ref = AsWeakPtr();
     if (hot_button_ != button) {
       SetHotTrackedButton(button);
     }
 
-    if (!this_ref) {
+    if (destroy_pending_) {
       return true;
     }
 
@@ -982,7 +988,7 @@
       bool processed = forward_to_root->ProcessMousePressed(event_for_root);
       // This object may be destroyed as a result of a mouse press event (some
       // item may close the menu).
-      if (!this_ref) {
+      if (destroy_pending_) {
         return true;
       }
 
@@ -1006,6 +1012,7 @@
 
 bool MenuController::OnMouseDragged(SubmenuView* source,
                                     const ui::MouseEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   if (current_mouse_event_target_) {
     return current_mouse_event_target_->ProcessMouseDragged(
         ConvertLocatedEventForRootView(*source, *current_mouse_event_target_,
@@ -1025,9 +1032,6 @@
     }
     return true;
   }
-  // Changing the selection or showing a sibling menu can cause `this` to be
-  // deleted as a side effect of accessibility notifications.
-  auto this_ref = AsWeakPtr();
   MenuItemView* mouse_menu = nullptr;
   if (part.type == MenuPartType::kMenuItem) {
     // If there is no menu target, but a submenu target, then we are interacting
@@ -1055,7 +1059,9 @@
       }
     }
   }
-  if (!this_ref) {
+  // Changing the selection or showing a sibling menu can cause `this` to be
+  // destroyed as a side effect of accessibility notifications.
+  if (destroy_pending_) {
     return false;
   }
   UpdateActiveMouseView(source, event, mouse_menu);
@@ -1065,10 +1071,9 @@
 
 void MenuController::OnMouseReleased(SubmenuView* source,
                                      const ui::MouseEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   current_mouse_pressed_state_ &= ~event.changed_button_flags();
 
-  auto this_ref = AsWeakPtr();
-
   if (current_mouse_event_target_) {
     // If this was the final mouse button, then remove the forwarding target.
     // We need to do this *before* dispatching the event to the root view
@@ -1147,7 +1152,7 @@
     // On the rare, off chance that an accessibility event is fired as a result
     // of the selection changing *and* the Accessibility tool causes the menu to
     // be closed, `this` will be otherwise dangling. Guard against that.
-    if (!this_ref) {
+    if (destroy_pending_) {
       return;
     }
   }
@@ -1157,6 +1162,7 @@
 
 void MenuController::OnMouseMoved(SubmenuView* source,
                                   const ui::MouseEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   if (current_mouse_event_target_) {
     current_mouse_event_target_->ProcessMouseMoved(
         ConvertLocatedEventForRootView(*source, *current_mouse_event_target_,
@@ -1190,9 +1196,8 @@
 
   // `HandleMouseLocation()` may change the selection, which can cause `this` to
   // be deleted as a side effect of accessibility notifications.
-  auto this_ref = AsWeakPtr();
   HandleMouseLocation(source, event.location());
-  if (!this_ref) {
+  if (destroy_pending_) {
     return;
   }
 
@@ -1213,6 +1218,7 @@
 
 bool MenuController::OnMouseWheel(SubmenuView* source,
                                   const ui::MouseWheelEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   // Stop scrolling via scroll button to prevent flickering.
   StopScrollingViaButton();
 
@@ -1220,12 +1226,18 @@
 
   SetSelection(part.menu ? part.menu.get() : state_.item.get(),
                SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY);
+  if (destroy_pending_) {
+    return false;
+  }
 
   return part.submenu && part.submenu->OnMouseWheel(event);
 }
 
 void MenuController::OnGestureEvent(SubmenuView* source,
                                     ui::GestureEvent* event) {
+  // Track stack depth to defer controller destruction during gesture
+  // processing and potential command execution.
+  ScopedDeletionGuard guard(AsWeakPtr());
   if (owner_ && send_gesture_events_to_owner()) {
 #if defined(USE_AURA)
     gfx::NativeView target = native_view_for_gestures_
@@ -1308,6 +1320,9 @@
 }
 
 void MenuController::OnTouchEvent(SubmenuView* source, ui::TouchEvent* event) {
+  // Track stack depth to defer controller destruction if RepostEventAndCancel
+  // closes the menu.
+  ScopedDeletionGuard guard(AsWeakPtr());
   // Bail if owner wants the current active gesture sequence.
   if (owner_ && send_gesture_events_to_owner()) {
     return;
@@ -1384,6 +1399,7 @@
 
 int MenuController::OnDragUpdated(SubmenuView* source,
                                   const ui::DropTargetEvent& event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   StopCancelAllTimer();
 
   const gfx::Point screen_loc =
@@ -1409,7 +1425,6 @@
   }
   MenuDelegate::DropPosition drop_position = MenuDelegate::DropPosition::kNone;
   int drop_operation = ui::DragDropTypes::DRAG_NONE;
-  auto this_ref = AsWeakPtr();
   if (menu_item) {
     gfx::Point menu_item_loc(event.location());
     View::ConvertPointToTarget(source, menu_item, &menu_item_loc);
@@ -1447,7 +1462,7 @@
   }
   // Accessibility events fired as a result of the selection changing may have
   // closed the menu and deleted `this`. Guard against that.
-  if (!this_ref) {
+  if (destroy_pending_) {
     return drop_operation;
   }
   SetDropMenuItem(menu_item, drop_position);
@@ -1526,6 +1541,7 @@
 }
 
 void MenuController::OnDragDropCompleted(bool should_close) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   DCHECK(drag_in_progress_);
   drag_in_progress_ = false;
   // During a drag, mouse events are processed directly by the widget, and not
@@ -1553,11 +1569,10 @@
     // canceled and deleted. Verify that this is still active before closing
     // the widgets.
     if (active_instance_ == this) {
-      base::WeakPtr<MenuController> this_ref = AsWeakPtr();
       CloseAllNestedMenus();
       Cancel(ExitType::kAll);
       // The above may have deleted us. If not perform a full shutdown.
-      if (!this_ref) {
+      if (destroy_pending_) {
         return;
       }
       ExitMenu();
@@ -1571,6 +1586,7 @@
 
 ui::PostDispatchAction MenuController::OnWillDispatchKeyEvent(
     ui::KeyEvent* event) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   if (exit_type() == ExitType::kAll || exit_type() == ExitType::kDestroyed) {
     // If the event has arrived after the menu's exit type has changed but
     // before its Widgets have been destroyed, the event will continue its
@@ -1582,7 +1598,6 @@
     return ui::POST_DISPATCH_PERFORM_DEFAULT;
   }
 
-  base::WeakPtr<MenuController> this_ref = AsWeakPtr();
   if (event->type() == ui::EventType::kKeyPressed) {
     bool key_handled = false;
 #if BUILDFLAG(IS_MAC)
@@ -1608,7 +1623,7 @@
     }
 
     // Key events can lead to this being deleted.
-    if (!this_ref) {
+    if (destroy_pending_) {
       event->StopPropagation();
       return ui::POST_DISPATCH_NONE;
     }
@@ -1623,7 +1638,7 @@
         char16_t c = event->GetCharacter();
         SelectByChar(c);
         // SelectByChar can lead to this being deleted.
-        if (!this_ref) {
+        if (destroy_pending_) {
           event->StopPropagation();
           return ui::POST_DISPATCH_NONE;
         }
@@ -1644,7 +1659,7 @@
       ViewsDelegate::GetInstance()->ProcessAcceleratorWhileMenuShowing(
           accelerator);
   // Above can lead to |this| being deleted.
-  if (!this_ref) {
+  if (destroy_pending_) {
     event->StopPropagation();
     return ui::POST_DISPATCH_NONE;
   }
@@ -1686,6 +1701,9 @@
 }
 
 void MenuController::OnWidgetDestroying(Widget* widget) {
+  // Track stack depth to defer controller destruction if ExitMenu() leads
+  // to releasing the runner or destroying the controller.
+  ScopedDeletionGuard guard(AsWeakPtr());
   DCHECK_EQ(owner_, widget);
   ClearOwner();
   native_view_for_gestures_ = gfx::NativeView();
@@ -1695,8 +1713,11 @@
 #endif
 
   // Exit menu to ensure that we are not holding on to resources when the
-  // widget has been destroyed.
-  ExitMenu();
+  // widget has been destroyed. Only trigger if not already exiting to avoid
+  // re-entrant exit calls during teardown.
+  if (exit_type_ == ExitType::kNone) {
+    ExitMenu();
+  }
 }
 
 bool MenuController::IsCancelAllTimerRunningForTest() {
@@ -1721,6 +1742,7 @@
     }
   }
 }
+
 // static
 void MenuController::TurnOffMenuSelectionHoldForTest() {
   menu_selection_hold_time = base::TimeDelta();
@@ -1743,33 +1765,18 @@
 #endif
   UnregisterAlertedItem(menu_item);
 
-  bool found_in_pending_state = false;
-  bool found_in_current_state = false;
-  int menu_stack_matches = 0;
-
   if (pending_state_.item == menu_item) {
     pending_state_.item = nullptr;
-    found_in_pending_state = true;
   }
   if (state_.item == menu_item) {
     state_.item = nullptr;
-    found_in_current_state = true;
   }
 
   for (auto& menu_state_pair : menu_stack_) {
     if (menu_state_pair.first.item == menu_item) {
       menu_state_pair.first.item = nullptr;
-      menu_stack_matches++;
     }
   }
-
-  if (found_in_pending_state || found_in_current_state ||
-      menu_stack_matches > 0) {
-    // This indicates a lifecycle management issue - MenuItemView destroyed
-    // while still referenced by MenuController.
-    // Remove this DumpWithoutCrashing once we get enough information.
-    base::debug::DumpWithoutCrashing();
-  }
 }
 
 void MenuController::AnimationProgressed(const gfx::Animation* animation) {
@@ -1786,6 +1793,7 @@
 
 void MenuController::SetSelection(MenuItemView* menu_item,
                                   int selection_types) {
+  ScopedDeletionGuard guard(AsWeakPtr());
   size_t paths_differ_at = 0;
   std::vector<MenuItemView*> current_path;
   std::vector<MenuItemView*> new_path;
@@ -1803,15 +1811,13 @@
       pending_state_.submenu_open !=
           !!(selection_types & SELECTION_OPEN_SUBMENU);
 
-  auto this_ref = AsWeakPtr();
-
   if (pending_item_changed && pending_state_.item) {
     SetHotTrackedButton(nullptr);
   }
 
   // SetHotTrackedButton does some accessibility stuff that could conceivably
-  // cause `this` to be deleted, so protect against that.
-  if (!this_ref) {
+  // cause `this` to be destroyed, so protect against that.
+  if (destroy_pending_) {
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/views/controls/menu/menu_controller_unittest.cc b/ui/views/controls/menu/menu_controller_unittest.cc
index 8c13124..b0db67a 100644
--- a/ui/views/controls/menu/menu_controller_unittest.cc
+++ b/ui/views/controls/menu/menu_controller_unittest.cc
@@ -1063,13 +1063,8 @@
     return;
   }
 
-  if (!owner_->IsClosed()) {
-    owner_->RemoveObserver(menu_controller_);
-  }
-
-  menu_controller_->showing_ = false;
-  menu_controller_->owner_ = nullptr;
-  delete menu_controller_.ExtractAsDangling();
+  menu_controller_->ClearOwner();
+  menu_controller_.ExtractAsDangling()->Destroy();
 }
 
 // static
diff --git a/ui/views/controls/menu/menu_runner_unittest.cc b/ui/views/controls/menu/menu_runner_unittest.cc
index cde50741..9ba9dd83 100644
--- a/ui/views/controls/menu/menu_runner_unittest.cc
+++ b/ui/views/controls/menu/menu_runner_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
+#include "base/test/gtest_util.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
 #include "base/test/simple_test_tick_clock.h"
@@ -37,6 +38,7 @@
 #include "ui/views/test/menu_test_utils.h"
 #include "ui/views/test/test_views.h"
 #include "ui/views/test/views_test_base.h"
+#include "ui/views/test/widget_test.h"
 #include "ui/views/widget/any_widget_observer.h"
 #include "ui/views/widget/native_widget_private.h"
 #include "ui/views/widget/widget.h"
@@ -680,6 +682,32 @@
   ~MenuRunnerImplTest() override = default;
 };
 
+// Tests that when nested menu runners are destroyed out of order, that
+// MenuController is not accessed after it has been destroyed. This should not
+// crash on ASAN bots.
+TEST_F(MenuRunnerImplTest, NestedMenuRunnersDestroyedOutOfOrder) {
+  internal::MenuRunnerImpl* menu_runner =
+      new internal::MenuRunnerImpl(CreateMenuItemView());
+  menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(),
+                         MenuAnchorPosition::kTopLeft);
+
+  auto menu_delegate2 = std::make_unique<TestMenuDelegate>();
+  auto menu_item_view2 = std::make_unique<MenuItemView>(menu_delegate2.get());
+  menu_item_view2->AppendMenuItem(1, u"One");
+
+  internal::MenuRunnerImpl* menu_runner2 =
+      new internal::MenuRunnerImpl(std::move(menu_item_view2));
+  menu_runner2->RunMenuAt(
+      owner(), nullptr, gfx::Rect(), MenuAnchorPosition::kTopLeft,
+      ui::mojom::MenuSourceType::kNone, MenuRunner::IS_NESTED);
+
+  menu_runner->Cancel();
+
+  // This should not access the destroyed MenuController
+  menu_runner2->Release();
+  ResetMenuItemView();
+  menu_runner->Release();
+}
 
 // Regression test demonstrating that the host_-exists branch of
 // SubmenuView::ShowAt lacks a WeakPtr liveness guard after ShowMenuHost.
@@ -752,6 +780,351 @@
   EXPECT_TRUE(fired);
 }
 
+// Regression test demonstrating that MenuItemView destruction during ShowAt
+// is deferred and completes safely without crash.
+TEST_F(MenuRunnerImplTest, NestedRunnerReleasedDuringShowAtSafe) {
+  // Build a root menu containing one submenu item.
+  auto root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* sub_item = root->AppendSubMenu(100, u"Sub");
+  sub_item->AppendMenuItem(101, u"Leaf");
+
+  internal::MenuRunnerImpl* menu_runner =
+      new internal::MenuRunnerImpl(std::move(root));
+  menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                         MenuAnchorPosition::kTopLeft);
+
+  MenuController* controller = MenuController::GetForOwnerWidget(owner());
+  ASSERT_TRUE(controller);
+
+  // We need to create a nested runner to simulate the nested menu.
+  auto nested_root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* nested_sub_item = nested_root->AppendSubMenu(200, u"NestedSub");
+  nested_sub_item->AppendMenuItem(201, u"NestedLeaf");
+
+  internal::MenuRunnerImpl* nested_runner =
+      new internal::MenuRunnerImpl(std::move(nested_root));
+  nested_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                           MenuAnchorPosition::kTopLeft,
+                           ui::mojom::MenuSourceType::kNone,
+                           MenuRunner::IS_NESTED);
+
+  bool fired = false;
+  base::WeakPtr<Widget> host_widget;
+  AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+  observer.set_shown_callback(
+      base::BindLambdaForTesting([&](views::Widget* widget) {
+        if (fired || widget->GetName() != "MenuHost") {
+          return;
+        }
+        fired = true;
+        host_widget = widget->GetWeakPtr();
+        // Release the runner. This should NOT crash, but defer deletion.
+        nested_runner->Release();
+
+        // The widget should still be alive because deletion is deferred.
+        ASSERT_TRUE(host_widget);
+      }));
+
+  nested_sub_item->GetMenuController()->SelectItemAndOpenSubmenu(
+      nested_sub_item);
+
+  EXPECT_TRUE(fired);
+
+  // After ShowSubmenuImmediately returns, the stack depth returns to 0.
+  // But since we didn't cancel the menu, the widget should still be alive.
+  EXPECT_TRUE(host_widget);
+
+  // Now cancel the menu. This should trigger OnMenuClosed, which will
+  // see delete_after_run_ is true, and delete the runner.
+  controller->Cancel(MenuController::ExitType::kAll);
+
+  // Wait for the runner to be deleted and destroy the widget.
+  if (host_widget) {
+    views::test::WidgetDestroyedWaiter(host_widget.get()).Wait();
+  }
+  EXPECT_FALSE(host_widget);
+
+  // Clean up parent.
+  menu_runner->Release();
+}
+
+// Regression test demonstrating that MenuController destruction during ShowAt
+// is blocked and results in a CHECK failure.
+using MenuRunnerImplDeathTest = MenuRunnerImplTest;
+TEST_F(MenuRunnerImplDeathTest, MenuControllerDeletedDuringShowAtCHECK) {
+  // Build a root menu containing one submenu item.
+  auto root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* root_ptr = root.get();
+  MenuItemView* sub_item = root->AppendSubMenu(100, u"Sub");
+  sub_item->AppendMenuItem(101, u"Leaf");
+
+  internal::MenuRunnerImpl* menu_runner =
+      new internal::MenuRunnerImpl(std::move(root));
+  menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                         MenuAnchorPosition::kTopLeft);
+
+  MenuController* controller = MenuController::GetForOwnerWidget(owner());
+  ASSERT_TRUE(controller);
+  Widget* root_widget = root_ptr->GetSubmenu()->GetWidget();
+  ASSERT_TRUE(root_widget);
+
+  // We need to create a nested runner to simulate the nested menu.
+  auto nested_root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* nested_root_ptr = nested_root.get();
+  MenuItemView* nested_sub_item = nested_root->AppendSubMenu(200, u"NestedSub");
+  nested_sub_item->AppendMenuItem(201, u"NestedLeaf");
+
+  internal::MenuRunnerImpl* nested_runner =
+      new internal::MenuRunnerImpl(std::move(nested_root));
+  nested_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                           MenuAnchorPosition::kTopLeft,
+                           ui::mojom::MenuSourceType::kNone,
+                           MenuRunner::IS_NESTED);
+  Widget* nested_widget = nested_root_ptr->GetSubmenu()->GetWidget();
+  ASSERT_TRUE(nested_widget);
+
+  // We expect the deletion to crash the process.
+  EXPECT_DEATH(
+      {
+        AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+        bool fired = false;
+        observer.set_shown_callback(
+            base::BindLambdaForTesting([&](views::Widget* widget) {
+              if (fired || widget->GetName() != "MenuHost") {
+                return;
+              }
+              fired = true;
+              // Fake scenario to delete the instance while blocking the
+              // deletion.
+              MenuController::DeleteForTesting(
+                  MenuController::GetActiveInstance());
+            }));
+        MenuController::GetActiveInstance()->SelectItemAndOpenSubmenu(
+            nested_sub_item);
+      },
+      "Check failed: stack_depth_ == 0");
+
+  // Clean up in the parent process.
+  base::WeakPtr<Widget> root_weak = root_widget->GetWeakPtr();
+  base::WeakPtr<Widget> nested_weak = nested_widget->GetWeakPtr();
+
+  nested_runner->Release();
+  menu_runner->Release();
+
+  if (nested_weak) {
+    views::test::WidgetDestroyedWaiter(nested_weak.get()).Wait();
+  }
+  if (root_weak) {
+    views::test::WidgetDestroyedWaiter(root_weak.get()).Wait();
+  }
+}
+
+// Regression test demonstrating that MenuHost destruction during ShowAt
+// is deferred and completed after ShowAt returns.
+TEST_F(MenuRunnerImplTest, MenuHostDeferredDuringShowAt) {
+  // Build a root menu containing one submenu item.
+  auto root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* sub_item = root->AppendSubMenu(100, u"Sub");
+  sub_item->AppendMenuItem(101, u"Leaf");
+
+  internal::MenuRunnerImpl* menu_runner =
+      new internal::MenuRunnerImpl(std::move(root));
+  menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                         MenuAnchorPosition::kTopLeft);
+
+  MenuController* controller = MenuController::GetForOwnerWidget(owner());
+  ASSERT_TRUE(controller);
+
+  // We need to create a nested runner to simulate the nested menu.
+  auto nested_root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* nested_sub_item = nested_root->AppendSubMenu(200, u"NestedSub");
+  nested_sub_item->AppendMenuItem(201, u"NestedLeaf");
+
+  internal::MenuRunnerImpl* nested_runner =
+      new internal::MenuRunnerImpl(std::move(nested_root));
+  nested_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                           MenuAnchorPosition::kTopLeft,
+                           ui::mojom::MenuSourceType::kNone,
+                           MenuRunner::IS_NESTED);
+
+  bool fired = false;
+  base::WeakPtr<Widget> host_widget;
+  AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+  observer.set_shown_callback(
+      base::BindLambdaForTesting([&](views::Widget* widget) {
+        if (fired || widget->GetName() != "MenuHost") {
+          return;
+        }
+        fired = true;
+        host_widget = widget->GetWeakPtr();
+
+        // Cancel the menu during ShowAt. This calls DestroyMenuHost on
+        // MenuHost. Since we are in ShowAt, the destruction should be deferred.
+        controller->Cancel(MenuController::ExitType::kAll);
+
+        // The widget should still be alive (destruction deferred).
+        ASSERT_TRUE(host_widget);
+        EXPECT_TRUE(static_cast<MenuHost*>(host_widget.get())
+                        ->destroying_for_testing());
+      }));
+
+  nested_sub_item->GetMenuController()->SelectItemAndOpenSubmenu(
+      nested_sub_item);
+
+  EXPECT_TRUE(fired);
+  // Wait for the deferred Widget::Close() to complete.
+  if (host_widget) {
+    views::test::WidgetDestroyedWaiter(host_widget.get()).Wait();
+  }
+  // After ShowSubmenuImmediately returns and waiter finishes, the deferred
+  // close should have run, destroying the widget.
+  EXPECT_FALSE(host_widget);
+
+  // Clean up
+  nested_runner->Release();
+  menu_runner->Release();
+}
+
+// Regression test demonstrating that MenuController destruction during ShowAt
+// is deferred and completed after ShowAt returns.
+TEST_F(MenuRunnerImplTest, MenuControllerDeferredDestructionDuringShowAt) {
+  // Build a root menu containing one submenu item.
+  auto root = std::make_unique<TestMenuItemView>(menu_delegate());
+  MenuItemView* sub_item = root->AppendSubMenu(100, u"Sub");
+  sub_item->AppendMenuItem(101, u"Leaf");
+
+  internal::MenuRunnerImpl* menu_runner =
+      new internal::MenuRunnerImpl(std::move(root));
+  menu_runner->RunMenuAt(owner(), nullptr, gfx::Rect(gfx::Size(200, 200)),
+                         MenuAnchorPosition::kTopLeft);
+
+  MenuController* controller = MenuController::GetForOwnerWidget(owner());
+  ASSERT_TRUE(controller);
+  base::WeakPtr<MenuController> controller_weak = controller->AsWeakPtr();
+
+  // We need to create a nested runner to simulate the nested menu.
... (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.