Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in FedCM
DescriptionUse after free in FedCM
ComponentFedCM
Sub-componentFedCM
Bug ClassUAF
Tracker557323166
Fix commit830aeea0f06c (chromium/src) +116/-32
CISA KEVNot listed
Creditedxinyang
Disclosed2026-10-01

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
modified
if
chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc
modified
if
chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
modified
TEST_F
chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
modified
BindLambdaForTesting
chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
  • chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
  • chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
  • chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc
From 830aeea0f06cb49499c00c0d27e74d17817d0264 Mon Sep 17 00:00:00 2001
From: Ryan Qin <ryqin@google.com>
Date: Wed, 09 Sep 2026 11:22:47 -0700
Subject: [PATCH] [FedCM] Guard against reentrancy UAF in modal dialog and desktop view

FedCmModalDialogView::ShowPopupWindow invokes OpenURLFromTab via the
WebContentsDelegate, which can synchronously destroy the modal dialog
view. Calling member functions or writing to member variables after
OpenURLFromTab results in a Use-After-Free. Additionally,
ResizeAndFocusPopupWindow() can also trigger re-entrant deletion of the
view. This CL adds:
- A null check for source_window_->GetDelegate() in ShowPopupWindow().
- base::WeakPtr checks after OpenURLFromTab() and after
  ResizeAndFocusPopupWindow() before continuing in ShowPopupWindow().
- Regression unit tests covering both deletion points.

In FedCmAccountSelectionView, several delegate notification methods can
synchronously destroy the view:
- Introduces a [[nodiscard]] NotifyDelegate() helper that guards
  delegate calls with a base::WeakPtr.
- Uses NotifyDelegate() across all delegate notification call sites
  (OnMoreDetails, OnAccountsDisplayed, OnLoginToIdP, OnGotIt).
- Replaces the existing NotifyDelegateOfAccountSelection() helper with
  direct calls to NotifyDelegate() and early returns upon deletion.

Fixed: 557323166
Change-Id: Ie3763e9fd84a3060b6a998d06c4fc0bb119f9f5d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8373422
Commit-Queue: Ryan Qin <ryqin@chromium.org>
Reviewed-by: Yi Gu <yigu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694753}
---
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
index 79893f5..c0d89c9 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc
@@ -173,8 +173,15 @@
         l10n_util::GetStringUTF16(IDS_FEDERATION_SIGNING_IN_TITLE));
     controller->ShowSuggestionChip(kActionFederation);
     controller->Show(kActionFederation);
