Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Bug ClassUAF
Tracker527676561
Fix commitc889717537a8 (chromium/src) +103/-0
CISA KEVNot listed
Creditedxinchaotian of Microsoft
Disclosed2026-07-14

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_frame_host_impl.cc
modified
if
content/browser/web_contents/web_contents_impl.cc
modified
DestroyOpenerOnAddNewContentsDelegate
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
if
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
BindLambdaForTesting
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
OutgoingSetRendererPrefsMojoWatcher
content/browser/web_contents/web_contents_impl_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/web_contents/web_contents_impl_browsertest.cc
From c889717537a805003583d4f5d88fd45204b58c67 Mon Sep 17 00:00:00 2001
From: Xinchao Tian <[email protected]>
Date: Fri, 26 Jun 2026 15:40:50 -0700
Subject: [PATCH] Guard against re-entrant destruction in CreateNewWindow

CreateNewWindow() (opener-suppressed path) calls
delegate_->AddNewContents() and then continues using state owned by this
(opener, delegate_, primary frame tree).

On Windows, AddNewContents() may enter a nested message loop while
showing a new browser window. A window-close message can destroy the
opener WebContents during this call, causing use-after-free.

Fix by capturing a WeakPtr to this before calling AddNewContents() and
aborting if it is invalidated, similar to existing weak_new_contents
logic.

Add a browsertest that destroys the opener inside AddNewContents() to
prevent regressions.

Bug: 527676561
Change-Id: If224916a349113845cce68c438b04f451126d6ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8003496
Commit-Queue: Xinchao Tian <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Bo Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1653497}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 48ed416a..667a3d27 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -10455,9 +10455,16 @@
 
   // The non-owning pointer |new_frame_tree| is valid in this stack frame at
   // least until the call to ShowCreatedWindow() below.
+  base::WeakPtr<RenderFrameHostImpl> weak_self = GetWeakPtr();
   FrameTree* new_frame_tree =
       delegate_->CreateNewWindow(this, *params, is_new_browsing_instance,
                                  was_consumed, cloned_namespace.get());
+  if (!weak_self) {
+    // This RFH may be deleted after CreateNewWindow() due to a nested message
+    // loop (e.g. showing the new window closes the window hosting `this`). See
+    // crbug.com/527676561.
+    return;
+  }
 
   transient_allow_popup_.Deactivate();
 
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 256b12d..66e57cf 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -5708,12 +5708,20 @@
   bool was_blocked = false;
   base::WeakPtr<WebContentsImpl> weak_new_contents =
       new_contents_impl->weak_factory_.GetWeakPtr();
