Medium chrome Memory Corruption 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Extensions
DescriptionMemory corruption in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassMemory Corruption
Tracker514066180
Fix commit4b8e494ea39f (chromium/src) +308/-76
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
ExtensionPopup
chrome/browser/ui/views/extensions/extension_popup.cc
modified
if
chrome/browser/ui/views/extensions/extension_popup.cc
modified
BrowserContext
chrome/browser/ui/views/extensions/extension_popup.h
modified
DevToolsAgentHost
chrome/browser/ui/views/extensions/extension_popup.h
modified
ScopedDevToolsAgentHostObservation
chrome/browser/ui/views/extensions/extension_popup.h
modified
for
chrome/browser/ui/views/extensions/security_dialog_tracker.cc
modified
if
chrome/browser/ui/views/extensions/security_dialog_tracker.cc
modified

Files Changed

  • chrome/browser/ui/views/extensions/BUILD.gn
  • chrome/browser/ui/views/extensions/security_dialog_tracker_browsertest.cc
  • chrome/browser/ui/views/extensions/extension_popup_interactive_uitest.cc
  • chrome/browser/ui/views/extensions/security_dialog_tracker.cc
  • chrome/browser/ui/views/extensions/extension_popup.cc
  • chrome/browser/devtools/devtools_window.cc
  • chrome/browser/ui/views/extensions/security_dialog_tracker.h
  • chrome/browser/ui/views/extensions/extension_popup.h
From 4b8e494ea39fbac80a26968fa0189502a263bec6 Mon Sep 17 00:00:00 2001
From: Eva Su <[email protected]>
Date: Mon, 21 Sep 2026 14:07:25 -0700
Subject: [PATCH] [Extensions] Dismiss extension popup when security dialog is shown

Previously, an extension with the `debugger` permission could call
`chrome.debugger.attach()` to attach a Chrome DevTools Protocol client
to its own popup target. This triggered `DevToolsAgentHostAttached()`,
setting the popup inspection state and causing
`ExtensionPopup::CloseUnlessBlockedByInspectionOrJSDialog()` to return
early. As a result, the extension popup remained pinned open over
security-sensitive dialogs (such as WebAuthn, FedCM, or Payment
Request).

This CL addresses the issue by prioritizing security-sensitive dialogs
over DevTools inspection state. Extension popups are now force-closed
under all circumstances when a security dialog is present.

Specifically, this CL introduces the following changes:
- Updates `SecurityDialogTracker` to implement the Observer pattern. `ExtensionPopup` now observes the tracker and proactively dismisses itself via a deferred task when a new security dialog is added.
- Checks `SecurityDialogTracker::BrowserHasVisibleSecurityDialogs()` before checking DevTools inspection state in the close logic.
- Checks `DevToolsWindow::GetInstanceForInspectedWebContents()` instead of relying on generic CDP attachment events. This ensures that only an active user-facing DevTools window prevents popup auto-dismissal, rather than any programmatic `chrome.debugger` client.
- Updates `DevToolsWindow::GetInstanceForInspectedWebContents()` to ignore windows in the `kClosing` state, preventing race conditions
where closing DevTools triggers auto-close logic before destruction.
- Updates `ExtensionPopup` activation and dialog-tracking logic to safely ignore child or descendant widgets belonging to the popup itself, preventing the popup from erroneously dismissing itself when interacting with its own sub-elements.

