diff --git a/chrome/browser/ui/passwords/password_generation_popup_view_browsertest.cc b/chrome/browser/ui/passwords/password_generation_popup_view_browsertest.cc index 4d8134d0b250c..d4b3b5e6b5d7a 100644 --- a/chrome/browser/ui/passwords/password_generation_popup_view_browsertest.cc +++ b/chrome/browser/ui/passwords/password_generation_popup_view_browsertest.cc @@ -209,6 +209,76 @@ IN_PROC_BROWSER_TEST_F(PasswordGenerationPopupViewTest, CheckViewAccessibilitySelected(cancel_button, /*selected=*/true); } +// Demonstrates that ButtonSelectionUpdated() is not safe against re-entrancy +// during NotifyAXSelection(). On Windows, firing kMenuStart/kMenuPopupStart +// reaches ::UiaRaiseAutomationEvent which can synchronously pump pending UI- +// thread Mojo tasks (the same primitive that required WeakPtr guards in +// MenuController::SetSelection). A renderer-queued +// PasswordGenerationDriver::ShowPasswordEditingPopup that runs at that point +// re-enters the controller and calls view_->UpdateState() -> +// RemoveAllChildViews(), freeing the NudgePasswordButtons (and its child +// MdTextButtons) that the outer ButtonSelectionUpdated() / NotifyAXSelection() +// stack frames still hold bare pointers/references to. Under ASAN this test +// produces heap-use-after-free at popup_base_view.cc:375 and at +// password_generation_popup_view_views.cc:373. +IN_PROC_BROWSER_TEST_F(PasswordGenerationPopupViewTest, + ReentrantUpdateStateDuringNotifyAXSelection) { + views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{}, + "PasswordGenerationPopupViewViews"); + ASSERT_TRUE(content::ExecJs( + WebContents(), "document.getElementById('password_field').focus()")); + auto* client = ChromePasswordManagerClient::FromWebContents(WebContents()); + client->GeneratePassword( + autofill::password_generation::PasswordGenerationType::kManual); + waiter.WaitIfNeededAndGet(); + + base::WeakPtr controller = + client->generation_popup_controller(); + ASSERT_TRUE(controller); + ASSERT_TRUE(controller->IsVisible()); + PasswordGenerationPopupViewViews* popup_view = + static_cast(controller->view()); + + // Arm the simulated UIA-pump re-entrancy: when NotifyAXSelection fires + // kMenuStart on `popup_view`, run the body of the renderer-supplied + // PasswordGenerationDriver::ShowPasswordEditingPopup Mojo handler (controller + // reuse + Show(kEditGeneratedPassword)). This hook is invoked from + // ViewAccessibility::FireNativeEvent -- the exact callsite where, on Windows, + // ax_platform_node_->NotifyAccessibilityEvent reaches + // ::UiaRaiseAutomationEvent and synchronously pumps the COM STA, dispatching + // the queued Mojo task. + bool fired = false; + popup_view->GetViewAccessibility().set_accessibility_events_callback( + base::BindRepeating( + [](bool* fired, + base::WeakPtr ctrl, + const ui::AXPlatformNodeDelegate*, ax::mojom::Event event_type) { + if (*fired || event_type != ax::mojom::Event::kMenuStart) { + return; + } + *fired = true; + ASSERT_TRUE(ctrl); + // GetOrCreate() with matching bounds/web_contents/driver/element_id + // returns the existing controller, so the Mojo handler reduces to: + ctrl->UpdateGeneratedPassword(u"reentrant-pwd"); + // -> view_->UpdateState() -> nudge_password_buttons_view_ = nullptr + // RemoveAllChildViews(); (synchronously frees children) + ctrl->Show( + PasswordGenerationPopupController::kEditGeneratedPassword); + }, + &fired, controller)); + + // User presses Down-arrow on the kOfferGeneration popup. Drive it via the + // controller test hook to follow the exact same code path: + // SelectElement -> view_->ButtonSelectionUpdated() + // -> caches bare NudgePasswordButtons* + // -> NotifyAXSelection(*cancel_button) [fires kMenuStart -> re-entry] + // -> selected_view.GetViewAccessibility()... <-- UAF (freed button) + // -> nudge_password_buttons->UpdateFocus(...) <-- UAF (freed view) + controller->SelectCancelButtonForTesting(); + EXPECT_TRUE(fired); +} + IN_PROC_BROWSER_TEST_F(PasswordGenerationPopupViewTest, HeaderAccessibilityProperties) { auto* client = ChromePasswordManagerClient::FromWebContents(WebContents());