+  base::WeakPtr<WebContentsImpl> weak_this = weak_factory_.GetWeakPtr();
   WebContentsImpl* contents_to_load = new_contents_impl;
   if (delegate_) {
     WebContents* web_contents_navigated = delegate_->AddNewContents(
         this, std::move(new_contents), params.target_url, params.disposition,
         *params.features, has_user_gesture, &was_blocked);
 
+    if (!weak_this) {
+      // `this` may be deleted after AddNewContents() due to a nested message
+      // loop (e.g. the window hosting the opener is closed). See
+      // crbug.com/527676561.
+      return nullptr;
+    }
+
     if (base::FeatureList::IsEnabled(features::kPwaNavigationCapturing)) {
       // The delegate may delete |new_contents_impl| during AddNewContents().
       // If that occurs and there isn't a replacement contents returned, exit.
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index 392b34d..2ab335b 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -3622,6 +3622,94 @@
 
 namespace {
 
+// A WebContentsDelegate whose AddNewContents() runs a caller-provided closure
+// before returning. The closure is used to destroy the opener WebContents
+// synchronously, simulating the re-entrant destruction that can happen when
+// AddNewContents() spins a nested run loop (e.g. on Windows, showing the new
+// browser window dispatches native messages that can close the opener window).
+class DestroyOpenerOnAddNewContentsDelegate : public WebContentsDelegate {
+ public:
+  explicit DestroyOpenerOnAddNewContentsDelegate(base::OnceClosure on_add)
+      : on_add_(std::move(on_add)) {}
+
+  WebContents* AddNewContents(
+      WebContents* source,
+      std::unique_ptr<WebContents> new_contents,
+      const GURL& target_url,
+      WindowOpenDisposition disposition,
+      const blink::mojom::WindowFeatures& window_features,
+      bool user_gesture,
+      bool* was_blocked) override {
+    // Keep the new popup alive so that CreateNewWindow()'s `weak_new_contents`
+    // guard does NOT short-circuit. Otherwise the function would return early
+    // before reaching the code that dereferences the (now destroyed) opener,
+    // and the regression would not be exercised.
+    new_contents_ = std::move(new_contents);
+    WebContents* raw_new_contents = new_contents_.get();
+    if (on_add_) {
+      std::move(on_add_).Run();
+    }
+    return raw_new_contents;
+  }
+
+ private:
+  base::OnceClosure on_add_;
+  std::unique_ptr<WebContents> new_contents_;
+};
+
+}  // namespace
+
+// Regression test for a use-after-free where the opener WebContents is
+// destroyed re-entrantly while WebContentsImpl::CreateNewWindow() is calling
+// WebContentsDelegate::AddNewContents(). With an opener-suppressed
+// (`noopener`) window.open(), CreateNewWindow() drives the new window through
+// the delegate and then keeps using `this` (and `delegate_`/`opener`) after
+// AddNewContents() returns. If the opener is torn down during that call, the
+// trailing code used to run on freed memory. See crbug.com/527676561.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
+                       CreateNewWindowOpenerDestroyedInAddNewContents) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  // Create an opener WebContents owned by the test, so the delegate can destroy
+  // it from within AddNewContents().
+  WebContents::CreateParams create_params(
+      shell()->web_contents()->GetBrowserContext());
+  create_params.desired_renderer_state =
+      WebContents::CreateParams::kInitializeAndWarmupRendererProcess;
+  std::unique_ptr<WebContents> opener(WebContents::Create(create_params));
+  WebContents* opener_ptr = opener.get();
+
+  base::RunLoop run_loop;
+  DestroyOpenerOnAddNewContentsDelegate delegate(
+      base::BindLambdaForTesting([&]() {
+        // Destroy the opener (`this` inside CreateNewWindow()) synchronously.
+        opener.reset();
+        run_loop.Quit();
+      }));
+  opener_ptr->SetDelegate(&delegate);
+
+  const GURL opener_url(
+      embedded_test_server()->GetURL("a.com", "/title1.html"));
+  ASSERT_TRUE(NavigateToURL(opener_ptr, opener_url));
+
+  // Open a new window with `noopener` so CreateNewWindow() takes the
+  // opener-suppressed path that shows/navigates the window via the delegate.
+  // The script is run fire-and-forget because the opener frame is destroyed
+  // while the window.open() IPC is being handled.
+  const GURL popup_url(
+      embedded_test_server()->GetURL("a.com", "/title2.html"));
+  ExecuteScriptAsync(
+      opener_ptr,
+      JsReplace("window.open($1, '_blank', 'noopener');", popup_url));
+
+  // The opener is destroyed during AddNewContents(). The test passes if this
+  // completes without a use-after-free (caught under ASAN).
+  run_loop.Run();
+  EXPECT_FALSE(opener);
+}
+
+namespace {
+
 class OutgoingSetRendererPrefsMojoWatcher {
  public:
   explicit OutgoingSetRendererPrefsMojoWatcher(RenderViewHostImpl* rvh)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index 392b34d..2ab335b 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -3622,6 +3622,94 @@
 
 namespace {
 
+// A WebContentsDelegate whose AddNewContents() runs a caller-provided closure
+// before returning. The closure is used to destroy the opener WebContents
+// synchronously, simulating the re-entrant destruction that can happen when
+// AddNewContents() spins a nested run loop (e.g. on Windows, showing the new
+// browser window dispatches native messages that can close the opener window).
+class DestroyOpenerOnAddNewContentsDelegate : public WebContentsDelegate {
+ public:
+  explicit DestroyOpenerOnAddNewContentsDelegate(base::OnceClosure on_add)
+      : on_add_(std::move(on_add)) {}
+
+  WebContents* AddNewContents(
+      WebContents* source,
+      std::unique_ptr<WebContents> new_contents,
+      const GURL& target_url,
+      WindowOpenDisposition disposition,
+      const blink::mojom::WindowFeatures& window_features,
+      bool user_gesture,
+      bool* was_blocked) override {
+    // Keep the new popup alive so that CreateNewWindow()'s `weak_new_contents`
+    // guard does NOT short-circuit. Otherwise the function would return early
+    // before reaching the code that dereferences the (now destroyed) opener,
+    // and the regression would not be exercised.
+    new_contents_ = std::move(new_contents);
+    WebContents* raw_new_contents = new_contents_.get();
+    if (on_add_) {
+      std::move(on_add_).Run();
+    }
+    return raw_new_contents;
+  }
+
+ private:
+  base::OnceClosure on_add_;
+  std::unique_ptr<WebContents> new_contents_;
+};
+
+}  // namespace
+
+// Regression test for a use-after-free where the opener WebContents is
+// destroyed re-entrantly while WebContentsImpl::CreateNewWindow() is calling
+// WebContentsDelegate::AddNewContents(). With an opener-suppressed
+// (`noopener`) window.open(), CreateNewWindow() drives the new window through
+// the delegate and then keeps using `this` (and `delegate_`/`opener`) after
+// AddNewContents() returns. If the opener is torn down during that call, the
+// trailing code used to run on freed memory. See crbug.com/527676561.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
+                       CreateNewWindowOpenerDestroyedInAddNewContents) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  // Create an opener WebContents owned by the test, so the delegate can destroy
+  // it from within AddNewContents().
+  WebContents::CreateParams create_params(
+      shell()->web_contents()->GetBrowserContext());
+  create_params.desired_renderer_state =
+      WebContents::CreateParams::kInitializeAndWarmupRendererProcess;
+  std::unique_ptr<WebContents> opener(WebContents::Create(create_params));
+  WebContents* opener_ptr = opener.get();
+
+  base::RunLoop run_loop;
+  DestroyOpenerOnAddNewContentsDelegate delegate(
+      base::BindLambdaForTesting([&]() {
+        // Destroy the opener (`this` inside CreateNewWindow()) synchronously.
+        opener.reset();
+        run_loop.Quit();
+      }));
+  opener_ptr->SetDelegate(&delegate);
+
+  const GURL opener_url(
+      embedded_test_server()->GetURL("a.com", "/title1.html"));
+  ASSERT_TRUE(NavigateToURL(opener_ptr, opener_url));
+
+  // Open a new window with `noopener` so CreateNewWindow() takes the
+  // opener-suppressed path that shows/navigates the window via the delegate.
+  // The script is run fire-and-forget because the opener frame is destroyed
+  // while the window.open() IPC is being handled.
+  const GURL popup_url(
+      embedded_test_server()->GetURL("a.com", "/title2.html"));
+  ExecuteScriptAsync(
+      opener_ptr,
+      JsReplace("window.open($1, '_blank', 'noopener');", popup_url));
+
+  // The opener is destroyed during AddNewContents(). The test passes if this
+  // completes without a use-after-free (caught under ASAN).
+  run_loop.Run();
+  EXPECT_FALSE(opener);
+}
+
+namespace {
+
 class OutgoingSetRendererPrefsMojoWatcher {
  public:
   explicit OutgoingSetRendererPrefsMojoWatcher(RenderViewHostImpl* rvh)
Loading diff…

Original Bug Report

reported by [email protected]

UAF: WebContentsImpl::CreateNewWindow can use `this` after it is destroyed re-entrantly during `delegate_->AddNewContents()`

Summary

In WebContentsImpl::CreateNewWindow, the opener-suppressed (noopener) branch calls delegate_->AddNewContents() and then keeps dereferencing state owned by this (opener, delegate_, and finally returns &new_contents_impl->GetPrimaryFrameTree()). On Windows, AddNewContents() can synchronously spin a nested run loop while showing the new browser window (ShowWindow dispatches native window messages). A window-close message (WM_SYSCOMMAND / SC_CLOSE) processed in that nested loop can destroy the opener WebContents — i.e. the this currently executing CreateNewWindow — before AddNewContents() returns. Execution then resumes on a freed this / opener, leading to a use-after-free.

Details

The current code only guards the newly created contents via weak_new_contents; it does not guard this. The re-entrant destruction path:

delegate_->AddNewContents()Browser::AddNewContentsNavigateBrowserView::Showviews::Widget::ShowDesktopWindowTreeHostWin::ShowShowWindow (synchronously pumps native window messages) → nested WndProc dispatch receives WM_SYSCOMMAND(SC_CLOSE) for the opener’s browser window → Browser::OnWindowClosingTabStripModel::CloseAllTabs → destroys all tabs' WebContents, including the opener (this).

After AddNewContents() returns, CreateNewWindow runs load_params->initiator_origin = opener->GetLastCommittedOrigin(); on the freed opener → UAF.

Location

content/browser/web_contents/web_contents_impl.cc, WebContentsImpl::CreateNewWindow, the opener-suppressed tail (the delegate_->AddNewContents(...) call and the code that follows it).

Evidence (representative stack)

Top frame is the crash point. Win32/comctl32/uxtheme message-dispatch noise is collapsed but preserved enough to show this is a genuine nested WndProc dispatch triggered synchronously by ShowWindow.

// ===== (4) Re-entrant destruction: opener WebContents (this) destroyed in nested loop =====
content::WebContentsImpl::~WebContentsImpl              content/browser/web_contents/web_contents_impl.cc
std::unique_ptr<content::WebContents>::reset/~          (WebContents owned by the tab)
tabs::TabModel::~TabModel                               chrome/browser/ui/tabs/tab_model.cc
std::unique_ptr<tabs::TabModel>::reset
TabStripModel::SendDetachWebContentsNotifications       chrome/browser/ui/tabs/tab_strip_model.cc
TabStripModel::InternalCloseTabsImpl                    chrome/browser/ui/tabs/tab_strip_model.cc
TabStripModel::CloseTabs                                chrome/browser/ui/tabs/tab_strip_model.cc
TabStripModel::CloseAllTabs                             chrome/browser/ui/tabs/tab_strip_model.cc
Browser::OnWindowClosingPostClearBrowsingData           chrome/browser/ui/browser.cc
Browser::OnWindowClosing                                chrome/browser/ui/browser.cc
BrowserView::OnWindowCloseRequested                     chrome/browser/ui/views/frame/browser_view.cc
views::Widget::CloseWithReason                          ui/views/widget/widget.cc
views::HWNDMessageHandler::ProcessWindowMessage         ui/views/win/hwnd_message_handler.h
views::HWNDMessageHandler::OnWndProc                    ui/views/win/hwnd_message_handler.cc
gfx::WindowImpl::WndProc                                ui/gfx/win/window_impl.cc
base::win::WrappedWindowProc<>                          base/win/wrapped_window_proc.h
    ... user32!CallWindowProc / comctl32 subclass procs ...
ui::WindowSubclass::StaticSubClassWndProc               ui/base/win/window_subclass.cc
    ... user32!DispatchClientMessage / ntdll!KiUserCallbackDispatcher ...
    ... uxtheme!OnDwpSysCommand / user32!DefWindowProcW ...      // default SC_CLOSE handling
views::HWNDMessageHandler::OnSysCommand                 ui/views/win/hwnd_message_handler.cc  // WM_SYSCOMMAND (SC_CLOSE)
views::HWNDMessageHandler::ProcessWindowMessage         ui/views/win/hwnd_message_handler.h
views::HWNDMessageHandler::OnWndProc                    ui/views/win/hwnd_message_handler.cc
gfx::WindowImpl::WndProc                                ui/gfx/win/window_impl.cc
base::win::WrappedWindowProc<>                          base/win/wrapped_window_proc.h
    ... user32!CallWindowProc / comctl32 subclass / KiUserCallbackDispatcher ...

// ===== (3) Re-entrancy boundary: ShowWindow synchronously pumps native messages =====
win32u!ZwUserShowWindow                                 // ::ShowWindow dispatches window messages
views::HWNDMessageHandler::Show                         ui/views/win/hwnd_message_handler.cc
views::DesktopWindowTreeHostWin::Show                   ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc
views::Widget::Show                                     ui/views/widget/widget.cc
BrowserView::Show                                       chrome/browser/ui/views/frame/browser_view.cc
(anonymous namespace)::ScopedBrowserShower::~ScopedBrowserShower  chrome/browser/ui/browser_navigator.cc
Navigate                                                chrome/browser/ui/browser_navigator.cc
chrome::AddWebContents                                  chrome/browser/ui/browser_tabstrip.cc

// ===== (2) Delegate shows/navigates the new window =====
Browser::AddNewContents                                 chrome/browser/ui/browser.cc
    // = content::WebContentsDelegate::AddNewContents

// ===== (1) opener is still on the CreateNewWindow stack frame =====
content::WebContentsImpl::CreateNewWindow               content/browser/web_contents/web_contents_impl.cc
    // call site: delegate_->AddNewContents(...);
    // after it returns, runs load_params->initiator_origin = opener->GetLastCommittedOrigin();
    //   → opener / this already destroyed by (4) → heap-use-after-free
content::RenderFrameHostImpl::CreateNewWindow           content/browser/renderer_host/render_frame_host_impl.cc
    // reached from renderer's window.open(url, '_blank', 'noopener') via mojo

> Note: exact frames between OnSysCommand and OnWindowClosing (the widget > teardown callbacks) may vary slightly by milestone.

Reachability

The crash was first observed downstream where the opener WebContents is destroyed inside AddNewContents()’s nested ShowWindow message loop via TabStripModel::CloseAllTabs. The same nested-loop-during-Show() race is reachable in upstream Chromium via a noopener window.open() whose synchronous window display coincides with the opener’s browser window closing. It is extremely timing-sensitive and rarely hit naturally, which is likely why it has not been reported upstream.

Why the existing guard is insufficient

CreateNewWindow already captures weak_new_contents for the newly created WebContents and even notes “The delegate may delete new_contents_impl during AddNewContents()”. However, nothing guards this. When the opener itself is destroyed re-entrantly, the trailing code (opener->GetLastCommittedOrigin(), opener->GetFrameToken(), delegate_, &new_contents_impl->GetPrimaryFrameTree()) runs on freed memory.

Proposed fix

Capture base::WeakPtr<WebContentsImpl> weak_this before AddNewContents() and bail out (return nullptr) if it has been invalidated afterward, mirroring the existing weak_new_contents guard.

View on issue tracker