Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebAppInstalls
DescriptionUse after free in WebAppInstalls
ComponentWebAppInstalls
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker553132148
Fix commit760174e8ebd0 (chromium/src) +160/-13
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-26 (per vendor advisory)
Fix landed2026-09-10 (public git)
Disclosed2026-09-15
Patch gap5 days — fix public in source before the advisory

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 by web_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/BrowserWindowInterface before it returns.
`NavigationCapturingProcess`
a NavigationHandleUserData attached 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.

Key insight
The single mistake was treating 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

  1. Trigger a capturing reparent Cause a navigation (e.g. a link-captured navigation or a web-app launch) that makes Chrome reparent a WebContents into a web-app window via ReparentWebContentsIntoAppBrowser / ReparentWebContentsToTabbedBrowser / ReparentWebContentsIntoBrowserImpl.
  2. 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 a views::AnyWidgetObserver shown callback, which destroys the Browser and its navigation objects mid-call.
  3. 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 writes redirection_result_ on the freed NavigationCapturingProcess, producing the use-after-free.
  4. 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

An attacker who can drive a navigation captured into an installed web app obtains a use-after-free in the browser process, the highest-privilege Chrome process. Preconditions are that a web app is installed and that the target window is closed synchronously during the reparenting 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

FunctionChangeNotes
if
chrome/browser/ui/web_applications/navigation_capturing_process.cc
modified
if
chrome/browser/ui/web_applications/web_app_launch_process.cc
modified
if
chrome/browser/ui/web_applications/web_app_launch_utils.cc
modified
if
chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
modified
BindLambdaForTesting
chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
modified
BindLambdaForTesting
chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
modified
if
chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
modified

Files Changed

  • chrome/browser/web_applications/navigation_capturing_browser_navigator_browsertest.cc
  • chrome/browser/ui/web_applications/web_app_launch_utils.cc
  • chrome/browser/ui/web_applications/navigation_capturing_process.h
  • chrome/browser/ui/web_applications/navigation_capturing_process.cc
  • chrome/browser/ui/web_applications/web_app_launch_process.cc
  • chrome/browser/ui/web_applications/web_app_launch_utils_browsertest.cc
  • chrome/browser/ui/browser_commands.cc

Audit Directions

  • Re-entrant `Show()` / window-display calls
    Audit every site that calls BrowserWindowInterface::GetWindow()->Show() (or a reparent that reaches it) and then keeps using the browser, its WebContents, tab strip, or attached user-data objects; these must weak-guard or re-fetch after the call.
  • Raw pointers returned across destructive callbacks
    Flag helpers that return a Browser*/BrowserWindowInterface* (or store a raw_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 calls
    Look for this-member assignments (like redirection_result_) placed after calls that can destroy the owning object; the safe pattern is to record state before the destructive call or to weak-check this afterward.
From 760174e8ebd0522cefd6ea3c8bd063a354a428d2 Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
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 <dljames@chromium.org>
Reviewed-by: Marijn Kruisselbrink <mek@chromium.org>
Reviewed-by: Kunjan Patel <ksukh@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
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(&params);
+  run_loop.Run();
+
+  EXPECT_TRUE(closed_app_widget);
+  EXPECT_TRUE(browser());
+}
+
 }  // namespace
 }  // namespace web_app
Loading diff…

Regression Test / PoC

shipped with the fix
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(&params);
+  run_loop.Run();
+
+  EXPECT_TRUE(closed_app_widget);
+  EXPECT_TRUE(browser());
+}
+
 }  // namespace
 }  // namespace web_app
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.