-    NotifyDelegateOfAccountSelection(*accounts_[0],
-                                     *accounts_[0]->identity_provider);
+    if (!NotifyDelegate([&]() {
+          delegate_->OnAccountSelected(
+              accounts_[0]->identity_provider->idp_metadata.config_url,
+              accounts_[0]->id,
+              accounts_[0]->idp_claimed_login_state.value_or(
+                  accounts_[0]->browser_trusted_login_state));
+        })) {
+      return;
+    }
   } else {
     // For sign-up users, we show a full modal dialog that gathers the necessary
     // permission from the user (e.g. privacy policies and terms of services).
@@ -344,8 +351,12 @@
 
       if (should_show_verifying_sheet) {
         state_ = State::VERIFYING;
-        if (!NotifyDelegateOfAccountSelection(*new_accounts_[0],
-                                              new_idp_data)) {
+        if (!NotifyDelegate([&]() {
+              delegate_->OnAccountSelected(
+                  new_idp_data.idp_metadata.config_url, new_accounts_[0]->id,
+                  new_accounts_[0]->idp_claimed_login_state.value_or(
+                      new_accounts_[0]->browser_trusted_login_state));
+            })) {
           // `this` has been deleted.
           return false;
         }
@@ -667,7 +678,9 @@
 }
 
 void FedCmAccountSelectionView::OnAccountsDisplayed() {
-  delegate_->OnAccountsDisplayed();
+  if (!NotifyDelegate([&]() { delegate_->OnAccountsDisplayed(); })) {
+    return;
+  }
 }
 
 bool FedCmAccountSelectionView::OnAccountSelected(
@@ -698,7 +711,12 @@
       (state_ == State::SINGLE_ACCOUNT_PICKER &&
        dialog_type_ == DialogType::BUBBLE)) {
     state_ = State::VERIFYING;
-    if (!NotifyDelegateOfAccountSelection(*account, idp_data)) {
+    if (!NotifyDelegate([&]() {
+          delegate_->OnAccountSelected(
+              idp_data.idp_metadata.config_url, account->id,
+              account->idp_claimed_login_state.value_or(
+                  account->browser_trusted_login_state));
+        })) {
       // `this` was deleted.
       return false;
     }
@@ -825,7 +843,10 @@
         webid::AccountChooserResult::kUseOtherAccountButton;
   }
 
-  delegate_->OnLoginToIdP(idp_config_url, idp_login_url);
+  if (!NotifyDelegate(
+          [&]() { delegate_->OnLoginToIdP(idp_config_url, idp_login_url); })) {
+    return;
+  }
 }
 
 void FedCmAccountSelectionView::OnGotIt(const ui::Event& event) {
@@ -835,7 +856,10 @@
     return;
   }
 
-  delegate_->OnDismiss(DismissReason::kGotItButton);
+  if (!NotifyDelegate(
+          [&]() { delegate_->OnDismiss(DismissReason::kGotItButton); })) {
+    return;
+  }
 }
 
 void FedCmAccountSelectionView::OnMoreDetails(const ui::Event& event) {
@@ -845,8 +869,13 @@
     return;
   }
 
-  delegate_->OnMoreDetails();
-  delegate_->OnDismiss(DismissReason::kMoreDetailsButton);
+  if (!NotifyDelegate([&]() { delegate_->OnMoreDetails(); })) {
+    return;
+  }
+  if (!NotifyDelegate(
+          [&]() { delegate_->OnDismiss(DismissReason::kMoreDetailsButton); })) {
+    return;
+  }
 }
 
 content::WebContents* FedCmAccountSelectionView::ShowModalDialog(
@@ -1040,20 +1069,12 @@
   Close(/*notify_delegate=*/true, /*hide_widget=*/false);
 }
 
