diff --git a/content/browser/digital_credentials/digital_identity_request_impl_unittest.cc b/content/browser/digital_credentials/digital_identity_request_impl_unittest.cc index 5359a9993a9fe..fc31f442ceee5 100644 --- a/content/browser/digital_credentials/digital_identity_request_impl_unittest.cc +++ b/content/browser/digital_credentials/digital_identity_request_impl_unittest.cc @@ -1158,6 +1158,118 @@ TEST_F(DigitalIdentityRequestImplProviderResetTest, ASSERT_NO_FATAL_FAILURE(impl->Abort()); } +// A DigitalIdentityProvider whose ShowDigitalIdentityInterstitial() returns an +// abort callback that runs an arbitrary closure. This models the production +// behaviour where DigitalIdentitySafetyInterstitialControllerDesktop::Abort() +// closes and re-shows a constrained tab-modal Widget via Widget::CloseWithReason +// + constrained_window::ShowWebModal, which (per the in-tree comment on +// CompleteRequestWithStatus()) can synchronously fire activation observers that +// destroy the hosting WebContents. +class WebContentsDestroyingInterstitialProvider + : public StubDigitalIdentityProvider { + public: + explicit WebContentsDestroyingInterstitialProvider(base::OnceClosure on_abort) + : on_abort_(std::move(on_abort)) {} + ~WebContentsDestroyingInterstitialProvider() override = default; + + DigitalIdentityInterstitialAbortCallback ShowDigitalIdentityInterstitial( + WebContents&, + const url::Origin&, + DigitalIdentityInterstitialType, + DigitalIdentityInterstitialCallback) override { + return std::move(on_abort_); + } + + private: + base::OnceClosure on_abort_; +}; + +class WebContentsDestroyingInterstitialBrowserClient + : public ContentBrowserClient { + public: + explicit WebContentsDestroyingInterstitialBrowserClient( + base::OnceClosure on_abort) + : on_abort_(std::move(on_abort)) {} + ~WebContentsDestroyingInterstitialBrowserClient() override = default; + + std::unique_ptr CreateDigitalIdentityProvider() + override { + return std::make_unique( + std::move(on_abort_)); + } + + private: + base::OnceClosure on_abort_; +}; + +class DigitalIdentityRequestImplInterstitialAbortTest + : public RenderViewHostTestHarness { + public: + void SetUp() override { + RenderViewHostTestHarness::SetUp(); + // Force the interstitial path so update_interstitial_on_abort_callback_ is + // populated by Get(). + scoped_feature_list_.InitAndEnableFeatureWithParameters( + features::kWebIdentityDigitalCredentials, {{"dialog", "low_risk"}}); + + browser_client_ = + std::make_unique( + base::BindOnce(&DigitalIdentityRequestImplInterstitialAbortTest:: + DeleteContents, + base::Unretained(this))); + old_client_ = content::SetBrowserClientForTesting(browser_client_.get()); + } + + void TearDown() override { + content::SetBrowserClientForTesting(old_client_); + RenderViewHostTestHarness::TearDown(); + } + + private: + base::test::ScopedFeatureList scoped_feature_list_; + std::unique_ptr + browser_client_; + raw_ptr old_client_ = nullptr; +}; + +// Regression test: DigitalIdentityRequestImpl::Abort() runs +// update_interstitial_on_abort_callback_ — which on desktop closes and re-shows +// a constrained tab-modal Widget — and then unconditionally calls +// CompleteRequestWithStatus() on `this` with no weak_this guard. The sibling +// hazard for provider_.reset() is already guarded; this path is not. +// +// This test models the activation-observer destruction directly: the abort +// callback synchronously deletes the harness WebContents, which deletes the +// DocumentService (`this`). Under ASAN this surfaces as heap-use-after-free +// when Abort() calls CompleteRequestWithStatus() on the freed object. +TEST_F(DigitalIdentityRequestImplInterstitialAbortTest, + AbortWhileInterstitialAbortCallbackDestroysWebContents) { + mojo::Remote remote; + base::WeakPtr impl = + DigitalIdentityRequestImpl::CreateInstance( + *web_contents()->GetPrimaryMainFrame(), + remote.BindNewPipeAndPassReceiver()); + ASSERT_TRUE(impl); + + DigitalCredentialGetRequestPtr request = DigitalCredentialGetRequest::New(); + request->protocol = "openid4vp"; + request->data = base::Value(base::DictValue()); + std::vector requests; + requests.push_back(std::move(request)); + + // Populates `provider_`, `callback_`, and (via the interstitial path) + // `update_interstitial_on_abort_callback_` with the closure that deletes the + // hosting WebContents. + impl->Get(std::move(requests), base::DoNothing()); + + // Abort(): + // std::move(update_interstitial_on_abort_callback_).Run() + // -> DeleteContents() -> ~WebContentsImpl -> ~RenderFrameHostImpl + // -> DocumentService::ResetAndDeleteThisInternal -> delete impl + // CompleteRequestWithStatus(...) // <- member call on freed `this` + impl->Abort(); +} + TEST_F(DigitalIdentityRequestImplTest, ShouldReturnSameErrorForNoCredentialAndUserDeclined) { const std::string kProtocol = "protocol";