Fixed: 514066180
Change-Id: I6b8f76900581929cf92e7382d1b026042306bc3f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8372287
Commit-Queue: Eva Su <[email protected]>
Reviewed-by: Tim <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1702043}
---
diff --git a/chrome/browser/devtools/devtools_window.cc b/chrome/browser/devtools/devtools_window.cc
index 53c1cda3..7613579 100644
--- a/chrome/browser/devtools/devtools_window.cc
+++ b/chrome/browser/devtools/devtools_window.cc
@@ -640,7 +640,8 @@
     return nullptr;
   }
   for (auto& instance : GetDevToolsWindowInstances()) {
-    if (instance->GetInspectedWebContents() == inspected_web_contents) {
+    if (instance->GetInspectedWebContents() == inspected_web_contents &&
+        instance->life_stage_ != kClosing) {
       return instance;
     }
   }
diff --git a/chrome/browser/ui/views/extensions/extension_popup.cc b/chrome/browser/ui/views/extensions/extension_popup.cc
index 6cbc396..7dbab95 100644
--- a/chrome/browser/ui/views/extensions/extension_popup.cc
+++ b/chrome/browser/ui/views/extensions/extension_popup.cc
@@ -7,6 +7,7 @@
 #include "base/check.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "chrome/browser/devtools/devtools_window.h"
 #include "chrome/browser/extensions/extension_view_host.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -15,7 +16,6 @@
 #include "components/javascript_dialogs/app_modal_dialog_queue.h"
 #include "components/web_modal/web_modal_utils.h"
 #include "content/public/browser/browser_context.h"
-#include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/web_contents.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/mojom/dialog_button.mojom.h"
@@ -49,31 +49,6 @@
 // The most recently constructed popup; used for testing purposes.
 ExtensionPopup* g_last_popup_for_testing = nullptr;
 
-// A helper class to scope the observation of DevToolsAgentHosts. We can't just
-// use base::ScopedObservation here because that requires a specific source
-// object, where as DevToolsAgentHostObservers are added to a singleton list.
-// The `observer_` passed into this object will be registered as an observer
-// for this object's lifetime.
-class ExtensionPopup::ScopedDevToolsAgentHostObservation {
- public:
-  explicit ScopedDevToolsAgentHostObservation(
-      content::DevToolsAgentHostObserver* observer)
-      : observer_(observer) {
-    content::DevToolsAgentHost::AddObserver(observer_);
-  }
-
-  ScopedDevToolsAgentHostObservation(
-      const ScopedDevToolsAgentHostObservation&) = delete;
-  ScopedDevToolsAgentHostObservation& operator=(
-      const ScopedDevToolsAgentHostObservation&) = delete;
-
-  ~ScopedDevToolsAgentHostObservation() {
-    content::DevToolsAgentHost::RemoveObserver(observer_);
-  }
-
- private:
-  raw_ptr<content::DevToolsAgentHostObserver> observer_;
-};
 
 // static
 ExtensionPopup* ExtensionPopup::last_popup_for_testing() {
@@ -154,9 +129,9 @@
 
 void ExtensionPopup::OnWidgetDestroying(views::Widget* widget) {
   BubbleDialogDelegateView::OnWidgetDestroying(widget);
-  scoped_devtools_observation_.reset();
   anchor_widget_observation_.Reset();
   extension_registry_observation_.Reset();
+  security_dialog_tracker_observation_.Reset();
   extension_view_ = nullptr;
   host_.reset();
   browser_ = nullptr;
@@ -178,6 +153,23 @@
   // TODO(crbug.com/326681253): don't show the popup if it might cover
   // security-sensitive UIs.
   if (active_widget != GetWidget()) {
+    // Ignore activation of child/descendant widgets belonging to this popup.
+    if (extensions::SecurityDialogTracker::IsWidgetOrDescendantOf(
+            active_widget, GetWidget())) {
+      return;
+    }
+
+    if (extensions::SecurityDialogTracker::GetInstance()
+            ->BrowserHasVisibleSecurityDialogs(browser_, GetWidget())) {
+      // Defer to avoid reentrant focus and window activation changes in
+      // FocusController while the security dialog is in the middle of being
+      // shown.
+      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+          FROM_HERE,
+          base::BindOnce(&ExtensionPopup::CloseIfSecurityDialogPresent,
+                         deferred_close_weak_ptr_factory_.GetWeakPtr()));
+      return;
+    }
     CloseUnlessBlockedByInspectionOrJSDialog();
   }
 }
@@ -219,12 +211,6 @@
     // try to access the host during Widget closure, destroy it immediately.
     RemoveChildViewT(extension_view_.ExtractAsDangling());
 
-    // Note: it's important that we unregister the devtools observation *before*
-    // we destroy `host_`. Otherwise, destroying `host_` can synchronously cause
-    // the associated WebContents to be destroyed, which will cause devtools to
-    // detach, which will notify our observer, where we rely on `host_` - all
-    // synchronously.
-    scoped_devtools_observation_.reset();
     host_.reset();
     // Stop observing the registry immediately to prevent any subsequent
     // notifications, since Widget::Close is asynchronous.
@@ -250,20 +236,18 @@
   }
 }
 
