CVE-2026-91710
Overview
Background
- Web contents reparenting
- moving a
content::WebContents(a tab) out of one browser window and into another, e.g. pulling a tab into an installed web-app window, driven byweb_app::ReparentWebContentsIntoBrowserImpl. - `BrowserWindowInterface::GetWindow()->Show()`
- the call that displays a browser window; it can synchronously run nested logic (widget close handling) that closes and destroys the
Browser/BrowserWindowInterfacebefore it returns. - `NavigationCapturingProcess`
- a
NavigationHandleUserDataattached to a navigation that decides how the navigation is captured/redirected into a web app; it is owned along the navigation/browser path and can be freed when that path is torn down. - `base::WeakPtr` / `base::WeakPtrFactory`
- a pointer that automatically becomes null when its pointee is destroyed, used here to detect that an object was freed across a re-entrant
Show()/reparent call.
Root Cause Analysis
The reparenting and web-app launch paths call target_browser->GetWindow()->Show() (or invoke ReparentWebContentsIntoBrowserImpl, which itself calls Show()) and then keep operating on the target browser and its navigation objects. Show() is re-entrant: displaying the window can synchronously trigger window closure, which destroys the Browser/BrowserWindowInterface and, along the redirect path, the owning NavigationCapturingProcess. Pre-patch code violated the invariant that these pointers stay valid across Show(): helpers returned the raw target_browser pointer to callers that dereferenced it, ReparentWebContentsIntoBrowserImpl ran a session-service reset on the freed browser after Show(), and the redirect handler wrote its redirection_result_ member on a possibly-freed this after initiating reparenting — all use-after-free reads/writes.
The fix takes a base::WeakPtr to the browser (and, in the redirect handler, to the NavigationCapturingProcess via a new weak_ptr_factory_) before the destructive call and bails out early (return nullptr / return) when the weak pointer is invalidated. It also records redirection_result_ before calling ReparentWebContentsToTabbedBrowser, so the member store never lands on a freed object, and null-checks the reparented-browser result before continuing the launch.
BrowserWindowInterface::Show() (and the reparent it drives) as non-reentrant, so downstream code went on using pointers to a browser/navigation object that Show() could synchronously free. The fix guards each continuation with a weak pointer (early-exiting when null) and reorders the redirection_result_ write to precede the destructive reparent call.Attack Path
- Trigger a capturing reparent
Cause a navigation (e.g. a link-captured navigation or a web-app launch) that makes Chrome reparent a
WebContentsinto a web-app window viaReparentWebContentsIntoAppBrowser/ReparentWebContentsToTabbedBrowser/ReparentWebContentsIntoBrowserImpl. - Close the target window during `Show()`
Arrange for the target app window to close synchronously while
GetWindow()->Show()is running; the regression tests do exactly this by closing the widget in aviews::AnyWidgetObservershown callback, which destroys theBrowserand its navigation objects mid-call. - Use freed memory on return
Control returns to the reparent/launch helper, which dereferences the now-freed
target_browser(returned as a raw pointer or used for the post-Show()session-service reset) or writesredirection_result_on the freedNavigationCapturingProcess, producing the use-after-free. - Further exploitation (inferred) Grooming the freed browser-process heap object to gain a read/write primitive or code execution is not demonstrated by the patch, which only shows the crash/UAF being avoided.
Impact Assessment
Show() — a narrow race that the tests reproduce deterministically. The advisory classifies this as a use-after-free in WebAppInstalls and does not report in-the-wild exploitation; escalation beyond the UAF to information disclosure or code execution is (inferred) and not established by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/web_applications/navigation_capturing_process.cc |
modified | |
ifchrome/browser/ui/web_applications/web_app_launch_process.cc |
modified | |
ifchrome/browser/ui/web_applications/web_app_launch_utils.cc |
modified | |
ifchrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc |
modified | |
BindLambdaForTestingchrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc |
modified | |
BindLambdaForTestingchrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc |
modified | |
ifchrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc |
modified |
Files Changed
chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.ccchrome/browser/ui/web_applications/web_app_launch_utils.ccchrome/browser/ui/web_applications/navigation_capturing_process.hchrome/browser/ui/web_applications/navigation_capturing_process.ccchrome/browser/ui/web_applications/web_app_launch_process.ccchrome/browser/ui/web_applications/web_app_launch_utils_browsertest.ccchrome/browser/ui/browser_commands.cc
Audit Directions
- Re-entrant `Show()` / window-display callsAudit every site that calls
BrowserWindowInterface::GetWindow()->Show()(or a reparent that reaches it) and then keeps using the browser, itsWebContents, tab strip, or attached user-data objects; these must weak-guard or re-fetch after the call. - Raw pointers returned across destructive callbacksFlag helpers that return a
Browser*/BrowserWindowInterface*(or store araw_ptr) obtained before a synchronous nested-loop/close-capable call, and confirm the pointee cannot be freed before the caller dereferences it. - Member writes after re-entrant callsLook for
this-member assignments (likeredirection_result_) placed after calls that can destroy the owning object; the safe pattern is to record state before the destructive call or to weak-checkthisafterward.
Patch
From 760174e8ebd0522cefd6ea3c8bd063a354a428d2 Mon Sep 17 00:00:00 2001 From: Nate Chapin <[email protected]> Date: Thu, 10 Sep 2026 13:49:26 -0700 Subject: [PATCH] Guard browser lifetime during web contents reparenting When displaying a target browser window during web contents reparenting, synchronous window closure can destroy the browser and its associated navigation objects during Show(). Previously, downstream logic in ReparentWebContentsIntoBrowserImpl and related launch flows continued execution assuming the browser instance remained valid. This CL updates the reparenting paths to track the target browser and navigation capturing process using weak pointers, ensuring early exit if the target window closes during display. In addition, redirection results in NavigationCapturingProcess are recorded before initiating reparenting, and session service retrieval includes null checks before resetting state. TAG=agy Fixed: 553132148 Change-Id: I0d6a3a37450b8267e9ab94c65171d2bf8e27e342 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8370904 Reviewed-by: Darryl James <[email protected]> Reviewed-by: Marijn Kruisselbrink <[email protected]> Reviewed-by: Kunjan Patel <[email protected]> Commit-Queue: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695633} --- diff --git a/chrome/browser/ui/browser_commands.cc b/chrome/browser/ui/browser_commands.cc index bd8bbbcd74..12ee081 100644 --- a/chrome/browser/ui/browser_commands.cc +++ b/chrome/browser/ui/browser_commands.cc @@ -19,6 +19,7 @@ #include "base/feature_list.h" #include "base/i18n/rtl.h" #include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" #include "base/metrics/field_trial_params.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" @@ -2902,11 +2903,13 @@ BrowserWindowCreateParams(hosted_app_browser->GetProfile(), true)); } + base::WeakPtr<BrowserWindowInterface> target_browser_weak = + target_browser->GetWeakPtr(); web_app::ReparentWebContentsIntoBrowserImpl( hosted_app_browser, hosted_app_browser->GetTabStripModel()->GetActiveWebContents(), target_browser); - return target_browser; + return target_browser_weak.get(); } bool CanViewSource(BrowserWindowInterface* browser) { diff --git a/chrome/browser/ui/web_applications/navigation_capturing_process.cc b/chrome/browser/ui/web_applications/navigation_capturing_process.cc index 63224f57..dd0d89c 100644 --- a/chrome/browser/ui/web_applications/navigation_capturing_process.cc +++ b/chrome/browser/ui/web_applications/navigation_capturing_process.cc @@ -173,11 +173,10 @@ /*user_gesture=*/true)); } CHECK(AppBrowserController::IsWebApp(target_browser)); - ReparentWebContentsIntoBrowserImpl( - main_browser, old_web_contents, target_browser, - AppBrowserController::From(target_browser) - ->IsUrlInHomeTabScope(target_url)); - CHECK(old_web_contents); + ReparentWebContentsIntoBrowserImpl(main_browser, old_web_contents, + target_browser, + AppBrowserController::From(target_browser) + ->IsUrlInHomeTabScope(target_url)); } void ReparentWebContentsToTabbedBrowser( @@ -1095,8 +1094,14 @@ const std::optional<webapps::AppId> initial_launched_app_id = launched_app_id_; + base::WeakPtr<NavigationCapturingProcess> self = + weak_ptr_factory_.GetWeakPtr(); RedirectDecision decision = HandleRedirectImpl(); + if (!self) { + return decision.action; + } + // If we cancel the current navigation, that target window will manage its own // launch state. We exit early here to prevent recording duplicate launches on // this aborted navigation. @@ -1214,11 +1219,11 @@ initial_nav_handling_result_ == NavigationCapturingInitialResult::kNewAppWindow) { debug_data_.Set("!redirection_result", "Reparent, btab"); + redirection_result_ = + NavigationCapturingRedirectionResult::kReparentBrowserTabToBrowserTab; ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, disposition_, navigation_params_browser_); - redirection_result_ = - NavigationCapturingRedirectionResult::kReparentBrowserTabToBrowserTab; } else { debug_data_.Set("!redirection_result", "Noop1"); redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; @@ -1247,11 +1252,11 @@ // standalone-app -> browser-tab-app. if (target_display_mode == blink::mojom::DisplayMode::kBrowser) { debug_data_.Set("!redirection_result", "app to btab"); + redirection_result_ = + NavigationCapturingRedirectionResult::kReparentAppToBrowserTab; ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, disposition_, navigation_params_browser_); - redirection_result_ = - NavigationCapturingRedirectionResult::kReparentAppToBrowserTab; return {content::NavigationThrottle::PROCEED, *target_app_id}; } debug_data_.Set("!redirection_result", "app to app"); @@ -1371,11 +1376,11 @@ if (initial_nav_handling_result_ == NavigationCapturingInitialResult::kNewAppWindow) { debug_data_.Set("!redirection_result", "btab"); + redirection_result_ = + NavigationCapturingRedirectionResult::kAppBrowserTabOpened; ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, disposition_, navigation_params_browser_); - redirection_result_ = - NavigationCapturingRedirectionResult::kAppBrowserTabOpened; } return {content::NavigationThrottle::PROCEED, *target_app_id}; } diff --git a/chrome/browser/ui/web_applications/navigation_capturing_process.h b/chrome/browser/ui/web_applications/navigation_capturing_process.h index 159e588..808a2fe 100644 --- a/chrome/browser/ui/web_applications/navigation_capturing_process.h +++ b/chrome/browser/ui/web_applications/navigation_capturing_process.h @@ -11,6 +11,7 @@ #include <tuple> #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "base/time/time.h" #include "base/values.h" #include "chrome/browser/ui/web_applications/web_app_launch_navigation_handle_user_data.h" @@ -323,6 +324,8 @@ // "handling" the current navigation when asked from Navigate(). base::TimeTicks time_navigation_started_{base::TimeTicks::Now()}; + base::WeakPtrFactory<NavigationCapturingProcess> weak_ptr_factory_{this}; + NAVIGATION_HANDLE_USER_DATA_KEY_DECL(); }; } // namespace web_app diff --git a/chrome/browser/ui/web_applications/web_app_launch_process.cc b/chrome/browser/ui/web_applications/web_app_launch_process.cc index 72ea7542..ad4b3e9 100644 --- a/chrome/browser/ui/web_applications/web_app_launch_process.cc +++ b/chrome/browser/ui/web_applications/web_app_launch_process.cc @@ -7,6 +7,7 @@ #include "base/debug/crash_logging.h" #include "base/files/file_path.h" #include "base/memory/values_equivalent.h" +#include "base/memory/weak_ptr.h" #include "base/metrics/histogram_functions.h" #include "build/build_config.h" #include "chrome/browser/apps/app_service/launch_utils.h" @@ -234,7 +235,11 @@ browser = CreateBrowserForLaunch(); is_new_browser = true; } + base::WeakPtr<BrowserWindowInterface> browser_weak = browser->GetWeakPtr(); browser->GetWindow()->Show(); + if (!browser_weak) { + return nullptr; + } WindowOpenDisposition navigation_disposition = GetNavigationDisposition(is_new_browser); diff --git a/chrome/browser/ui/web_applications/web_app_launch_utils.cc b/chrome/browser/ui/web_applications/web_app_launch_utils.cc index 7ea9797..6d4d4da 100644 --- a/chrome/browser/ui/web_applications/web_app_launch_utils.cc +++ b/chrome/browser/ui/web_applications/web_app_launch_utils.cc @@ -145,10 +145,12 @@ chrome::NewTab(source_browser, NewTabTypes::kNoUserAction); } + base::WeakPtr<BrowserWindowInterface> target_browser_weak = + target_browser->GetWeakPtr(); ReparentWebContentsIntoBrowserImpl( source_browser, contents, target_browser, /*insert_as_pinned_home_tab=*/insert_as_pinned_home_tab); - return target_browser; + return target_browser_weak.get(); } #if BUILDFLAG(IS_CHROMEOS) @@ -383,8 +385,14 @@ apps::EnableLinkCapturingInfoBarDelegate::RemoveInfoBar(web_contents); } #endif + base::WeakPtr<BrowserWindowInterface> target_browser_weak = + target_browser->GetWeakPtr(); target_browser->GetWindow()->Show(); + if (!target_browser_weak) { + return; + } + // The window will be registered correctly, however the tab will not be // correctly tracked. We need to do a reset to get the tab correctly tracked // by either the app service or the regular service @@ -641,6 +649,9 @@ BrowserWindowInterface* reparented_browser = ReparentWebContentsIntoAppBrowser(contents, browser, app_id, as_pinned_home_tab); + if (!reparented_browser) { + return nullptr; + } UpdateLaunchMetricsAndStats( app_id, apps::LaunchContainer::kLaunchContainerWindow, diff --git a/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc b/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc index 16849163..d8d710e 100644 --- a/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc +++ b/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc @@ -8,9 +8,11 @@ #include <vector> #include "base/memory/weak_ptr.h" +#include "base/test/bind.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_window/public/create_browser_window.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h" #include "chrome/browser/ui/web_applications/web_app_browser_controller.h" #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h" @@ -24,6 +26,8 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/window_open_disposition.h" +#include "ui/views/widget/any_widget_observer.h" +#include "ui/views/widget/widget.h" namespace web_app { namespace { @@ -151,5 +155,83 @@ return ToString(param_info.param); }); +IN_PROC_BROWSER_TEST_F(WebAppBrowserTestBase, + ReparentWebContentsWindowClosedOnShow) { + const GURL app_url = + embedded_https_test_server().GetURL("/web_apps/simple/index.html"); + webapps::AppId app_id = InstallWebAppInNewTabAndClose(browser(), app_url); + + content::WebContents* to_reparent = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), app_url)); + + // Create a second tab so browser() remains open after detaching to_reparent. + chrome::NewTab(browser(), NewTabTypes::kNoUserAction); + + BrowserWindowInterface* app_browser = + CreateBrowserWindow(BrowserWindowCreateParams::CreateForApp( + GenerateApplicationNameFromAppId(app_id), + /*trusted_source=*/true, gfx::Rect(), profile(), + /*user_gesture=*/true)); + + BrowserView* browser_view = + BrowserView::GetBrowserViewForBrowser(app_browser); + ASSERT_TRUE(browser_view); + views::Widget* target_widget = browser_view->GetWidget(); + ASSERT_TRUE(target_widget); + + views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{}); + observer.set_shown_callback(base::BindRepeating( + [](views::Widget* target_widget, views::Widget* shown_widget) { + if (shown_widget == target_widget) { + shown_widget->CloseNow(); + } + }, + target_widget)); + + ui_test_utils::BrowserDestroyedObserver browser_destroyed_observer( + app_browser); + ReparentWebContentsIntoBrowserImpl(browser(), to_reparent, app_browser); + browser_destroyed_observer.Wait(); + + EXPECT_EQ(1, browser()->tab_strip_model()->count()); +} + +IN_PROC_BROWSER_TEST_F(WebAppBrowserTestBase, + ReparentWebContentsIntoAppBrowserWindowClosedOnShow) { + const GURL app_url = + embedded_https_test_server().GetURL("/web_apps/simple/index.html"); + webapps::AppId app_id = InstallWebAppInNewTabAndClose(browser(), app_url); + + content::WebContents* to_reparent = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), app_url)); + + // Create a second tab so browser() remains open after detaching to_reparent. + chrome::NewTab(browser(), NewTabTypes::kNoUserAction); + + views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{}); + bool closed_app_widget = false; + observer.set_shown_callback( + base::BindLambdaForTesting([&](views::Widget* widget) { + if (!closed_app_widget) { + BrowserView* browser_view = + BrowserView::GetBrowserViewForNativeWindow( + widget->GetNativeWindow()); + if (browser_view && browser_view->browser()->GetType() == + BrowserWindowInterface::Type::TYPE_APP) { + closed_app_widget = true; + widget->CloseNow(); + } + } + })); + + BrowserWindowInterface* result = + ReparentWebContentsIntoAppBrowser(to_reparent, app_id); + EXPECT_TRUE(closed_app_widget); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(1, browser()->tab_strip_model()->count()); +} + } // namespace } // namespace web_app diff --git a/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc b/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc index ffe549d..1a5d3bc 100644 --- a/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc +++ b/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc @@ -5,6 +5,7 @@ #include <vector> #include "base/strings/strcat.h" +#include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_future.h" @@ -41,6 +42,8 @@ #include "third_party/blink/public/mojom/manifest/manifest_launch_handler.mojom-shared.h" #include "ui/base/page_transition_types.h" #include "ui/base/window_open_disposition.h" +#include "ui/views/widget/any_widget_observer.h" +#include "ui/views/widget/widget.h" namespace web_app { @@ -1115,5 +1118,40 @@ TabStripModel::kNoTab); } +IN_PROC_BROWSER_TEST_F(NavigationCapturingWithRedirectionBrowserNavigatorTest, + RedirectReparentWindowClosedOnShow) { + // Test that if an app window is closed during presentation following a + // redirect reparenting, the browser gracefully handles the window closure + // without crashing. + InstallTestWebApp(GetLandingPage(), mojom::UserDisplayMode::kStandalone); + + base::RunLoop run_loop; + views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{}); + bool closed_app_widget = false; + observer.set_shown_callback( + base::BindLambdaForTesting([&](views::Widget* widget) { + if (!closed_app_widget) { + BrowserView* browser_view = + BrowserView::GetBrowserViewForNativeWindow( + widget->GetNativeWindow()); + if (browser_view && browser_view->browser()->GetType() == + BrowserWindowInterface::Type::TYPE_APP) { + closed_app_widget = true; + widget->CloseNow(); + run_loop.Quit(); + } + } + })); + + NavigateParams params(browser(), GetRedirectFromPage(), + ui::PAGE_TRANSITION_LINK); + params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; + Navigate(¶ms); + run_loop.Run(); + + EXPECT_TRUE(closed_app_widget); + EXPECT_TRUE(browser()); +} + } // namespace } // namespace web_app
Regression Test / PoC
diff --git a/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc b/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
index 16849163..d8d710e 100644
--- a/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
+++ b/chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
@@ -8,9 +8,11 @@
#include <vector>
#include "base/memory/weak_ptr.h"
+#include "base/test/bind.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/web_app_browser_controller.h"
#include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
@@ -24,6 +26,8 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/window_open_disposition.h"
+#include "ui/views/widget/any_widget_observer.h"
+#include "ui/views/widget/widget.h"
namespace web_app {
namespace {
@@ -151,5 +155,83 @@
return ToString(param_info.param);
});
+IN_PROC_BROWSER_TEST_F(WebAppBrowserTestBase,
+ ReparentWebContentsWindowClosedOnShow) {
+ const GURL app_url =
+ embedded_https_test_server().GetURL("/web_apps/simple/index.html");
+ webapps::AppId app_id = InstallWebAppInNewTabAndClose(browser(), app_url);
+
+ content::WebContents* to_reparent =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), app_url));
+
+ // Create a second tab so browser() remains open after detaching to_reparent.
+ chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
+
+ BrowserWindowInterface* app_browser =
+ CreateBrowserWindow(BrowserWindowCreateParams::CreateForApp(
+ GenerateApplicationNameFromAppId(app_id),
+ /*trusted_source=*/true, gfx::Rect(), profile(),
+ /*user_gesture=*/true));
+
+ BrowserView* browser_view =
+ BrowserView::GetBrowserViewForBrowser(app_browser);
+ ASSERT_TRUE(browser_view);
+ views::Widget* target_widget = browser_view->GetWidget();
+ ASSERT_TRUE(target_widget);
+
+ views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+ observer.set_shown_callback(base::BindRepeating(
+ [](views::Widget* target_widget, views::Widget* shown_widget) {
+ if (shown_widget == target_widget) {
+ shown_widget->CloseNow();
+ }
+ },
+ target_widget));
+
+ ui_test_utils::BrowserDestroyedObserver browser_destroyed_observer(
+ app_browser);
+ ReparentWebContentsIntoBrowserImpl(browser(), to_reparent, app_browser);
+ browser_destroyed_observer.Wait();
+
+ EXPECT_EQ(1, browser()->tab_strip_model()->count());
+}
+
+IN_PROC_BROWSER_TEST_F(WebAppBrowserTestBase,
+ ReparentWebContentsIntoAppBrowserWindowClosedOnShow) {
+ const GURL app_url =
+ embedded_https_test_server().GetURL("/web_apps/simple/index.html");
+ webapps::AppId app_id = InstallWebAppInNewTabAndClose(browser(), app_url);
+
+ content::WebContents* to_reparent =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), app_url));
+
+ // Create a second tab so browser() remains open after detaching to_reparent.
+ chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
+
+ views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+ bool closed_app_widget = false;
+ observer.set_shown_callback(
+ base::BindLambdaForTesting([&](views::Widget* widget) {
+ if (!closed_app_widget) {
+ BrowserView* browser_view =
+ BrowserView::GetBrowserViewForNativeWindow(
+ widget->GetNativeWindow());
+ if (browser_view && browser_view->browser()->GetType() ==
+ BrowserWindowInterface::Type::TYPE_APP) {
+ closed_app_widget = true;
+ widget->CloseNow();
+ }
+ }
+ }));
+
+ BrowserWindowInterface* result =
+ ReparentWebContentsIntoAppBrowser(to_reparent, app_id);
+ EXPECT_TRUE(closed_app_widget);
+ EXPECT_EQ(result, nullptr);
+ EXPECT_EQ(1, browser()->tab_strip_model()->count());
+}
+
} // namespace
} // namespace web_app
diff --git a/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc b/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
index ffe549d..1a5d3bc 100644
--- a/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
+++ b/chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
@@ -5,6 +5,7 @@
#include <vector>
#include "base/strings/strcat.h"
+#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
@@ -41,6 +42,8 @@
#include "third_party/blink/public/mojom/manifest/manifest_launch_handler.mojom-shared.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
+#include "ui/views/widget/any_widget_observer.h"
+#include "ui/views/widget/widget.h"
namespace web_app {
@@ -1115,5 +1118,40 @@
TabStripModel::kNoTab);
}
+IN_PROC_BROWSER_TEST_F(NavigationCapturingWithRedirectionBrowserNavigatorTest,
+ RedirectReparentWindowClosedOnShow) {
+ // Test that if an app window is closed during presentation following a
+ // redirect reparenting, the browser gracefully handles the window closure
+ // without crashing.
+ InstallTestWebApp(GetLandingPage(), mojom::UserDisplayMode::kStandalone);
+
+ base::RunLoop run_loop;
+ views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
+ bool closed_app_widget = false;
+ observer.set_shown_callback(
+ base::BindLambdaForTesting([&](views::Widget* widget) {
+ if (!closed_app_widget) {
+ BrowserView* browser_view =
+ BrowserView::GetBrowserViewForNativeWindow(
+ widget->GetNativeWindow());
+ if (browser_view && browser_view->browser()->GetType() ==
+ BrowserWindowInterface::Type::TYPE_APP) {
+ closed_app_widget = true;
+ widget->CloseNow();
+ run_loop.Quit();
+ }
+ }
+ }));
+
+ NavigateParams params(browser(), GetRedirectFromPage(),
+ ui::PAGE_TRANSITION_LINK);
+ params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ Navigate(¶ms);
+ run_loop.Run();
+
+ EXPECT_TRUE(closed_app_widget);
+ EXPECT_TRUE(browser());
+}
+
} // namespace
} // namespace web_app