CVE-2026-11671
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.cc
Patch
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.
Original Bug Report
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.cccontent/browser/renderer_host/navigator.cccontent/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:2321readsframe_tree_node_->navigation_request()from freed memory.navigation_request.cc:2410callsGetDelegate()->DidFinishNavigation(this).GetDelegate()dereferencesframe_tree_node_to get the freedFrameTree, thenNavigator, thenNavigatorDelegate, 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.
- On macOS, Page A opens same-site popup Page B.
- Page B requests HTML fullscreen (
document.documentElement.requestFullscreen()) and then navigates to a cross-document location. - Immediately before the commit occurs, Page A closes B (
w.close()), queuing a destruction task in the browser’s event loop. - When
DidCommitProvisionalLoadis processed on the browser,ExitFullscreenspins a nested AppKit run loop to animate the fullscreen transition. - The nested loop executes the pending close task, destroying
WebContentsImpland freeing itsFrameTreeNode. - Once the nested loop exits, the
!weak_rfhguard fires inNavigator::DidNavigateand returns early. - This triggers
~NavigationRequest, dereferencing the freedframe_tree_node_and callingGetDelegate()->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.