-bool FedCmAccountSelectionView::NotifyDelegateOfAccountSelection(
-    const Account& account,
-    const content::IdentityProviderData& idp_data) {
-  DCHECK(state_ == State::VERIFYING || state_ == State::AUTO_REAUTHN);
-
-  base::WeakPtr<FedCmAccountSelectionView> weak_ptr(
-      weak_ptr_factory_.GetWeakPtr());
-  delegate_->OnAccountSelected(idp_data.idp_metadata.config_url, account.id,
-                               account.idp_claimed_login_state.value_or(
-                                   account.browser_trusted_login_state));
-
-  // AccountSelectionView::Delegate::OnAccountSelected() might delete this.
-  // See https://crbug.com/40248291 for details.
-  return static_cast<bool>(weak_ptr);
+bool FedCmAccountSelectionView::NotifyDelegate(
+    base::FunctionRef<void()> notify_callback) {
+  base::WeakPtr<FedCmAccountSelectionView> weak_this =
+      weak_ptr_factory_.GetWeakPtr();
+  notify_callback();
+  return weak_this != nullptr;
 }
 
 void FedCmAccountSelectionView::ShowVerifyingSheet(
diff --git a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
index feca0a4e..2d65b55c 100644
--- a/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
+++ b/chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "base/functional/function_ref.h"
 #include "base/gtest_prod_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
@@ -398,11 +399,10 @@
   // the background, becomes foregrounded.
   void BackgroundTaskTabForegrounded(tabs::TabInterface* tab);
 
+  // Calls a delegate method while guarding against `this` being deleted.
   // Returns false if `this` got deleted. In that case, the caller must early
   // return.
-  bool NotifyDelegateOfAccountSelection(
-      const Account& account,
-      const content::IdentityProviderData& idp_data);
+  [[nodiscard]] bool NotifyDelegate(base::FunctionRef<void()> notify_callback);
 
   // Shows the verifying sheet.
   void ShowVerifyingSheet(const IdentityRequestAccountPtr& account);
diff --git a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc
index 33521fa6..f0e0b40a 100644
--- a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc
+++ b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc
@@ -65,10 +65,12 @@
   // the latter only tracks tab fullscreen (e.g., a video playing in
   // fullscreen), not browser fullscreen (e.g., pressing F11). `GetDisplayMode`
   // returns `kFullscreen` in both cases.
-  bool is_fullscreen =
-      source_window_->GetDelegate() &&
-      source_window_->GetDelegate()->GetDisplayMode(source_window_) ==
-          blink::mojom::DisplayMode::kFullscreen;
+  content::WebContentsDelegate* delegate = source_window_->GetDelegate();
+  if (!delegate) {
+    return nullptr;
+  }
+  bool is_fullscreen = delegate->GetDisplayMode(source_window_) ==
+                       blink::mojom::DisplayMode::kFullscreen;
   WindowOpenDisposition disposition =
       is_fullscreen ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                     : WindowOpenDisposition::NEW_POPUP;
@@ -76,9 +78,16 @@
   content::OpenURLParams params(url, content::Referrer(), disposition,
                                 ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
                                 /*is_renderer_initiated=*/false);
-  popup_window_ = source_window_->GetDelegate()->OpenURLFromTab(
+  base::WeakPtr<FedCmModalDialogView> weak_this =
+      weak_ptr_factory_.GetWeakPtr();
+  content::WebContents* popup_window = delegate->OpenURLFromTab(
       source_window_, params, /*navigation_handle_callback=*/{});
 
+  if (!weak_this) {
+    return nullptr;
+  }
+
+  popup_window_ = popup_window;
   if (!popup_window_) {
     return nullptr;
   }
@@ -87,6 +96,9 @@
   // because we requested a `NEW_FOREGROUND_TAB` disposition.
   if (!is_fullscreen) {
     ResizeAndFocusPopupWindow();
+    if (!weak_this) {
+      return nullptr;
+    }
   }
   Observe(popup_window_);
 
diff --git a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
index 773aa5ec..d3806873 100644
--- a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
+++ b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ui/webid/identity_ui_utils.h"
 #include "chrome/test/base/testing_profile.h"
@@ -51,6 +52,10 @@
       const content::OpenURLParams& params,
       base::OnceCallback<void(content::NavigationHandle&)>
           navigation_handle_callback) override {
+    if (on_open_url_from_tab_) {
+      std::move(on_open_url_from_tab_).Run();
+    }
+
     if (should_return_null_popup_window_) {
       return nullptr;
     }
@@ -65,6 +70,12 @@
     bounds_ = bounds;
   }
 
+  void ActivateContents(content::WebContents* contents) override {
+    if (on_activate_contents_) {
+      std::move(on_activate_contents_).Run();
+    }
+  }
+
   blink::mojom::DisplayMode GetDisplayMode(
       const content::WebContents* web_contents) override {
     return is_fullscreen_ ? blink::mojom::DisplayMode::kFullscreen
@@ -77,6 +88,14 @@
 
   void SetIsFullscreen(bool is_fullscreen) { is_fullscreen_ = is_fullscreen; }
 
+  void SetOnOpenURLFromTab(base::OnceClosure on_open_url_from_tab) {
+    on_open_url_from_tab_ = std::move(on_open_url_from_tab);
+  }
+
+  void SetOnActivateContents(base::OnceClosure on_activate_contents) {
+    on_activate_contents_ = std::move(on_activate_contents);
+  }
+
   int opened() const { return opened_; }
   gfx::Rect bounds() const { return bounds_; }
   WindowOpenDisposition disposition() const { return disposition_; }
@@ -85,6 +104,8 @@
   int opened_ = 0;
   bool should_return_null_popup_window_{false};
   bool is_fullscreen_{false};
+  base::OnceClosure on_open_url_from_tab_;
+  base::OnceClosure on_activate_contents_;
   gfx::Rect bounds_;
   WindowOpenDisposition disposition_{WindowOpenDisposition::UNKNOWN};
 };
@@ -105,6 +126,36 @@
   ASSERT_TRUE(web_contents);
 }
 
+TEST_F(FedCmModalDialogViewTest, ShowPopupWindowDeletesView) {
+  TestDelegate delegate(web_contents());
+
+  std::unique_ptr<FedCmModalDialogView> popup_window_view =
+      std::make_unique<FedCmModalDialogView>(web_contents(),
+                                             /*observer=*/nullptr);
+  delegate.SetOnOpenURLFromTab(
+      base::BindLambdaForTesting([&]() { popup_window_view.reset(); }));
+  content::WebContents* web_contents = popup_window_view->ShowPopupWindow(
+      GURL(u"https://example.com"), /*user_close_cancels_flow=*/true);
+
+  EXPECT_EQ(1, delegate.opened());
+  EXPECT_FALSE(web_contents);
+}
+
+TEST_F(FedCmModalDialogViewTest, ShowPopupWindowDeletesViewDuringResize) {
+  TestDelegate delegate(web_contents());
+
+  std::unique_ptr<FedCmModalDialogView> popup_window_view =
+      std::make_unique<FedCmModalDialogView>(web_contents(),
+                                             /*observer=*/nullptr);
+  delegate.SetOnActivateContents(
+      base::BindLambdaForTesting([&]() { popup_window_view.reset(); }));
+  content::WebContents* web_contents = popup_window_view->ShowPopupWindow(
+      GURL(u"https://example.com"), /*user_close_cancels_flow=*/true);
+
+  EXPECT_EQ(1, delegate.opened());
+  EXPECT_FALSE(web_contents);
+}
+
 TEST_F(FedCmModalDialogViewTest, ShowPopupWindowInFullscreen) {
   // Override the delegate to test that OpenURLFromTab gets called.
   TestDelegate delegate(web_contents());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
index 773aa5ec..d3806873 100644
--- a/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
+++ b/chrome/browser/ui/views/webid/fedcm_modal_dialog_view_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ui/webid/identity_ui_utils.h"
 #include "chrome/test/base/testing_profile.h"
@@ -51,6 +52,10 @@
       const content::OpenURLParams& params,
       base::OnceCallback<void(content::NavigationHandle&)>
           navigation_handle_callback) override {
+    if (on_open_url_from_tab_) {
+      std::move(on_open_url_from_tab_).Run();
+    }
+
     if (should_return_null_popup_window_) {
       return nullptr;
     }
@@ -65,6 +70,12 @@
     bounds_ = bounds;
   }
 
+  void ActivateContents(content::WebContents* contents) override {
+    if (on_activate_contents_) {
+      std::move(on_activate_contents_).Run();
+    }
+  }
+
   blink::mojom::DisplayMode GetDisplayMode(
       const content::WebContents* web_contents) override {
     return is_fullscreen_ ? blink::mojom::DisplayMode::kFullscreen
@@ -77,6 +88,14 @@
 
   void SetIsFullscreen(bool is_fullscreen) { is_fullscreen_ = is_fullscreen; }
 
+  void SetOnOpenURLFromTab(base::OnceClosure on_open_url_from_tab) {
+    on_open_url_from_tab_ = std::move(on_open_url_from_tab);
+  }
+
+  void SetOnActivateContents(base::OnceClosure on_activate_contents) {
+    on_activate_contents_ = std::move(on_activate_contents);
+  }
+
   int opened() const { return opened_; }
   gfx::Rect bounds() const { return bounds_; }
   WindowOpenDisposition disposition() const { return disposition_; }
@@ -85,6 +104,8 @@
   int opened_ = 0;
   bool should_return_null_popup_window_{false};
   bool is_fullscreen_{false};
+  base::OnceClosure on_open_url_from_tab_;
+  base::OnceClosure on_activate_contents_;
   gfx::Rect bounds_;
   WindowOpenDisposition disposition_{WindowOpenDisposition::UNKNOWN};
 };
@@ -105,6 +126,36 @@
   ASSERT_TRUE(web_contents);
 }
 
+TEST_F(FedCmModalDialogViewTest, ShowPopupWindowDeletesView) {
+  TestDelegate delegate(web_contents());
+
+  std::unique_ptr<FedCmModalDialogView> popup_window_view =
+      std::make_unique<FedCmModalDialogView>(web_contents(),
+                                             /*observer=*/nullptr);
+  delegate.SetOnOpenURLFromTab(
+      base::BindLambdaForTesting([&]() { popup_window_view.reset(); }));
+  content::WebContents* web_contents = popup_window_view->ShowPopupWindow(
+      GURL(u"https://example.com"), /*user_close_cancels_flow=*/true);
+
+  EXPECT_EQ(1, delegate.opened());
+  EXPECT_FALSE(web_contents);
+}
+
+TEST_F(FedCmModalDialogViewTest, ShowPopupWindowDeletesViewDuringResize) {
+  TestDelegate delegate(web_contents());
+
+  std::unique_ptr<FedCmModalDialogView> popup_window_view =
+      std::make_unique<FedCmModalDialogView>(web_contents(),
+                                             /*observer=*/nullptr);
+  delegate.SetOnActivateContents(
+      base::BindLambdaForTesting([&]() { popup_window_view.reset(); }));
+  content::WebContents* web_contents = popup_window_view->ShowPopupWindow(
+      GURL(u"https://example.com"), /*user_close_cancels_flow=*/true);
+
+  EXPECT_EQ(1, delegate.opened());
+  EXPECT_FALSE(web_contents);
+}
+
 TEST_F(FedCmModalDialogViewTest, ShowPopupWindowInFullscreen) {
   // Override the delegate to test that OpenURLFromTab gets called.
   TestDelegate delegate(web_contents());
Loading diff…

Original Bug Report

reported by xi...@anthropic.com

FedCM `FedCmModalDialogView::ShowPopupWindow` / `FedCmAccountSelectionView::OnMoreDetails` use `this` after `OpenURLFromTab()` can have destroyed it — browser-process use-after-free WRITE + virtual call

component of chain reported in issue 557108531

Report #5: FedCM FedCmModalDialogView::ShowPopupWindow / FedCmAccountSelectionView::OnMoreDetails use this after OpenURLFromTab() can have destroyed it — browser-process use-after-free WRITE + virtual call

Summary

webid::FedCmModalDialogView::ShowPopupWindow() opens the IdP pop-up with WebContentsDelegate::OpenURLFromTab() and then assigns the result to its member popup_window_, calls the virtual ResizeAndFocusPopupWindow(), and registers itself as a WebContentsObserver. Opening and activating a new browser window runs embedder code that can synchronously destroy the RP document and, with it, the whole FedCM object tree (webid::RequestService → IdentityDialogController → FedCmAccountSelectionView → FedCmModalDialogView). Upstream commits 5d16106435e6 / 63a684bc6a90 (2026-09-01) added weak-this guards for exactly this scenario one layer down in content::webid::Request, but the views object that actually opens the window still uses this afterwards.

Needs: a synchronous destroyer of the RP frame inside OpenURLFromTab(). The only one we know to be web-reachable is Report #4 (a JS-dialog-deferred prerender activation resumed by TabModalDialogManager::HandleTabSwitchAway() when the pop-up window becomes active); with that, an attacker RP page plus an attacker IdP reach it with one click anywhere on the page (or with none from a compromised renderer, which can assert user activation itself). Gets: an 8-byte pointer write into a freed 104-byte FedCmModalDialogView, followed by a virtual call through the freed object (ResizeAndFocusPopupWindow()), then a dangling WebContentsObserver registration — in the browser process. This is the browser-process primitive used in the end-to-end chain described in ../CHAIN.md.

Affected versions / platforms

Tested:

  • Chromium main at d81900a3939b6 (155.0.8038.0), Linux x86-64: ASan build (report below, 5/5 in earlier runs, 1/1 re-run for this package); non-ASan release build with PartitionAlloc-Everywhere: SIGSEGV/SIGTRAP at fedcm_modal_dialog_view.cc:87 (call *0x400(%rax), the ResizeAndFocusPopupWindow() vcall) 23/23.
  • Google Chrome for Testing 152.0.7977.64 (official build), Linux x86-64: 3/3 SIGSEGV in the devirtualised ResizeAndFocusPopupWindow() after the popup_window_ write into the freed slot.
  • Google Chrome Stable 152.0.7977.75 (official build), macOS 26, Apple Silicon: same site reached as the final browser-process stage of the chain in ../CHAIN.md.
  • unit_tests regression test in regression-test.patch (FedCmModalDialogViewTest.DestroyedDuringOpenURLFromTab): compiles against d81900a3939b6; we could not run unit_tests on our build host (target not built), so please treat it as reviewed-but-unexecuted. By construction it is an 8-byte ASan write UAF before the fix and fails deterministically without ASan on EXPECT_EQ(gfx::Rect(), delegate.bounds()).

ShowPopupWindow() has used this after OpenURLFromTab() since the file was added (2023); the code is shared by Windows, macOS, Linux and ChromeOS (chrome/browser/ui/views/webid/). Android has a different FedCM UI and is not affected by this file.

Component

Blink>Identity>FedCM (browser UI: chrome/browser/ui/views/webid), UI>Browser>WebID.

Vulnerability details

File/line references are to Chromium d81900a3939b6.

chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc:48-92:

content::WebContents* FedCmModalDialogView::ShowPopupWindow(
    const GURL& url, bool user_close_cancels_flow) {
  ...
  content::OpenURLParams params(url, content::Referrer(), disposition,
                                ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
                                /*is_renderer_initiated=*/false);
  popup_window_ = source_window_->GetDelegate()->OpenURLFromTab(      // :77  <- may destroy |this|;
      source_window_, params, /*navigation_handle_callback=*/{});    //        then 8-byte WRITE to this+0x28
  if (!popup_window_) {
    return nullptr;
  }
  if (!is_fullscreen) {
    ResizeAndFocusPopupWindow();                                     // :87  virtual call through freed |this|
  }
  Observe(popup_window_);                                            // :89  dangling observer registration
  return popup_window_;
}

ResizeAndFocusPopupWindow() (:123-134) reads source_window_, custom_y_position_ and popup_window_ from the freed object and calls SetContentsBounds / ActivateContents on whatever popup_window_ now points to.

The same pattern one frame up — chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc:836-845:

void FedCmAccountSelectionView::OnMoreDetails(const ui::Event& event) {
  ...
  delegate_->OnMoreDetails();                              // opens the same pop-up window
  delegate_->OnDismiss(DismissReason::kMoreDetailsButton); // raw_ptr member of a possibly-freed view
}

How this dies inside OpenURLFromTab() (the Report #4 trampoline; full stack in logs/asan_chrome_uaf_deepstack_excerpt.txt): BrowserWebContentsDelegate::OpenURLFromTab → Navigate() → ~ScopedBrowserShower → BrowserView::Show() → BrowserActiveStateManager::DidBecomeActive() → JavaScriptTabModalDialogManagerDelegateDesktop::OnBrowserActivated → TabModalDialogManager::BrowserActiveStateChanged → HandleTabSwitchAway() runs the RP tab’s pending alert() callback → WebContentsImpl::OnDialogClosed() → javascript_dialog_dismiss_notifier_.reset() → ~JavaScriptDialogDismissNotifier runs the resume closure of a JS-dialog-deferred prerender activation, which commits synchronously → RenderFrameHostManager::CommitPending(main) → UnloadOldFrame(M) → PendingDeletionCheckCompleted → RemoveChild(X) → ~RenderFrameHostImpl(X) → ~DocumentAssociatedData → SupportsUserData::ClearAllUserData → webid::RequestService::~RequestService (content/browser/webid/request_service.cc:72-77) → ~IdentityDialogController → ~FedCmAccountSelectionView → ~FedCmModalDialogView = free of the object whose ShowPopupWindow() is executing.

Path from the renderer to ShowPopupWindow() with everything owned by the doomed RequestService on the stack: FederatedAuthRequest.RequestToken(mode = active) from child frame X (requires transient user activation) → well-known/config/accounts fetches to the attacker IdP → IdP answers the accounts request with an empty list (or 401) → AccountsFetcher::HandleAccountsFetchFailure → Request::OnAccountsResultsReceived → MaybeShowActiveModeModalDialog → LoginToIdP → Request::ShowModalDialog (content/browser/webid/request.cc:1537-1545, wrapped in the new ShowDialog() weak check) → IdentityDialogController::ShowModalDialog (tail call) → FedCmAccountSelectionView::ShowModalDialog (:858 creates the FedCmModalDialogView, :900 tail-calls ShowPopupWindow, :1491-1500) → FedCmModalDialogView::ShowPopupWindow. Every frame below ShowPopupWindow either is a tail call, returns on !weak_this, or touches only locals after the call — we verified each — so the stale accesses are exactly fedcm_modal_dialog_view.cc:77-91 (and :123-134 when the vcall lands). The popup blocker does not intervene (is_renderer_initiated=false ⇒ user_gesture=true).

Object: webid::FedCmModalDialogView, 104 bytes (vtable at +0, source_window_ +0x20, popup_window_ +0x28, …, weak_ptr_factory_), unique_ptr-owned, not an ADVANCED_MEMORY_SAFETY_CHECKS type, no live raw_ptr to it at free time, and freed while a primary-main-frame navigation is in progress (the UI-thread scheduler-loop quarantine is paused) — on the shipping allocator configuration the slot is on the regular freelist at the time of the write and the vcall. Release-build disassembly of the use: movq 0x28(%rbx),%rdi (read + BackupRefPtr release of stale popup_window_) → movq %r14,0x28(%rbx) (write) → movq (%rbx),%rax; callq *0x400(%rax) (vcall, slot 128 = ResizeAndFocusPopupWindow) → WebContentsObserver::Observe(this).

Prior art: chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc has no lifetime fix in its history; content/browser/webid/request.cc received 5d16106435e6 “[FedCM] Fix UAF in content::webid::Request” and 63a684bc6a90 “[FedCM] Guard dialog calls against reentrancy UAF” on 2026-09-01, whose description names this trigger (“the browser opens a popup window … loses focus … detaches the initiating iframe”). This report is the still-unguarded chrome/ half of that fix. (On the 152/153 branches, where those two commits are absent, Request::ShowModalDialog’s own post-call write did_show_ui_ = true is one more stale write on the same stack.)

Reproduction

Build

# args.gn (Linux x86-64), chromium @ d81900a3939b6
is_asan = true
is_debug = false
dcheck_always_on = true
is_component_build = false
symbol_level = 1

autoninja -C out/asan chrome unit_tests

(a) Unit test (regression-test.patch)

git apply regression-test.patch
autoninja -C out/asan unit_tests
out/asan/unit_tests --gtest_filter=FedCmModalDialogViewTest.DestroyedDuringOpenURLFromTab

The test’s WebContentsDelegate::OpenURLFromTab() deletes the view before returning, modelling any embedder code that tears down the RP document while the window opens. Expected without the fix: ASan heap-use-after-free WRITE of size 8 in webid::FedCmModalDialogView::ShowPopupWindow; without ASan, failure of EXPECT_EQ(gfx::Rect(), delegate.bounds()). (Not executed on our side — see “Affected versions”.)

(b) Web-reachable trigger via Report #4 (poc/)

cd poc && ./run.sh out_dir /path/to/out/asan/chrome        # ~25 s

drive.py runs one loopback HTTP server that plays both the RP (http://127.0.0.1:PORT) and the IdP (http://localhost:PORT: /.well-known/web-identity, /fedcm.json, /accounts held and then answered {"accounts":[]}, /login), launches chrome --headless=new --disable-field-trial-config --remote-debugging-pipe …/main.html, and uses the DevTools pipe only to deliver one synthetic mouse click to the RP page (FedCM active mode requires transient activation; a compromised renderer would assert it via LocalFrameHost.UpdateUserActivationState), then detaches before anything is armed. Page flow: child X calls navigator.credentials.get({identity:{mode:'active', providers:[{configURL: IDP + '/fedcm.json', …}]}}); 5.6 s later (after the activation has expired, so the navigation is gesture-less) M runs location.href='/next.html'; alert('one') (prerendered ⇒ activation deferred behind the dialog); 1.5 s later the IdP releases the accounts response ⇒ login pop-up ⇒ free ⇒ use.

Expected server.log: X fedcm get() issued → IdP accounts request received; holding → S1: M location.href=/next.html (ua=false) then sync alert() → releasing accounts response → GET localhost /login → chrome exits with the ASan report.

Observed result

logs/asan_chrome_uaf_deepstack_excerpt.txt (ASan chrome, malloc_context_size=150; full: logs/asan_chrome_uaf_deepstack_full.log; the packaged poc/ re-run is logs/asan_chrome_uaf_rerun_*.log with identical use/free stacks truncated at 60 frames):

==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bad52471308 ...
READ of size 8 at 0x7bad52471308 thread T0 (chrome)
    #0 webid::FedCmModalDialogView::ShowPopupWindow(...) base/allocator/.../raw_ptr.h:590:29     (= fedcm_modal_dialog_view.cc:77, popup_window_ =)
    #1 webid::FedCmAccountSelectionView::ShowModalDialog(...) chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.cc:1500:25
    #2 IdentityDialogController::ShowModalDialog(...) chrome/browser/ui/webid/identity_dialog_controller.cc:556:25
    #3/#4 content::webid::Request::ShowModalDialog(...) content/browser/webid/request.cc:1538:47
    #5 content::webid::Request::LoginToIdP(...)
    #6 content::webid::Request::OnAccountsResultsReceived(...) content/browser/webid/request.cc:2517:3
    #9 content::webid::AccountsFetcher::AddResult(...) content/browser/webid/accounts_fetcher.cc:871:26
    #10 content::webid::AccountsFetcher::HandleAccountsFetchFailure(...) accounts_fetcher.cc:770:7
    #11 content::webid::AccountsFetcher::OnAccountsResponseReceived(...) accounts_fetcher.cc:403:5
0x7bad52471308 is located 40 bytes inside of 104-byte region [0x7bad524712e0,0x7bad52471348)
freed by thread T0 (chrome) here:
    #1 webid::FedCmAccountSelectionView::~FedCmAccountSelectionView(...)
    #3 IdentityDialogController::~IdentityDialogController(...) chrome/browser/ui/webid/identity_dialog_controller.cc:102:57
    #5 content::webid::RequestService::~RequestService(...) content/browser/webid/request_service.cc:72:35
    #8 base::SupportsUserData::ClearAllUserData(...)
    #9 content::DocumentAssociatedData::~DocumentAssociatedData(...) content/browser/renderer_host/document_associated_data.cc:80:3
    #11 content::RenderFrameHostImpl::~RenderFrameHostImpl(...) render_frame_host_impl.cc:2835:45
    #13 content::FrameTreeNode::~FrameTreeNode(...) frame_tree_node.cc:320:1
    #15 content::RenderFrameHostImpl::RemoveChild(...)
    #16 content::RenderFrameHostImpl::PendingDeletionCheckCompleted(...) render_frame_host_impl.cc:12835:16
    #20 content::RenderFrameHostManager::UnloadOldFrame(...) render_frame_host_manager.cc:1525:23
    #21 content::RenderFrameHostManager::CommitPending(...) render_frame_host_manager.cc:5660:3
    #27 content::NavigationRequest::CommitPageActivation(...) navigation_request.cc:7556:25
    ...
    #41 content::CachedNavigationURLLoader::Start(...) cached_navigation_url_loader.cc:80:7
    #57 content::CommitDeferringConditionRunner::ResumeProcessing(...) commit_deferring_condition_runner.cc:108:3
    #60 content::WebContentsImpl::OnDialogClosed(...) web_contents_impl.cc:741:27          (~JavaScriptDialogDismissNotifier)
    #66 javascript_dialogs::TabModalDialogManager::HandleTabSwitchAway(...) tab_modal_dialog_manager.cc:508:35
    #72 BrowserActiveStateManager::DidBecomeActive(...) chrome/browser/ui/browser_active_state_manager/browser_active_state_manager.cc:55:38
    #73 BrowserView::Show(...) chrome/browser/ui/views/frame/browser_view.cc:1621:47
    #77 BrowserWebContentsDelegate::OpenURLFromTab(...) chrome/browser/ui/browser_web_contents_delegate/browser_web_contents_delegate.cc:593:7
    #78 webid::FedCmModalDialogView::ShowPopupWindow(...) chrome/browser/ui/views/webid/fedcm_modal_dialog_view.cc:77:50
    #79 webid::FedCmAccountSelectionView::ShowModalDialog(...) fedcm_account_selection_view_desktop.cc:1500:25
previously allocated by thread T0 (chrome) here:
    #1 webid::FedCmAccountSelectionView::CreatePopupWindow(...)
    #2 webid::FedCmAccountSelectionView::ShowModalDialog(...) fedcm_account_selection_view_desktop.cc:858:21

ASan reports the read half of the raw_ptr assignment first; the write to the same field and the virtual call follow in a non-ASan build (release build: Received signal 11 at fedcm_modal_dialog_view.cc:87:5, 23/23; Chrome for Testing 152.0.7977.64: SIGSEGV inside the devirtualised ResizeAndFocusPopupWindow after the write, 3/3).

Impact

Browser-process heap use-after-free with an attacker-timed 8-byte write into a freed 104-byte slot and a virtual call through that slot, reachable from web content given Report #4 (one click on the attacker page, or zero clicks with a compromised renderer). Unlike the RenderFrameHost getters in Report #4’s other holders, this use is not neutralised by devirtualisation on official builds: the write happens unconditionally, and the freed object is small, unquarantined and freed during a window in which page-controlled allocations of the same size class occur, which is what made it the practical browser-process code-execution primitive in the chain on stock Chrome 152 for macOS (../CHAIN.md). We rate it High on its own (needs Report #4 or another synchronous destroyer) and Critical in combination.

View on issue tracker