CVE-2026-79047
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/controls/menu/menu_controller.cc |
modified | |
TEST_Fui/views/controls/menu/menu_controller_unittest.cc |
modified | |
BindLambdaForTestingui/views/controls/menu/menu_controller_unittest.cc |
modified |
Files Changed
ui/views/controls/menu/menu_controller.ccui/views/controls/menu/menu_controller_unittest.cc
Patch
From 265aa71a9761f1d9a1e7b3bcb45662d815671a5b Mon Sep 17 00:00:00 2001 From: Daniel Clark <[email protected]> Date: Tue, 28 Jul 2026 09:54:17 -0700 Subject: [PATCH] Protect against MenuController being destroyed due to selection change MenuController calls out to SetSelection in several places, and the SetSelection call can cause accessibility events to be triggered that can cause the MenuController to be destroyed. crbug.com/517515945 identifies this issue in OnDragUpdate and an inspection of the code shows that there are several other spots that are also affected. In these spots where MenuController functions continue doing work after a call to SetSelection, fix the potential crashes by having the MenuController grab a weak ref to itself and bail out if it's null after the SetSelection call. Bug: 517515945 Change-Id: I0185d07633687593b147a0320ae73077b6a54d9f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8143939 Reviewed-by: Dana Fried <[email protected]> Commit-Queue: Dan Clark <[email protected]> Cr-Commit-Position: refs/heads/main@{#1669578} --- diff --git a/ui/views/controls/menu/menu_controller.cc b/ui/views/controls/menu/menu_controller.cc index 6eb15680..b600d44b 100644 --- a/ui/views/controls/menu/menu_controller.cc +++ b/ui/views/controls/menu/menu_controller.cc @@ -864,8 +864,15 @@ void MenuController::SelectItemAndOpenSubmenu(MenuItemView* item) { DCHECK(item); + auto this_ref = 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) { + return; + } + // If `item` has not a submenu, hot track `item`'s initial focusable button // if any. if (!item->HasSubmenu()) { @@ -1348,6 +1355,7 @@ } 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); @@ -1383,6 +1391,11 @@ } else { SetSelection(source->GetMenuItem(), SELECTION_OPEN_SUBMENU); } + // Accessibility events fired as a result of the selection changing may have + // closed the menu and deleted `this`. Guard against that. + if (!this_ref) { + return drop_operation; + } SetDropMenuItem(menu_item, drop_position); last_drop_operation_ = drop_operation; return drop_operation; @@ -2652,7 +2665,7 @@ } } // Setting the selection can indirectly destroy this object via accessibility - // system callbacks and activation changes. This should be rare bug must be + // system callbacks and activation changes. This should be rare but must be // protected against. const auto weak_this = AsWeakPtr(); SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); @@ -3304,8 +3317,15 @@ } // Show the sub-menu. + auto this_ref = 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) { + return; + } + MenuItemView* to_select = nullptr; if (!item->GetSubmenu()->GetMenuItems().empty()) { to_select = FindInitialSelectableMenuItem(item, INCREMENT_SELECTION_DOWN); @@ -3768,7 +3788,14 @@ if (!item) { return; } + auto this_ref = AsWeakPtr(); SetSelection(item, SELECTION_DEFAULT); + + // Accessibility events fired as a result of the selection changing may have + // closed the menu and deleted `this`. Guard against that. + if (!this_ref) { + return; + } View* hot_view = GetInitialFocusableView(item, direction == INCREMENT_SELECTION_DOWN); SetHotTrackedButton(Button::AsButton(hot_view)); diff --git a/ui/views/controls/menu/menu_controller_unittest.cc b/ui/views/controls/menu/menu_controller_unittest.cc index f962d71..6336da69 100644 --- a/ui/views/controls/menu/menu_controller_unittest.cc +++ b/ui/views/controls/menu/menu_controller_unittest.cc @@ -429,7 +429,7 @@ MenuAnchorPosition menu_anchor); protected: - void SetPendingStateItem(MenuItemView* item); + void SetPendingStateItem(MenuItemView* item, bool submenu_open = true); void SetState(MenuItemView* item); @@ -864,9 +864,10 @@ submenu->Close(); } -void MenuControllerTest::SetPendingStateItem(MenuItemView* item) { +void MenuControllerTest::SetPendingStateItem(MenuItemView* item, + bool submenu_open) { menu_controller_->pending_state_.item = item; - menu_controller_->pending_state_.submenu_open = true; + menu_controller_->pending_state_.submenu_open = submenu_open; } void MenuControllerTest::SetState(MenuItemView* item) { @@ -3114,6 +3115,158 @@ EXPECT_TRUE(observer.fired()); } +// Tests that OnDragUpdated handles the controller being synchronously +// destroyed by an accessibility observer reacting to the selection change. +// Should not crash in ASAN. +TEST_F(MenuControllerTest, DragUpdateWithControllerDeletedDuringSelection) { + MenuItemView* const item_with_buttons = + AddButtonMenuItems(/*single_child=*/true); + SubmenuView* const submenu = menu_item()->GetSubmenu(); + GET_CHILD_BUTTON(button, item_with_buttons, 0); + + // Select the item containing the button and make the button hot-tracked so + // that the next selection change clears it and updates the submenu's active + // descendant. + SetPendingStateItem(item_with_buttons); + SetHotTrackedButton(button); + + // Destroy the controller as a side effect of the active descendant changing + // during the selection update. + CallbackOnAXEventObserver observer( + ax::mojom::Event::kActiveDescendantChanged, + base::BindLambdaForTesting([this]() { DestroyMenuController(); })); + + // Dispatch a drag update at a point outside any menu item so that + // OnDragUpdated selects the root item, triggering the selection change. + ui::OSExchangeData drop_data; + const gfx::PointF location(-1, -1); + const ui::DropTargetEvent target_event(drop_data, location, location, + ui::DragDropTypes::DRAG_MOVE); + menu_controller()->OnDragUpdated(submenu, target_event); + + EXPECT_TRUE(observer.fired()); + EXPECT_EQ(nullptr, menu_controller()); +} + +// Tests that SelectItemAndOpenSubmenu handles the controller being +// synchronously destroyed by an accessibility observer reacting to the +// selection change. Should not crash in ASAN. +TEST_F(MenuControllerTest, + SelectItemAndOpenSubmenuWithControllerDeletedDuringSelection) { + MenuItemView* const item_with_buttons = + AddButtonMenuItems(/*single_child=*/true); + SubmenuView* const submenu = menu_item()->GetSubmenu(); + GET_CHILD_BUTTON(button, item_with_buttons, 0); + + // Select the item containing the button and make the button hot-tracked so + // that the next selection change clears it and updates the submenu's active + // descendant. + SetPendingStateItem(item_with_buttons); + SetHotTrackedButton(button); + + // Destroy the controller as a side effect of the active descendant changing + // during the selection update. + CallbackOnAXEventObserver observer( + ax::mojom::Event::kActiveDescendantChanged, + base::BindLambdaForTesting([this]() { DestroyMenuController(); })); + + // Selecting a different (leaf) item clears the hot-tracked button, firing the + // active descendant change that destroys the controller mid-selection. + menu_controller()->SelectItemAndOpenSubmenu(submenu->GetMenuItemAt(0)); + + EXPECT_TRUE(observer.fired()); + EXPECT_EQ(nullptr, menu_controller()); +} + +// Tests that MenuChildrenChanged handles the controller being synchronously
Regression Test / PoC
diff --git a/ui/views/controls/menu/menu_controller_unittest.cc b/ui/views/controls/menu/menu_controller_unittest.cc
index f962d71..6336da69 100644
--- a/ui/views/controls/menu/menu_controller_unittest.cc
+++ b/ui/views/controls/menu/menu_controller_unittest.cc
@@ -429,7 +429,7 @@
MenuAnchorPosition menu_anchor);
protected:
- void SetPendingStateItem(MenuItemView* item);
+ void SetPendingStateItem(MenuItemView* item, bool submenu_open = true);
void SetState(MenuItemView* item);
@@ -864,9 +864,10 @@
submenu->Close();
}
-void MenuControllerTest::SetPendingStateItem(MenuItemView* item) {
+void MenuControllerTest::SetPendingStateItem(MenuItemView* item,
+ bool submenu_open) {
menu_controller_->pending_state_.item = item;
- menu_controller_->pending_state_.submenu_open = true;
+ menu_controller_->pending_state_.submenu_open = submenu_open;
}
void MenuControllerTest::SetState(MenuItemView* item) {
@@ -3114,6 +3115,158 @@
EXPECT_TRUE(observer.fired());
}
+// Tests that OnDragUpdated handles the controller being synchronously
+// destroyed by an accessibility observer reacting to the selection change.
+// Should not crash in ASAN.
+TEST_F(MenuControllerTest, DragUpdateWithControllerDeletedDuringSelection) {
+ MenuItemView* const item_with_buttons =
+ AddButtonMenuItems(/*single_child=*/true);
+ SubmenuView* const submenu = menu_item()->GetSubmenu();
+ GET_CHILD_BUTTON(button, item_with_buttons, 0);
+
+ // Select the item containing the button and make the button hot-tracked so
+ // that the next selection change clears it and updates the submenu's active
+ // descendant.
+ SetPendingStateItem(item_with_buttons);
+ SetHotTrackedButton(button);
+
+ // Destroy the controller as a side effect of the active descendant changing
+ // during the selection update.
+ CallbackOnAXEventObserver observer(
+ ax::mojom::Event::kActiveDescendantChanged,
+ base::BindLambdaForTesting([this]() { DestroyMenuController(); }));
+
+ // Dispatch a drag update at a point outside any menu item so that
+ // OnDragUpdated selects the root item, triggering the selection change.
+ ui::OSExchangeData drop_data;
+ const gfx::PointF location(-1, -1);
+ const ui::DropTargetEvent target_event(drop_data, location, location,
+ ui::DragDropTypes::DRAG_MOVE);
+ menu_controller()->OnDragUpdated(submenu, target_event);
+
+ EXPECT_TRUE(observer.fired());
+ EXPECT_EQ(nullptr, menu_controller());
+}
+
+// Tests that SelectItemAndOpenSubmenu handles the controller being
+// synchronously destroyed by an accessibility observer reacting to the
+// selection change. Should not crash in ASAN.
+TEST_F(MenuControllerTest,
+ SelectItemAndOpenSubmenuWithControllerDeletedDuringSelection) {
+ MenuItemView* const item_with_buttons =
+ AddButtonMenuItems(/*single_child=*/true);
+ SubmenuView* const submenu = menu_item()->GetSubmenu();
+ GET_CHILD_BUTTON(button, item_with_buttons, 0);
+
+ // Select the item containing the button and make the button hot-tracked so
+ // that the next selection change clears it and updates the submenu's active
+ // descendant.
+ SetPendingStateItem(item_with_buttons);
+ SetHotTrackedButton(button);
+
+ // Destroy the controller as a side effect of the active descendant changing
+ // during the selection update.
+ CallbackOnAXEventObserver observer(
+ ax::mojom::Event::kActiveDescendantChanged,
+ base::BindLambdaForTesting([this]() { DestroyMenuController(); }));
+
+ // Selecting a different (leaf) item clears the hot-tracked button, firing the
+ // active descendant change that destroys the controller mid-selection.
+ menu_controller()->SelectItemAndOpenSubmenu(submenu->GetMenuItemAt(0));
+
+ EXPECT_TRUE(observer.fired());
+ EXPECT_EQ(nullptr, menu_controller());
+}
+
+// Tests that MenuChildrenChanged handles the controller being synchronously
+// destroyed by an accessibility observer reacting to the selection change.
+// Should not crash in ASAN.
+TEST_F(MenuControllerTest,
+ MenuChildrenChangedWithControllerDeletedDuringSelection) {
+ MenuItemView* const item_with_buttons =
+ AddButtonMenuItems(/*single_child=*/true);
+ GET_CHILD_BUTTON(button, item_with_buttons, 0);
+
+ SetPendingStateItem(item_with_buttons);
+ SetHotTrackedButton(button);
+
+ CallbackOnAXEventObserver observer(
+ ax::mojom::Event::kActiveDescendantChanged,
+ base::BindLambdaForTesting([this]() { DestroyMenuController(); }));
+
+ // Reselecting the changed item (the root) clears the hot-tracked button and
+ // fires the active descendant change that destroys the controller
+ // mid-selection.
+ MenuChildrenChanged(menu_item());
+
+ EXPECT_TRUE(observer.fired());
+ EXPECT_EQ(nullptr, menu_controller());
+}
+
+// Tests that SetInitialHotTrackedView (reached via keyboard navigation into an
+// open submenu) handles the controller being synchronously destroyed by an
+// accessibility observer reacting to the selection change. Should not crash in
+// ASAN.
+TEST_F(MenuControllerTest,
+ SetInitialHotTrackedViewWithControllerDeletedDuringSelection) {
+ // Give the root a submenu item with a child so arrow navigation moves the
+ // selection into the submenu via SetInitialHotTrackedView().
+ MenuItemView* const submenu_item = menu_item()->AppendSubMenu(10, u"Submenu");
+ submenu_item->AppendMenuItem(11, u"Child");
+ MenuItemView* const item_with_buttons =
+ AddButtonMenuItems(/*single_child=*/true);
+ GET_CHILD_BUTTON(button, item_with_buttons, 0);
+ ShowSubmenu(submenu_item->GetSubmenu());
+
+ // Select the submenu item (with its submenu open) and hot-track a sibling's
+ // button so the next selection change clears it and fires the active
+ // descendant change.
+ SetPendingStateItem(submenu_item);
+ SetHotTrackedButton(button);
+
+ CallbackOnAXEventObserver observer(
+ ax::mojom::Event::kActiveDescendantChanged,
+ base::BindLambdaForTesting([this]() { DestroyMenuController(); }));
+
+ // Arrow-down moves the selection into the open submenu, whose SetSelection()
+ // clears the hot-tracked button and destroys the controller mid-selection.
+ IncrementSelection();
+
+ EXPECT_TRUE(observer.fired());
+ EXPECT_EQ(nullptr, menu_controller());
+}
+
+// Tests that OpenSubmenuChangeSelectionIfCan (reached via the right-arrow key)
+// handles the controller being synchronously destroyed by an accessibility
+// observer reacting to the selection change. Should not crash in ASAN.
+TEST_F(MenuControllerTest,
+ OpenSubmenuChangeSelectionIfCanWithControllerDeletedDuringSelection) {
+ MenuItemView* const submenu_item = menu_item()->AppendSubMenu(10, u"Submenu");
+ submenu_item->AppendMenuItem(11, u"Child");
+ MenuItemView* const item_with_buttons =
+ AddButtonMenuItems(/*single_child=*/true);
+ GET_CHILD_BUTTON(button, item_with_buttons, 0);
+ ShowSubmenu(submenu_item->GetSubmenu());
+
+ // Select the submenu item without opening its submenu, and hot-track a
+ // sibling's button so that opening the submenu clears it and fires the active
+ // descendant change.
+ SetPendingStateItem(submenu_item, /*submenu_open=*/false);
+ SetHotTrackedButton(button);
+
+ CallbackOnAXEventObserver observer(
+ ax::mojom::Event::kActiveDescendantChanged,
+ base::BindLambdaForTesting([this]() { DestroyMenuController(); }));
+
+ // Right-arrow opens the submenu via OpenSubmenuChangeSelectionIfCan(), whose
+ // first SetSelection() clears the hot-tracked button and destroys the
+ // controller mid-selection.
+ DispatchKey(ui::VKEY_RIGHT);
+
+ EXPECT_TRUE(observer.fired());
+ EXPECT_EQ(nullptr, menu_controller());
+}
+
TEST_F(MenuControllerTest, SetSelectionIndices_MenuItemsOnly) {
SubmenuView* const submenu = menu_item()->GetSubmenu();
MenuItemView* const item1 = submenu->GetMenuItemAt(0);
Original Bug Report
Potential Use-After-Free in MenuController::OnDragUpdated via accessibility re-entrancy
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in MenuController::OnDragUpdated due to a missing WeakPtr validation guard after invoking SetSelection(). Focus or selection changes inside SetSelection() can trigger synchronous menu dismissal and deletion of the MenuController via re-entrant accessibility events. Subsequent executions in OnDragUpdated operate on the freed memory, potentially leading to memory corruption in the browser process.
Affected files:
ui/views/controls/menu/menu_controller.cc
Estimated timestamp from git blame: 2026-02-10
Summary
A potential Use-After-Free (UAF) memory corruption vulnerability exists in the browser process within MenuController::OnDragUpdated in ui/views/controls/menu/menu_controller.cc. When processing drag-and-drop hover updates, OnDragUpdated calls SetSelection(). If active accessibility software (such as a screen reader or automation client) is running, selection/focus updates can synchronously fire native OS accessibility events. Processing these events can trigger synchronous dismissal of the menu and deletion of the MenuController instance. Because OnDragUpdated does not check if this has been destroyed after SetSelection() returns, it executes subsequent operations on the deallocated controller object.
Root Cause Analysis
In ui/views/controls/menu/menu_controller.cc, MenuController::OnDragUpdated performs the following sequence:
int MenuController::OnDragUpdated(SubmenuView* source,
const ui::DropTargetEvent& event) {
...
if (menu_item) {
...
// If the menu has a submenu, schedule the submenu to open.
SetSelection(menu_item, menu_item->HasSubmenu() ? SELECTION_OPEN_SUBMENU
: SELECTION_DEFAULT);
// Missing lifetime check for `this` here!
if (drop_position == MenuDelegate::DropPosition::kNone ||
drop_operation == ui::DragDropTypes::DRAG_NONE) {
menu_item = nullptr;
}
} else {
SetSelection(source->GetMenuItem(), SELECTION_OPEN_SUBMENU);
// Missing lifetime check for `this` here!
}
SetDropMenuItem(menu_item, drop_position); // UAF read and write on this->
last_drop_operation_ = drop_operation; // UAF write to this->last_drop_operation_
return drop_operation;
}
Inside SetSelection(), multiple guards using auto this_ref = AsWeakPtr() and checking if (!this_ref) return; exist to handle re-entrant destruction safely. These guards are necessary because operations like setting the popup focus override or firing accessibility selection events can cause active assistive technology (AT) observers to synchronously dismiss the menu, deleting the MenuController (via MenuRunnerImpl::OnMenuClosed -> delete controller_.get()).
When a guard inside SetSelection is triggered, it returns early. However, OnDragUpdated unconditionally continues execution after SetSelection() returns, leading to:
- A UAF read and write inside
SetDropMenuItem()when accessingdrop_target_tracker_(a by-valueViewTrackermember) and assigningdrop_position_. - A UAF write to
last_drop_operation_on the freedMenuControllerheap space.
Potential Steps to Reproduce
Note: These are potential steps based on static analysis of the codebase; our tooling does not currently have the capability to run code to confirm a working Proof of Concept.
- Enable system accessibility tools (e.g., a screen reader or magnifier) to activate Chromium’s accessibility engine.
- Open a menu that accepts drag-and-drop operations (such as the Bookmark Bar overflow menu or folder submenus).
- Drag a draggable web element or link from an attacker-controlled webpage and hover over the menu items.
- As the drag hover transitions between menu items,
MenuController::OnDragUpdatedis invoked, which callsSetSelection()and triggers synchronous accessibility events. - If the assistive technology client’s reaction causes a synchronous window focus loss or deactivation, the menu is dismissed and the
MenuControlleris deleted. - On return from
SetSelection(),OnDragUpdatedcontinues executing, resulting in heap read/write operations on the freedMenuControllerallocation.
Suggested Fix
To mitigate this issue, check the controller’s lifetime using its weak pointer factory after returning from any call to SetSelection() within OnDragUpdated():
auto this_ref = AsWeakPtr();
if (menu_item) {
...
SetSelection(menu_item, menu_item->HasSubmenu() ? SELECTION_OPEN_SUBMENU
: SELECTION_DEFAULT);
if (!this_ref) {
return ui::DragDropTypes::DRAG_NONE;
}
if (drop_position == MenuDelegate::DropPosition::kNone ||
drop_operation == ui::DragDropTypes::DRAG_NONE) {
menu_item = nullptr;
}
} else {
SetSelection(source->GetMenuItem(), SELECTION_OPEN_SUBMENU);
if (!this_ref) {
return ui::DragDropTypes::DRAG_NONE;
}
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.