Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Navigation
DescriptionUse after free in Navigation
ComponentNavigation
Bug ClassUAF
Tracker516608438
Fix commitf4e796657195 (chromium/src) +15/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_frame_host_impl.cc
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
From f4e79665719591b82490c347611095cb4f6098b8 Mon Sep 17 00:00:00 2001
From: Charlie Reis <[email protected]>
Date: Thu, 28 May 2026 08:53:45 -0700
Subject: [PATCH] Early return if a nested message loop deletes the RFH.

Bug: 516608438
Change-Id: Id8dca00538f844f024d31c3f362363582d2dd1f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879617
Reviewed-by: Arthur Sonzogni <[email protected]>
Commit-Queue: Charlie Reis <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1637734}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 75b89a4..d7e66aa 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -6476,10 +6476,16 @@
   });
 #endif
 
+  base::WeakPtr<RenderFrameHostImpl> weak_ptr = GetWeakPtr();
   DidCommitNavigationInternal(
       std::move(owned_request), std::move(params),
       /*same_document_params=*/nullptr,
       /*did_commit_ipc_received_time=*/base::TimeTicks());
+  if (!weak_ptr) {
+    // This RFH may be deleted after DidCommitNavigationInternal due to a nested
+    // message loop. All callers should handle this.
+    return;
+  }
 
   // NOTE: Navigation metrics assume that not much work is done between
   // DidCommitNavigationInternal() and the end of this function. Avoid adding
@@ -16340,10 +16346,17 @@
                                            : navigation_request->StartedByAd();
 
   // TODO(crbug.com/40150370): Do not pass |params| to DidNavigate().
-  NavigationRequest* raw_navigation_request = navigation_request.get();
-  raw_navigation_request->frame_tree_node()->navigator().DidNavigate(
+  FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
+  base::WeakPtr<RenderFrameHostImpl> weak_ptr = GetWeakPtr();
+  frame_tree_node->navigator().DidNavigate(
       this, *params, std::move(navigation_request), is_same_document_navigation,
       caused_by_ad);
+  if (!weak_ptr) {
+    // This RFH may be deleted after DidNavigate due to a nested message loop.
+    // That occurs before the navigation has actually committed, so return false
+    // to indicate that the commit did not succeed.
+    return false;
+  }
 
   // Run any deferred shared storage operations from response headers now that
   // commit has occurred.
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser UAF in ~NavigationRequest due to early-return in Navigator::DidNavigate

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the browser process on macOS when Navigator::DidNavigate returns early. If the WebContents is destroyed during a nested AppKit message loop spun by ExitFullscreenModeForTab, the NavigationRequest unique_ptr is dropped upon early-return and its destructor dereferences a freed FrameTreeNode. Because the FrameTreeNode pointer is marked as RAW_PTR_EXCLUSION, it is not protected by MiraclePtr.

Affected files:

  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigator.cc
  • content/browser/renderer_host/navigation_request.h

Estimated timestamp from git blame: 2026-03-06

Root Cause Analysis

A potential Use-After-Free (UAF) vulnerability exists in the browser process due to ~NavigationRequest dereferencing a freed FrameTreeNode when Navigator::DidNavigate returns early on macOS.

In Navigator::DidNavigate (content/browser/renderer_host/navigator.cc):

void Navigator::DidNavigate(
    RenderFrameHostImpl* render_frame_host,
    const mojom::DidCommitProvisionalLoadParams& params,
    std::unique_ptr<NavigationRequest> navigation_request,
    bool was_within_same_document, bool caused_by_ad) {
  ...
  base::WeakPtr<RenderFrameHostImpl> weak_rfh = render_frame_host->GetWeakPtr();
  delegate_->DidNavigateAnyFramePreCommit(navigation_request.get(),
                                          was_within_same_document);
  if (!weak_rfh) {
    return; // Destroys navigation_request -> ~NavigationRequest()
  }

If DidNavigateAnyFramePreCommit triggers a nested AppKit message loop (e.g., via ExitFullscreenModeForTab on macOS) and destroys the WebContents (due to a queued window-close task), the function returns early at the !weak_rfh check.

However, the WebContentsImpl and its root FrameTreeNode have already been freed. NavigationRequest::frame_tree_node_ is explicitly excluded from MiraclePtr (RAW_PTR_EXCLUSION in content/browser/renderer_host/navigation_request.h):

RAW_PTR_EXCLUSION FrameTreeNode* const frame_tree_node_;

Consequently, ~NavigationRequest dereferences this stale pointer:

  • navigation_request.cc:2321 reads frame_tree_node_->navigation_request() from freed memory.
  • navigation_request.cc:2410 calls GetDelegate()->DidFinishNavigation(this). GetDelegate() dereferences frame_tree_node_ to get the freed FrameTree, then Navigator, then NavigatorDelegate, executing a virtual method call on a potentially attacker-controlled pointer.

Potential Attack Scenario

Note: These are potential steps to trigger the issue, as our tooling agent does not have the ability to run code or verify a live exploit.

  1. On macOS, Page A opens same-site popup Page B.
  2. Page B requests HTML fullscreen (document.documentElement.requestFullscreen()) and then navigates to a cross-document location.
  3. Immediately before the commit occurs, Page A closes B (w.close()), queuing a destruction task in the browser’s event loop.
  4. When DidCommitProvisionalLoad is processed on the browser, ExitFullscreen spins a nested AppKit run loop to animate the fullscreen transition.
  5. The nested loop executes the pending close task, destroying WebContentsImpl and freeing its FrameTreeNode.
  6. Once the nested loop exits, the !weak_rfh guard fires in Navigator::DidNavigate and returns early.
  7. This triggers ~NavigationRequest, dereferencing the freed frame_tree_node_ and calling GetDelegate()->DidFinishNavigation(this).

Impact

This issue could lead to a browser-process Use-After-Free (UAF). If the memory is reclaimed and sprayed by an attacker, they can control the layout and execute arbitrary virtual method calls, potentially resulting in Remote Code Execution (RCE) in the browser process (sandbox escape).

Suggested Fix

To prevent the UAF, do not dereference frame_tree_node_ directly in the destructor of NavigationRequest if it is no longer valid. Since FrameTreeNode provides a stable ID lookup, we can store FrameTreeNodeId and retrieve the node dynamically, verifying it is still alive before performing actions in the destructor:

// In NavigationRequest::~NavigationRequest():
FrameTreeNode* node = FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
if (node) {
  if (NavigationRequest* request = node->navigation_request()) {
    if (request->IsQueued() && request != this) {
      ...
    }
  }
}

And update GetDelegate() to handle null pointers gracefully if the node has been destroyed:

NavigatorDelegate* NavigationRequest::GetDelegate() const {
  FrameTreeNode* node = FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
  return node ? node->navigator().GetDelegate() : nullptr;
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker