diff --git a/chrome/browser/ui/autofill/payments/mandatory_reauth_bubble_controller_impl_unittest.cc b/chrome/browser/ui/autofill/payments/mandatory_reauth_bubble_controller_impl_unittest.cc index ec34c650ca88f..dbd6360c1c7a7 100644 --- a/chrome/browser/ui/autofill/payments/mandatory_reauth_bubble_controller_impl_unittest.cc +++ b/chrome/browser/ui/autofill/payments/mandatory_reauth_bubble_controller_impl_unittest.cc @@ -10,6 +10,7 @@ #include "base/test/mock_callback.h" #include "base/test/with_feature_override.h" #include "chrome/browser/ui/autofill/autofill_bubble_base.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/test/base/browser_with_test_window_test.h" #include "components/autofill/core/browser/metrics/payments/mandatory_reauth_metrics.h" #include "components/autofill/core/browser/test_utils/autofill_test_utils.h" @@ -141,6 +142,39 @@ TEST_P(MandatoryReauthBubbleControllerImplTestWithFeatureOverride, INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE( MandatoryReauthBubbleControllerImplTestWithFeatureOverride); +// Regression test: on macOS without biometrics, the accept callback enters +// AuthorizationCopyRights() which spins a nested CFRunLoop. While that nested +// run loop is pumping UI tasks, the WebContents (and with it this +// WebContentsUserData controller) can be destroyed via popup.close() from an +// opener page. When the modal returns, OnBubbleClosed() continues at line 229 +// and calls virtual UpdatePageActionIcon() on a freed `this`. +// +// This test models that scenario by supplying an accept callback that +// synchronously destroys the WebContents (the same end state the nested run +// loop produces on macOS). Under ASAN this triggers heap-use-after-free. +TEST_F(MandatoryReauthBubbleControllerImplTest, + OnBubbleClosedSurvivesWebContentsDestructionInAcceptCallback) { + TestMandatoryReauthBubbleControllerImpl* ctrl = controller(); + + // The accept callback destroys the WebContents that owns `ctrl` (via + // WebContentsUserData). On macOS this happens via the nested CFRunLoop + // pumping a window-close IPC; here we model it directly. + base::OnceClosure destroy_web_contents = base::BindOnce( + [](Browser* browser) { + browser->tab_strip_model()->DetachAndDeleteWebContentsAt(0); + }, + browser()); + + ctrl->SetupAndShowBubble(std::move(destroy_web_contents), + base::DoNothing(), + base::DoNothing()); + + // Simulate the user clicking "Yes" on the opt-in bubble. OnBubbleClosed() + // runs the accept callback (which frees `ctrl`), then dereferences `this` + // for UpdatePageActionIcon() — heap-use-after-free under ASAN. + ctrl->OnBubbleClosed(PaymentsUiClosedReason::kAccepted); +} + class MandatoryReauthBubbleControllerOptInBubbleMetricsTest : public MandatoryReauthBubbleControllerImplTest, public testing::WithParamInterface {