CVE-2026-95356
Overview
Background
- `AppModalDialogController`
- Object that drives a single JavaScript app-modal dialog (
alert,confirm,beforeunload), owning itsview_and aDialogClosedCallback. - `AppModalDialogQueue`
- Singleton FIFO of pending
AppModalDialogControllerinstances that shows one dialog at a time and callsShowNextDialog()/GetNextDialog()to advance. - Re-entrancy
- Here, a
DialogClosedCallbacksynchronously resuming deferred work (such as prerender activation) that re-entersCancelDialogs()while an earlierInvalidate()is still on the stack. - `base::WeakPtr`
- A pointer that automatically becomes null when its referent is destroyed, letting a stack frame detect that
thiswas freed mid-call.
Root Cause Analysis
AppModalDialogController::NotifyDelegate() and Invalidate() called CallDialogClosedCallback() (and CompleteDialog()) and then continued to touch member state such as view_ and valid_. That callback could synchronously resume a deferred prerender activation, which re-entered AppModalDialogManager::CancelDialogs(); closing the active dialog triggered ShowNextDialog() -> GetNextDialog(), which drained and destroyed every invalidated queued controller — including the very this whose Invalidate() was still executing — leaving the outer frame with a dangling object. The invariant violated is that a method must not use this after invoking a callback that can free this.
The fix installs a WeakPtrFactory on AppModalDialogController, captures a weak_this before each re-entrant call (CallDialogClosedCallback(), CompleteDialog(), ShowModalDialog()) and returns early when it is null, sets valid_ = false at the top of NotifyDelegate() to close the re-entrancy window, invalidates weak pointers first in the destructor, and in CancelDialogs() snapshots WeakPtrs into a local vector before invalidating so circular_deque iterators are not invalidated mid-iteration.
DialogClosedCallback that can synchronously destroy the controller through the dialog queue; the fix detects that destruction with a base::WeakPtr captured before the callback and bails out instead of dereferencing freed memory.Attack Path
- Queue a dialog then defer activation
A page enqueues an app-modal dialog (e.g. a
beforeunloaddialog) inAppModalDialogQueuewhile a prerender or navigation is deferred pending that dialog. - Trigger cancellation
Navigation or prerender activation drives
AppModalDialogManager::CancelDialogs(), which iterates the queue and callsInvalidate()/NotifyDelegate()on a queued controller. - Re-enter through the closed callback
CallDialogClosedCallback()synchronously resumes the deferred activation, which re-entersCancelDialogs()and closes the active dialog, reachingShowNextDialog()->GetNextDialog(). - Free the in-flight controller
GetNextDialog()drains and destroys all invalidated queued controllers, including the one whoseInvalidate()is still on the stack, freeing its backing memory. - Use the freed object
The outer
Invalidate()/CancelDialogs()frame then readsview_or the freed controller via a stale queue iterator, yielding a use-after-free that an attacker could shape for further corruption(inferred).
Impact Assessment
AppModalDialogController still referenced on the stack, producing a use-after-free in the browser process where the JavaScript dialog machinery runs. Preconditions are a queued app-modal dialog (such as beforeunload) plus a deferred operation (such as prerender activation) whose synchronous resumption re-enters CancelDialogs(). The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory does not state exploited-in-the-wild status or an assigned severity.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/javascript_dialogs/app_modal_dialog_controller.cc |
modified | |
forcomponents/javascript_dialogs/app_modal_dialog_manager.cc |
modified | |
ifcomponents/javascript_dialogs/app_modal_dialog_manager.cc |
modified | |
forcomponents/javascript_dialogs/app_modal_dialog_queue.cc |
modified | |
ifcomponents/javascript_dialogs/app_modal_dialog_queue_unittest.cc |
modified | |
TestDialogControllerBasecomponents/javascript_dialogs/app_modal_dialog_queue_unittest.cc |
modified | |
was_destroyed_components/javascript_dialogs/app_modal_dialog_queue_unittest.cc |
modified | |
MakeReentrantCancelCallbackcomponents/javascript_dialogs/app_modal_dialog_queue_unittest.cc |
modified | |
TEST_Fcomponents/javascript_dialogs/app_modal_dialog_queue_unittest.cc |
modified |
Files Changed
components/javascript_dialogs/app_modal_dialog_controller.hcomponents/javascript_dialogs/app_modal_dialog_manager.cccomponents/javascript_dialogs/app_modal_dialog_controller.cccomponents/javascript_dialogs/app_modal_dialog_queue_unittest.cccomponents/javascript_dialogs/app_modal_dialog_queue.cc
Audit Directions
- Callback re-entrancy after member accessAudit any method that invokes a client-supplied or deferred callback and then reads
thisstate; require abase::WeakPtrguard or early return after the call. - Container mutation during iterationFlag loops over
circular_deque/singleton queues that call code capable of mutating the same container; snapshot into a local copy before iterating. - Destructor-side effectsReview destructors that run further logic (like
CompleteDialog()showing the next dialog) and ensure weak pointers are invalidated before such side effects run.
Patch
From f96629e877623801b3dcb24453aee1ce15032465 Mon Sep 17 00:00:00 2001
From: Dan Murphy <dmurph@chromium.org>
Date: Wed, 16 Sep 2026 10:55:23 -0700
Subject: [PATCH] [JavaScript Dialogs] Fix Invalidate() potential UAF
When AppModalDialogManager::CancelDialogs() invalidates a queued dialog
(such as a beforeunload dialog), its DialogClosedCallback can
synchronously resume deferred operations (e.g. prerender activation) and
re-entrantly invoke CancelDialogs().
Closing the active dialog triggers ShowNextDialog() -> GetNextDialog(),
which drains and destroys all invalidated queued controllers from
AppModalDialogQueue, including the controller whose Invalidate() is
currently executing on the call stack. Accessing view_ after
CallDialogClosedCallback() then results in a potential use-after-free.
In addition, iterating AppModalDialogQueue directly in CancelDialogs()
causes circular_deque iterator invalidation when re-entrancy mutates the
queue.
Similarly, AppModalDialogQueue::InvalidateAndClearQueuedDialogs() popped
dialogs one by one, allowing re-entrant ShowNextDialog() calls to drain
and destroy queued controllers while the loop was still walking them. In
NotifyDelegate(), valid_ was set to false at the bottom of the method,
leaving a window where synchronous callbacks could re-enter
NotifyDelegate() or Invalidate().
Fix this by:
1. Adding WeakPtrFactory to AppModalDialogController, checking weak_this
after CallDialogClosedCallback(), CompleteDialog(), and
ShowModalDialog() before accessing member variables, and invalidating
weak pointers at the start of ~AppModalDialogController().
2. Marking valid_ = false immediately at the top of NotifyDelegate() to
close the re-entrancy window.
3. Snapshotting WeakPtrs into a local vector in CancelDialogs() before
calling Invalidate(), and re-fetching the active dialog afterwards.
4. Moving queued dialogs into a local container before invalidating in
AppModalDialogQueue::InvalidateAndClearQueuedDialogs(), with an outer
while loop to catch dialogs enqueued during invalidation.
5. Adding regression tests in app_modal_dialog_queue_unittest.cc
covering both CancelDialogs() and CancelAllDialogs() re-entrancy.
TAG=agy
CONV=b95c3950-7b1c-4e87-bf3c-e8b6423aeef8
Fixed: 560439699
Test: components_unittests --gtest_filter="AppModalDialogQueueTest.*"
Change-Id: Ic910991545fbb878b60fb3a0b5d8f48d1f98cfd0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8414918
Commit-Queue: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699065}
---
diff --git a/components/javascript_dialogs/app_modal_dialog_controller.cc b/components/javascript_dialogs/app_modal_dialog_controller.cc
index 805778f..70b64deb 100644
--- a/components/javascript_dialogs/app_modal_dialog_controller.cc
+++ b/components/javascript_dialogs/app_modal_dialog_controller.cc
@@ -88,16 +88,25 @@
callback_(std::move(callback)) {}
AppModalDialogController::~AppModalDialogController() {
+ // `CompleteDialog()` below can synchronously run more dialog code (it shows
+ // the next dialog in the queue). Invalidate weak pointers first so that any
+ // such code, as well as any stack frame holding a weak pointer to `this`,
+ // sees this dialog as already gone instead of as a half-destroyed object.
+ weak_ptr_factory_.InvalidateWeakPtrs();
CompleteDialog();
}
void AppModalDialogController::ShowModalDialog(
std::unique_ptr<AppModalDialogController> controller) {
CHECK_EQ(this, controller.get());
+ // Showing the dialog can synchronously close it again, which destroys the
+ // view and with it `this`.
+ base::WeakPtr<AppModalDialogController> weak_this =
+ weak_ptr_factory_.GetWeakPtr();
view_ = AppModalDialogManager::GetInstance()->view_factory()->Run(
std::move(controller));
view_->ShowAppModalDialog();
- if (app_modal_dialog_observer) {
+ if (weak_this && app_modal_dialog_observer) {
app_modal_dialog_observer->Notify(this);
}
}
@@ -133,7 +142,20 @@
}
valid_ = false;
+
+ // The closed callback can synchronously re-enter the dialog machinery: for
+ // example, closing a beforeunload dialog can resume a deferred prerender
+ // activation, which cancels dialogs again. That eventually reaches
+ // `AppModalDialogQueue::GetNextDialog()`, which destroys every invalidated
+ // queued dialog - including `this`, whose `Invalidate()` is still on the
+ // stack. See https://crbug.com/560439699.
+ base::WeakPtr<AppModalDialogController> weak_this =
+ weak_ptr_factory_.GetWeakPtr();
CallDialogClosedCallback(false, std::u16string());
+ if (!weak_this) {
+ return;
+ }
+
if (view_) {
CloseModalDialog();
}
@@ -146,14 +168,28 @@
// that were still open in the ModalDialogQueue, which would send activation
// back to this one. The framework should be improved to handle this, so this
// is a temporary workaround.
+ //
+ // `CompleteDialog()` shows the next dialog in the queue, which can run
+ // arbitrary code that destroys `this`.
+ base::WeakPtr<AppModalDialogController> weak_this =
+ weak_ptr_factory_.GetWeakPtr();
CompleteDialog();
+ if (!weak_this) {
+ return;
+ }
NotifyDelegate(/*success=*/false, std::u16string(), suppress_js_messages);
}
void AppModalDialogController::OnAccept(const std::u16string& prompt_text,
bool suppress_js_messages) {
+ // See the comment in `OnCancel()`: `CompleteDialog()` can destroy `this`.
+ base::WeakPtr<AppModalDialogController> weak_this =
+ weak_ptr_factory_.GetWeakPtr();
CompleteDialog();
+ if (!weak_this) {
+ return;
+ }
NotifyDelegate(/*success=*/true, override_prompt_text_.value_or(prompt_text),
suppress_js_messages);
@@ -179,7 +215,20 @@
return;
}
+ // Mark invalid immediately so that any re-entrant calls (such as OnClose()
+ // from Views during window destruction, or Invalidate() from navigation)
+ // return early. On Views, we can also end up coming through this code path a
+ // second time asynchronously (see https://crbug.com/40085084).
+ valid_ = false;
+
+ // The close callback can destroy `this`, e.g. by closing the dialog and
+ // letting the dialog queue delete it. See https://crbug.com/560439699.
+ base::WeakPtr<AppModalDialogController> weak_this =
+ weak_ptr_factory_.GetWeakPtr();
CallDialogClosedCallback(success, user_input);
+ if (!weak_this) {
+ return;
+ }
// The close callback above may delete web_contents_, thus removing the extra
// data from the map owned by ::AppModalDialogManager. Make sure
@@ -191,10 +240,6 @@
extra_data->second.suppress_javascript_messages_ = suppress_js_messages;
}
}
-
- // On Views, we can end up coming through this code path twice :(.
- // See https://crbug.com/40085084.
- valid_ = false;
}
void AppModalDialogController::CallDialogClosedCallback(
diff --git a/components/javascript_dialogs/app_modal_dialog_controller.h b/components/javascript_dialogs/app_modal_dialog_controller.h
index e907a2bb..b83c325 100644
--- a/components/javascript_dialogs/app_modal_dialog_controller.h
+++ b/components/javascript_dialogs/app_modal_dialog_controller.h
@@ -11,6 +11,7 @@
#include "base/compiler_specific.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "content/public/browser/javascript_dialog_manager.h"
#include "content/public/browser/web_contents_observer.h"
@@ -72,6 +73,11 @@
// Invalidates the dialog, therefore causing it to not be shown when its turn
// to be shown comes around.
+ //
+ // WARNING: This runs the dialog's closed callback, which can synchronously
+ // re-enter the dialog queue and destroy `this` (see the implementation).
+ // Callers must not touch this object after calling this method unless they
+ // hold a weak pointer to it, see `GetWeakPtr()`.
void Invalidate();
// Callbacks from NativeDialog when the user accepts or cancels the dialog.
@@ -98,6 +104,12 @@
bool is_before_unload_dialog() const { return is_before_unload_dialog_; }
bool is_reload() const { return is_reload_; }
+ // Weak pointers are invalidated at the start of the destructor, before
+ // `CompleteDialog()` runs.
+ base::WeakPtr<AppModalDialogController> GetWeakPtr() {
+ return weak_ptr_factory_.GetWeakPtr();
+ }
+
// content::WebContentsObserver overrides:
void WebContentsDestroyed() final;
@@ -143,6 +155,8 @@
// Used only for testing. Specifies alternative prompt text that should be
// used when notifying the delegate.
std::optional<std::u16string> override_prompt_text_;
+
+ base::WeakPtrFactory<AppModalDialogController> weak_ptr_factory_{this};
};
// An interface to observe that a modal dialog is shown.
diff --git a/components/javascript_dialogs/app_modal_dialog_manager.cc b/components/javascript_dialogs/app_modal_dialog_manager.cc
index 65763a2..6a809675 100644
--- a/components/javascript_dialogs/app_modal_dialog_manager.cc
+++ b/components/javascript_dialogs/app_modal_dialog_manager.cc
@@ -7,8 +7,10 @@
#include <algorithm>
#include <memory>
#include <utility>
+#include <vector>
#include "base/functional/bind.h"
+#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
@@ -233,16 +235,40 @@
void AppModalDialogManager::CancelDialogs(content::WebContents* web_contents,
bool reset_state) {
AppModalDialogQueue* queue = AppModalDialogQueue::GetInstance();
+
+ // `AppModalDialogController::Invalidate()` runs the dialog's closed callback,
+ // which can synchronously re-enter this method and the dialog queue. That
+ // re-entrancy can both mutate `queue` (invalidating the iterators used here)
+ // and destroy the queued dialogs, since
+ // `AppModalDialogQueue::GetNextDialog()` deletes invalidated dialogs as it
+ // drains the queue. So collect weak pointers to the dialogs to cancel up
+ // front, and re-check each one before using it.
+ // See https://crbug.com/560439699.
+ std::vector<base::WeakPtr<AppModalDialogController>> dialogs_to_cancel;
for (auto& dialog : *queue) {
if (dialog->web_contents() == web_contents) {
+ dialogs_to_cancel.push_back(dialog->GetWeakPtr());
+ }
+ }
+ for (base::WeakPtr<AppModalDialogController>& dialog : dialogs_to_cancel) {
+ // `dialog` is null if it was already destroyed by the re-entrant cancelling
+ // triggered by an earlier dialog in this loop.
+ if (dialog) {
dialog->Invalidate();
}
}
+
+ // The active dialog is deliberately looked up after the loop above: closing
+ // the queued dialogs can have shown a different dialog in the meantime.
+ // `Invalidate()` may destroy `active_dialog` (it closes its view, which owns
+ // it), so nothing may be done with the pointer afterwards.
AppModalDialogController* active_dialog = queue->active_dialog();
if (active_dialog && active_dialog->web_contents() == web_contents) {
active_dialog->Invalidate();
}
+ // Note: no weak pointer is needed for `this`: `AppModalDialogManager` is a
+ // `base::NoDestructor` singleton, so it outlives any re-entrant call above.
if (reset_state) {
javascript_dialog_extra_data_.erase(web_contents);
}
diff --git a/components/javascript_dialogs/app_modal_dialog_queue.cc b/components/javascript_dialogs/app_modal_dialog_queue.cc
index 96b6cc8..23e8171 100644
--- a/components/javascript_dialogs/app_modal_dialog_queue.cc
+++ b/components/javascript_dialogs/app_modal_dialog_queue.cc
@@ -4,6 +4,9 @@
#include "components/javascript_dialogs/app_modal_dialog_queue.h"
+#include <memory>
+#include <utility>
+
#include "base/no_destructor.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
@@ -27,9 +30,25 @@
}
void AppModalDialogQueue::InvalidateAndClearQueuedDialogs() {
+ // `AppModalDialogController::Invalidate()` runs the dialog's closed callback,
+ // which can synchronously re-enter this queue: it can come back into this
+ // method (e.g. via `ShowNextDialog()` during shutdown), and it can enqueue
+ // new dialogs. Take ownership of the queued dialogs before invalidating any
+ // of them, so that a re-entrant call sees an empty queue and cannot pop and
+ // destroy a dialog whose `Invalidate()` is still on the stack.
+ // See https://crbug.com/560439699.
+ //
+ // The outer loop handles dialogs that were enqueued by the re-entrant calls
+ // made while invalidating; those dialogs must be invalidated as well.
while (!app_modal_dialog_queue_.empty()) {
- app_modal_dialog_queue_.front()->Invalidate();
- app_modal_dialog_queue_.pop_front();
+ DialogQueue dialogs = std::move(app_modal_dialog_queue_);
+ // The state of a moved-from container is unspecified, so explicitly empty
+ // the member queue for any re-entrant caller.
+ app_modal_dialog_queue_.clear();
+ for (std::unique_ptr<AppModalDialogController>& dialog : dialogs) {
+ dialog->Invalidate();
+ }
+ // `dialogs` goes out of scope here, destroying the invalidated dialogs.
}
}
diff --git a/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc b/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
index 31b1e70..1efde0e 100644
--- a/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
+++ b/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
@@ -5,9 +5,12 @@
#include "components/javascript_dialogs/app_modal_dialog_queue.h"
#include <memory>
+#include <utility>
+#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
+#include "components/javascript_dialogs/app_modal_dialog_manager.h"
#include "components/javascript_dialogs/app_modal_dialog_view.h"
#include "content/public/browser/javascript_dialog_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -24,7 +27,14 @@
void ShowAppModalDialog() override {}
void ActivateAppModalDialog() override {}
- void CloseAppModalDialog() override {}
+ // Real views notify their controller that the dialog was dismissed when they
+ // are closed. Note that, unlike the real views, this one keeps `controller_`
+ // alive so that tests control its lifetime.
+ void CloseAppModalDialog() override {
+ if (controller_) {
+ controller_->OnCancel(/*suppress_js_messages=*/false);
+ }
+ }
void AcceptAppModalDialog() override {}
void CancelAppModalDialog() override {}
bool IsShowing() const override { return true; }
@@ -35,7 +45,10 @@
class TestDialogControllerBase : public AppModalDialogController {
public:
- explicit TestDialogControllerBase(bool* was_destroyed)
+ explicit TestDialogControllerBase(
+ bool* was_destroyed,
+ content::JavaScriptDialogManager::DialogClosedCallback callback =
+ base::DoNothing())
: AppModalDialogController(nullptr,
&extra_data_,
u"test",
@@ -45,7 +58,7 @@
false,
false,
false,
- base::DoNothing()),
+ std::move(callback)),
was_destroyed_(was_destroyed) {}
~TestDialogControllerBase() override {
@@ -179,5 +192,92 @@
dialog1_ptr->Invalidate();
}
+// Returns a DialogClosedCallback that re-entrantly cancels all dialogs for the
+// test WebContents (nullptr), and records that it ran.
+content::JavaScriptDialogManager::DialogClosedCallback
+MakeReentrantCancelCallback(bool* callback_ran) {
+ return base::BindOnce(
+ [](bool* callback_ran, bool /*success*/,
+ const std::u16string& /*user_input*/) {
+ *callback_ran = true;
+ // This invalidates the active dialog, whose view then dispatches
+ // OnCancel -> CompleteDialog -> ShowNextDialog -> GetNextDialog, which
+ // drains and destroys the invalidated dialogs still in the queue - one
+ // of which is the dialog whose Invalidate() is running this callback.
+ AppModalDialogManager::GetInstance()->CancelDialogs(
+ /*web_contents=*/nullptr, /*reset_state=*/false);
+ },
+ callback_ran);
+}
+
+// Regression test for https://crbug.com/560439699: a queued dialog's closed
+// callback re-entrantly cancels dialogs, which destroys the queued dialogs
+// while AppModalDialogController::Invalidate() and the CancelDialogs() loop
+// iterating the queue are still on the stack.
+TEST_F(AppModalDialogQueueTest, ReentrantCancelDialogsDuringInvalidate) {
+ auto* queue = AppModalDialogQueue::GetInstance();
+ auto* manager = AppModalDialogManager::GetInstance();
+
+ bool dialog1_shown = false;
+ bool dialog1_destroyed = false;
+ bool dialog2_destroyed = false;
+ bool dialog3_destroyed = false;
+ bool callback_ran = false;
+ std::unique_ptr<FakeView> dialog1_view;
... (file diff truncated)
Regression Test / PoC
diff --git a/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc b/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
index 31b1e70..1efde0e 100644
--- a/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
+++ b/components/javascript_dialogs/app_modal_dialog_queue_unittest.cc
@@ -5,9 +5,12 @@
#include "components/javascript_dialogs/app_modal_dialog_queue.h"
#include <memory>
+#include <utility>
+#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
+#include "components/javascript_dialogs/app_modal_dialog_manager.h"
#include "components/javascript_dialogs/app_modal_dialog_view.h"
#include "content/public/browser/javascript_dialog_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -24,7 +27,14 @@
void ShowAppModalDialog() override {}
void ActivateAppModalDialog() override {}
- void CloseAppModalDialog() override {}
+ // Real views notify their controller that the dialog was dismissed when they
+ // are closed. Note that, unlike the real views, this one keeps `controller_`
+ // alive so that tests control its lifetime.
+ void CloseAppModalDialog() override {
+ if (controller_) {
+ controller_->OnCancel(/*suppress_js_messages=*/false);
+ }
+ }
void AcceptAppModalDialog() override {}
void CancelAppModalDialog() override {}
bool IsShowing() const override { return true; }
@@ -35,7 +45,10 @@
class TestDialogControllerBase : public AppModalDialogController {
public:
- explicit TestDialogControllerBase(bool* was_destroyed)
+ explicit TestDialogControllerBase(
+ bool* was_destroyed,
+ content::JavaScriptDialogManager::DialogClosedCallback callback =
+ base::DoNothing())
: AppModalDialogController(nullptr,
&extra_data_,
u"test",
@@ -45,7 +58,7 @@
false,
false,
false,
- base::DoNothing()),
+ std::move(callback)),
was_destroyed_(was_destroyed) {}
~TestDialogControllerBase() override {
@@ -179,5 +192,92 @@
dialog1_ptr->Invalidate();
}
+// Returns a DialogClosedCallback that re-entrantly cancels all dialogs for the
+// test WebContents (nullptr), and records that it ran.
+content::JavaScriptDialogManager::DialogClosedCallback
+MakeReentrantCancelCallback(bool* callback_ran) {
+ return base::BindOnce(
+ [](bool* callback_ran, bool /*success*/,
+ const std::u16string& /*user_input*/) {
+ *callback_ran = true;
+ // This invalidates the active dialog, whose view then dispatches
+ // OnCancel -> CompleteDialog -> ShowNextDialog -> GetNextDialog, which
+ // drains and destroys the invalidated dialogs still in the queue - one
+ // of which is the dialog whose Invalidate() is running this callback.
+ AppModalDialogManager::GetInstance()->CancelDialogs(
+ /*web_contents=*/nullptr, /*reset_state=*/false);
+ },
+ callback_ran);
+}
+
+// Regression test for https://crbug.com/560439699: a queued dialog's closed
+// callback re-entrantly cancels dialogs, which destroys the queued dialogs
+// while AppModalDialogController::Invalidate() and the CancelDialogs() loop
+// iterating the queue are still on the stack.
+TEST_F(AppModalDialogQueueTest, ReentrantCancelDialogsDuringInvalidate) {
+ auto* queue = AppModalDialogQueue::GetInstance();
+ auto* manager = AppModalDialogManager::GetInstance();
+
+ bool dialog1_shown = false;
+ bool dialog1_destroyed = false;
+ bool dialog2_destroyed = false;
+ bool dialog3_destroyed = false;
+ bool callback_ran = false;
+ std::unique_ptr<FakeView> dialog1_view;
+
+ queue->AddDialog(std::make_unique<TestDialogController>(
+ &dialog1_shown, &dialog1_destroyed, &dialog1_view));
+ ASSERT_TRUE(dialog1_shown);
+ ASSERT_TRUE(queue->HasActiveDialog());
+
+ queue->AddDialog(std::make_unique<TestDialogControllerBase>(
+ &dialog2_destroyed, MakeReentrantCancelCallback(&callback_ran)));
+ queue->AddDialog(
+ std::make_unique<TestDialogControllerBase>(&dialog3_destroyed));
+
+ manager->CancelDialogs(/*web_contents=*/nullptr, /*reset_state=*/false);
+
+ EXPECT_TRUE(callback_ran);
+ EXPECT_TRUE(dialog2_destroyed);
+ EXPECT_TRUE(dialog3_destroyed);
+ EXPECT_FALSE(queue->HasActiveDialog());
+ // The active dialog is owned by its view, so it outlives the cancellation.
+ EXPECT_FALSE(dialog1_destroyed);
+}
+
+// Regression test for https://crbug.com/560439699: the same re-entrancy, but
+// entered through AppModalDialogQueue::InvalidateAndClearQueuedDialogs(). The
+// re-entrant ShowNextDialog() must not drain (and destroy) the dialogs that the
+// outer invalidation loop is still walking over.
+TEST_F(AppModalDialogQueueTest, ReentrantCancelDuringCancelAllDialogs) {
+ auto* queue = AppModalDialogQueue::GetInstance();
+
+ bool dialog1_shown = false;
+ bool dialog1_destroyed = false;
+ bool dialog2_destroyed = false;
+ bool dialog3_destroyed = false;
+ bool callback_ran = false;
+ std::unique_ptr<FakeView> dialog1_view;
+
+ queue->AddDialog(std::make_unique<TestDialogController>(
+ &dialog1_shown, &dialog1_destroyed, &dialog1_view));
+ ASSERT_TRUE(dialog1_shown);
+ ASSERT_TRUE(queue->HasActiveDialog());
+
+ queue->AddDialog(std::make_unique<TestDialogControllerBase>(
+ &dialog2_destroyed, MakeReentrantCancelCallback(&callback_ran)));
+ queue->AddDialog(
+ std::make_unique<TestDialogControllerBase>(&dialog3_destroyed));
+
+ queue->CancelAllDialogs();
+
+ EXPECT_TRUE(callback_ran);
+ EXPECT_TRUE(dialog2_destroyed);
+ EXPECT_TRUE(dialog3_destroyed);
+ EXPECT_FALSE(queue->HasActiveDialog());
+ // The active dialog is owned by its view, so it outlives the cancellation.
+ EXPECT_FALSE(dialog1_destroyed);
+}
+
} // namespace
} // namespace javascript_dialogs