-void ExtensionPopup::DevToolsAgentHostAttached(
-    content::DevToolsAgentHost* agent_host) {
-  DCHECK(host_);
-  if (host_->host_contents() == agent_host->GetWebContents()) {
-    inspected_ = true;
+void ExtensionPopup::OnSecurityDialogAdded(views::Widget* widget) {
+  // Don't close if the security dialog belongs to this popup.
+  if (extensions::SecurityDialogTracker::IsWidgetOrDescendantOf(widget,
+                                                                GetWidget())) {
+    return;
   }
-}
 
-void ExtensionPopup::DevToolsAgentHostDetached(
-    content::DevToolsAgentHost* agent_host) {
-  DCHECK(host_);
-  if (host_->host_contents() == agent_host->GetWebContents()) {
-    inspected_ = false;
-  }
+  // Defer to avoid reentrant focus and window activation changes in
+  // FocusController while the security dialog is in the middle of being shown.
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(&ExtensionPopup::CloseIfSecurityDialogPresent,
+                                deferred_close_weak_ptr_factory_.GetWeakPtr()));
 }
 
 ExtensionPopup::ExtensionPopup(
@@ -301,13 +285,14 @@
   // See comments in OnWidgetActivationChanged().
   set_close_on_deactivate(false);
 
-  scoped_devtools_observation_ =
-      std::make_unique<ScopedDevToolsAgentHostObservation>(this);
   browser_->GetTabStripModel()->AddObserver(this);
 
   CHECK(anchor_widget());
   anchor_widget_observation_.Observe(anchor_widget()->GetPrimaryWindowWidget());
 
+  security_dialog_tracker_observation_.Observe(
+      extensions::SecurityDialogTracker::GetInstance());
+
   // Handle the containing view calling window.close();
   // The base::Unretained() below is safe because this object owns `host_`, so
   // the callback will never fire if `this` is deleted.
@@ -331,7 +316,7 @@
   // Don't show the popup if there are visible security dialogs. This protects
   // the security dialogs from spoofing.
   if (extensions::SecurityDialogTracker::GetInstance()
-          ->BrowserHasVisibleSecurityDialogs(browser_)) {
+          ->BrowserHasVisibleSecurityDialogs(browser_, GetWidget())) {
     CloseDeferredIfNecessary();
     return;
   }
@@ -352,9 +337,19 @@
   }
 }
 
+void ExtensionPopup::CloseIfSecurityDialogPresent() {
+  if (!browser_ || !host_) {
+    return;
+  }
+
+  if (extensions::SecurityDialogTracker::GetInstance()
+          ->BrowserHasVisibleSecurityDialogs(browser_, GetWidget())) {
+    CloseDeferredIfNecessary(views::Widget::ClosedReason::kLostFocus);
+  }
+}
+
 void ExtensionPopup::CloseUnlessBlockedByInspectionOrJSDialog() {
-  // Don't close if the extension page is under inspection.
-  if (inspected_) {
+  if (!browser_ || !host_) {
     return;
   }
 
@@ -372,6 +367,12 @@
     return;
   }
 
+  // Don't close if the extension page is under inspection in a DevTools window.
+  if (DevToolsWindow::GetInstanceForInspectedWebContents(
+          host_->host_contents())) {
+    return;
+  }
+
   CloseDeferredIfNecessary(views::Widget::ClosedReason::kLostFocus);
 }
 
diff --git a/chrome/browser/ui/views/extensions/extension_popup.h b/chrome/browser/ui/views/extensions/extension_popup.h
index cbc211a..ec99063 100644
--- a/chrome/browser/ui/views/extensions/extension_popup.h
+++ b/chrome/browser/ui/views/extensions/extension_popup.h
@@ -12,7 +12,7 @@
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
 #include "chrome/browser/ui/views/extensions/extension_view_views.h"
-#include "content/public/browser/devtools_agent_host_observer.h"
+#include "chrome/browser/ui/views/extensions/security_dialog_tracker.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "extensions/browser/extension_host.h"
 #include "extensions/browser/extension_registry.h"
@@ -26,7 +26,6 @@
 
 namespace content {
 class BrowserContext;
-class DevToolsAgentHost;
 }  // namespace content
 
 namespace extensions {
@@ -42,7 +41,7 @@
                        public extensions::ExtensionRegistryObserver,
                        public content::WebContentsObserver,
                        public TabStripModelObserver,
-                       public content::DevToolsAgentHostObserver {
+                       public extensions::SecurityDialogTracker::Observer {
   METADATA_HEADER(ExtensionPopup, views::BubbleDialogDelegateView)
 
  public:
@@ -102,11 +101,8 @@
       const TabStripModelChange& change,
       const TabStripSelectionChange& selection) override;
 
-  // content::DevToolsAgentHostObserver:
-  void DevToolsAgentHostAttached(
-      content::DevToolsAgentHost* agent_host) override;
-  void DevToolsAgentHostDetached(
-      content::DevToolsAgentHost* agent_host) override;
+  // extensions::SecurityDialogTracker::Observer:
+  void OnSecurityDialogAdded(views::Widget* widget) override;
 
   // Closes the popup immediately, if possible. On Mac, if a nested
   // run loop is running, schedule a deferred close for after the
@@ -118,8 +114,6 @@
   static ExtensionPopup* last_popup_for_testing();
 
  private:
-  class ScopedDevToolsAgentHostObservation;
-
   ExtensionPopup(BrowserWindowInterface* browser,
                  std::unique_ptr<extensions::ExtensionViewHost> host,
                  views::BubbleAnchor anchor,
@@ -135,6 +129,10 @@
   //   2. an open web dialog, e.g. JS alert.
   void CloseUnlessBlockedByInspectionOrJSDialog();
 
+  // Closes the bubble if there is a security dialog present in the browser
+  // window.
+  void CloseIfSecurityDialogPresent();
+
   // Handles a signal from the extension host to close.
   void HandleCloseExtensionHost(extensions::ExtensionHost* host);
 
@@ -149,22 +147,18 @@
                           extensions::ExtensionRegistryObserver>
       extension_registry_observation_{this};
 
+  base::ScopedObservation<extensions::SecurityDialogTracker,
+                          extensions::SecurityDialogTracker::Observer>
+      security_dialog_tracker_observation_{this};
+
   // Action (as requested by the caller) to be done when the popup gets shown.
   const PopupShowAction show_action_;
 
-  // Whether DevTools is currently inspecting the popup contents.
-  bool inspected_ = false;
-
   ShowPopupCallback shown_callback_;
 
   base::ScopedObservation<views::Widget, views::WidgetObserver>
       anchor_widget_observation_{this};
 
-  // Note: This must be reset *before* `host_`. See note in
-  // OnExtensionUnloaded().
-  std::unique_ptr<ScopedDevToolsAgentHostObservation>
-      scoped_devtools_observation_;
-
   base::WeakPtrFactory<ExtensionPopup> deferred_close_weak_ptr_factory_;
 };
 
diff --git a/chrome/browser/ui/views/extensions/security_dialog_tracker.cc b/chrome/browser/ui/views/extensions/security_dialog_tracker.cc
index 438742c1..f10819e3 100644
--- a/chrome/browser/ui/views/extensions/security_dialog_tracker.cc
+++ b/chrome/browser/ui/views/extensions/security_dialog_tracker.cc
@@ -41,6 +41,14 @@
   return &*s_instance;
 }
 
+void SecurityDialogTracker::AddObserver(Observer* observer) {
+  observers_.AddObserver(observer);
+}
+
+void SecurityDialogTracker::RemoveObserver(Observer* observer) {
+  observers_.RemoveObserver(observer);
+}
+
 void SecurityDialogTracker::AddSecurityDialog(views::Widget* widget) {
   views::View* root_view = widget->GetRootView();
   if (root_view->GetProperty(kIdentifierViewKey)) {
@@ -50,6 +58,10 @@
   // Add an invisible identifier view to the root view of the widget.
   root_view->SetProperty(kIdentifierViewKey,
                          root_view->AddChildView(MakeIdentifierView()));
+
+  for (Observer& observer : observers_) {
+    observer.OnSecurityDialogAdded(widget);
+  }
 }
 
 void SecurityDialogTracker::RemoveSecurityDialog(views::Widget* widget) {
@@ -63,15 +75,46 @@
   root_view->ClearProperty(kIdentifierViewKey);
 }
 
+// static
+bool SecurityDialogTracker::IsWidgetOrDescendantOf(
+    const views::Widget* widget,
+    const views::Widget* ancestor) {
+  if (!widget || !ancestor) {
+    return false;
+  }
+  for (const views::Widget* w = widget; w; w = w->parent()) {
+    if (w == ancestor) {
+      return true;
+    }
+  }
+  return false;
+}
+
 bool SecurityDialogTracker::BrowserHasVisibleSecurityDialogs(
-    BrowserWindowInterface* browser) const {
-  const auto views = BrowserElementsViews::From(browser)->GetAllViews(
-      kSecuritySensitiveDialogIdentifier);
+    BrowserWindowInterface* browser,
+    const views::Widget* ignore_widget) const {
+  if (!browser) {
+    return false;
+  }
+  auto* browser_elements = BrowserElementsViews::From(browser);
+  if (!browser_elements) {
+    return false;
+  }
+  const auto views =
+      browser_elements->GetAllViews(kSecuritySensitiveDialogIdentifier);
   return std::any_of(views.begin(), views.end(),
-                     [](views::View* identifier_view) {
+                     [ignore_widget](views::View* identifier_view) {
                        views::Widget* dialog_widget =
                            identifier_view->GetWidget();
-                       return dialog_widget && dialog_widget->IsVisible();
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/extensions/extension_popup_interactive_uitest.cc b/chrome/browser/ui/views/extensions/extension_popup_interactive_uitest.cc
index 1b56f9b3..2f0ad286 100644
--- a/chrome/browser/ui/views/extensions/extension_popup_interactive_uitest.cc
+++ b/chrome/browser/ui/views/extensions/extension_popup_interactive_uitest.cc
@@ -79,6 +79,12 @@
   raw_ptr<content::WebContents> web_contents_;
 };
 
+class CloseableTestDevToolsProtocolClient
+    : public content::TestDevToolsProtocolClient {
+ public:
+  CloseableTestDevToolsProtocolClient() { set_agent_host_can_close(); }
+};
+
 views::UniqueWidgetPtr CreateTestTopLevelWidget() {
   views::UniqueWidgetPtr widget = std::make_unique<views::Widget>();
   views::Widget::InitParams params(
@@ -773,6 +779,158 @@
   popup_waiter.WaitForHostDestroyed();
 }
 
+// Tests that an extension popup under active DevTools inspection is dismissed
+// when a security dialog is shown.
+IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
+                       InspectedPopupClosesWhenSecurityDialogShown) {
+  static constexpr char kManifest[] =
+      R"({
+           "name": "Test Extension",
+           "manifest_version": 3,
+           "action": { "default_popup": "popup.html" },
+           "version": "0.1"
+         })";
+
+  extensions::TestExtensionDir test_dir;
+  test_dir.WriteManifest(kManifest);
+  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
+                     "<html>Hello, world!</html>");
+  const extensions::Extension* extension =
+      LoadExtension(test_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Open the popup and inspect it.
+  extensions::ExtensionHostTestHelper popup_waiter(profile(), extension->id());
+  popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
+  ExtensionActionTestHelper::Create(browser())->InspectPopup(extension->id());
+  popup_waiter.WaitForHostCompletedFirstLoad();
+  content::WebContents* extension_contents =
+      ExtensionPopup::last_popup_for_testing()->host()->host_contents();
+  ASSERT_NE(extension_contents, nullptr);
+  DevToolsAttachWaiter(extension_contents).Wait();
+
+  base::WeakPtr<views::Widget> extension_popup_widget =
+      ExtensionPopup::last_popup_for_testing()->GetWidget()->GetWeakPtr();
+  ASSERT_TRUE(extension_popup_widget);
+  views::test::WidgetVisibleWaiter(extension_popup_widget.get()).Wait();
+  EXPECT_TRUE(extension_popup_widget->IsVisible());
+
+  // Show a security dialog.
+  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
+  views::UniqueWidgetPtr security_widget =
+      CreateTestDialogWidget(browser_view->GetWidget());
+  extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
+      security_widget.get());
+  security_widget->Show();
+  views::test::WidgetVisibleWaiter(security_widget.get()).Wait();
+
+  // The popup should be dismissed even though it was under inspection.
+  ExpectWidgetDestroy(extension_popup_widget);
+}
+
+// Tests that an extension popup attached to a DevTools protocol client
+// (emulating chrome.debugger.attach()) is dismissed when a security dialog is
+// shown.
+IN_PROC_BROWSER_TEST_F(
+    ExtensionPopupInteractiveUiTest,
+    DevToolsProtocolClientAttachedPopupClosesWhenSecurityDialogShown) {
+  static constexpr char kManifest[] =
+      R"({
+           "name": "Test Extension",
+           "manifest_version": 3,
+           "action": { "default_popup": "popup.html" },
+           "version": "0.1"
+         })";
+
+  extensions::TestExtensionDir test_dir;
+  test_dir.WriteManifest(kManifest);
+  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
+                     "<html>Hello, world!</html>");
+  const extensions::Extension* extension =
+      LoadExtension(test_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Open the popup.
+  base::WeakPtr<views::Widget> extension_popup_widget =
+      OpenExtensionPopup(browser(), extension);
+  ASSERT_TRUE(extension_popup_widget);
+  EXPECT_TRUE(extension_popup_widget->IsVisible());
+
+  content::WebContents* extension_contents =
+      ExtensionPopup::last_popup_for_testing()->host()->host_contents();
+  ASSERT_NE(extension_contents, nullptr);
+
+  // Attach a DevTools protocol client (emulating chrome.debugger.attach()).
+  CloseableTestDevToolsProtocolClient devtools_client;
+  devtools_client.AttachToWebContents(extension_contents);
+
+  // Show a security dialog.
+  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
+  views::UniqueWidgetPtr security_widget =
+      CreateTestDialogWidget(browser_view->GetWidget());
+  extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
+      security_widget.get());
+  security_widget->Show();
+  views::test::WidgetVisibleWaiter(security_widget.get()).Wait();
+
+  // The popup should be dismissed.
+  ExpectWidgetDestroy(extension_popup_widget);
+}
+
+// Tests that an extension popup in one browser window does not close when a
+// security dialog is shown in a different browser window.
+IN_PROC_BROWSER_TEST_F(
+    ExtensionPopupInteractiveUiTest,
+    PopupDoesNotCloseWhenSecurityDialogShownInDifferentBrowser) {
+  static constexpr char kManifest[] =
+      R"({
+           "name": "Test Extension",
+           "manifest_version": 3,
+           "action": { "default_popup": "popup.html" },
+           "version": "0.1"
+         })";
+
+  extensions::TestExtensionDir test_dir;
+  test_dir.WriteManifest(kManifest);
+  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
+                     "<html>Hello, world!</html>");
+  const extensions::Extension* extension =
+      LoadExtension(test_dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Open the popup in the first browser.
+  base::WeakPtr<views::Widget> extension_popup_widget =
+      OpenExtensionPopup(browser(), extension);
+  ASSERT_TRUE(extension_popup_widget);
+  EXPECT_TRUE(extension_popup_widget->IsVisible());
+
+  // Create a second browser window and show a security dialog in it.
+  BrowserWindowInterface* second_browser = CreateBrowser(profile());
+  BrowserView* second_browser_view =
+      BrowserView::GetBrowserViewForBrowser(second_browser);
+  views::UniqueWidgetPtr security_widget =
+      CreateTestDialogWidget(second_browser_view->GetWidget());
+  extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
+      security_widget.get());
+  security_widget->Show();
+  views::test::WidgetVisibleWaiter(security_widget.get()).Wait();
+
+  // Flush the deferred CloseIfSecurityDialogPresent task.
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, run_loop.QuitClosure());
+  run_loop.Run();
+
+  // The popup in the first browser should remain open.
+  EXPECT_TRUE(extension_popup_widget);
+  EXPECT_TRUE(extension_popup_widget->IsVisible());
+
+  // Clean up.
+  security_widget->Close();
+  views::test::WidgetDestroyedWaiter(security_widget.get()).Wait();
+  CloseBrowserSynchronously(second_browser);
+}
+
 // Tests that pressing Escape in an extension popup does not close the popup
 // if the keydown event is default-prevented by JavaScript.
 IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
diff --git a/chrome/browser/ui/views/extensions/security_dialog_tracker_browsertest.cc b/chrome/browser/ui/views/extensions/security_dialog_tracker_browsertest.cc
index 5618d0a..327060d 100644
--- a/chrome/browser/ui/views/extensions/security_dialog_tracker_browsertest.cc
+++ b/chrome/browser/ui/views/extensions/security_dialog_tracker_browsertest.cc
@@ -47,6 +47,20 @@
   views::test::WidgetVisibleWaiter(security_widget.get()).Wait();
   // Security dialog is now visible.
   EXPECT_TRUE(tracker->BrowserHasVisibleSecurityDialogs(browser()));
+  EXPECT_FALSE(tracker->BrowserHasVisibleSecurityDialogs(
+      browser(), security_widget.get()));
+  EXPECT_FALSE(tracker->BrowserHasVisibleSecurityDialogs(nullptr));
+
+  EXPECT_TRUE(SecurityDialogTracker::IsWidgetOrDescendantOf(
+      security_widget.get(), security_widget.get()));
+  EXPECT_TRUE(SecurityDialogTracker::IsWidgetOrDescendantOf(
+      security_widget.get(), browser_view->GetWidget()));
+  EXPECT_FALSE(SecurityDialogTracker::IsWidgetOrDescendantOf(
+      browser_view->GetWidget(), security_widget.get()));
+  EXPECT_FALSE(SecurityDialogTracker::IsWidgetOrDescendantOf(
+      nullptr, security_widget.get()));
+  EXPECT_FALSE(SecurityDialogTracker::IsWidgetOrDescendantOf(
+      security_widget.get(), nullptr));
 
   BrowserWindowInterface* new_browser = CreateBrowser(browser()->GetProfile());
   // No security dialogs under a different browser.
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.