Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Navigation
DescriptionUse after free in Navigation
ComponentNavigation
Bug ClassUAF
Tracker495931147
Fix commit43dd54a96e26 (chromium/src) +31/-22
CISA KEVNot listed
CreditedGoogle
Disclosed2026-03-31

Changed Functions

FunctionChangeNotes
for
content/browser/renderer_host/navigation_entry_impl.cc
modified
if
content/browser/renderer_host/navigation_entry_impl.cc
modified

Files Changed

  • content/browser/renderer_host/navigation_entry_impl.cc
From 43dd54a96e266921e64bdef75b15bfcda40c2cb3 Mon Sep 17 00:00:00 2001
From: Charlie Reis <creis@chromium.org>
Date: Thu, 26 Mar 2026 12:37:16 -0700
Subject: [PATCH] Remove NavigationEntryImpl::TreeNode from parent.

If we already have the TreeNode to remove, it is not necessary to find
it again from the root in RemoveEntryForFrame. Factor out the node
removal code and use it directly.

Change-Id: Id5d5f4cefa91e7d571230676698a2f11623a1f22
Bug: 495931147
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7703455
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1605715}
---

diff --git a/content/browser/renderer_host/navigation_entry_impl.cc b/content/browser/renderer_host/navigation_entry_impl.cc
index 2d36dab..ce2f66ca3 100644
--- a/content/browser/renderer_host/navigation_entry_impl.cc
+++ b/content/browser/renderer_host/navigation_entry_impl.cc
@@ -236,6 +236,33 @@
   return true;
 }
 
+// Removes a given subframe `node` directly from its parent, updating any
+// necessary bookkeeping. Not for use with main frames.
+void RemoveTreeNodeFromParent(NavigationEntryImpl::TreeNode* node) {
+  CHECK(node->parent);
+  auto* frame_entry = node->frame_entry.get();
+  if (frame_entry && frame_entry->committed_origin()) {
+    // Normally default-isolated origins are tracked through their presence in
+    // session history, which is consulted whenever an origin newly requests
+    // isolation. If we remove a frame_entry, its origin won't be available
+    // to any future global walk if the same origin later wants to opt-in. So
+    // we add it to the non-opt-in list here to be spec compliant (unless it's
+    // currently opted-in, in which case this call will do nothing).
+    ChildProcessSecurityPolicyImpl::GetInstance()
+        ->AddDefaultIsolatedOriginIfNeeded(
+            frame_entry->site_instance()->GetIsolationContext(),
+            frame_entry->committed_origin().value(),
+            true /* global_ walk_or_frame_removal */);
+  }
+
+  NavigationEntryImpl::TreeNode* parent_node = node->parent;
+  auto it =
+      std::ranges::find(parent_node->children, node,
+                        &std::unique_ptr<NavigationEntryImpl::TreeNode>::get);
+  CHECK(it != parent_node->children.end());
+  parent_node->children.erase(it);
+}
+
 void RegisterOriginsRecursive(NavigationEntryImpl::TreeNode* node,
                               const url::Origin& origin) {
   if (node->frame_entry->committed_origin().has_value()) {
@@ -1183,7 +1210,8 @@
   for (const auto& child : parent_node->children) {
     if (child->frame_entry->frame_unique_name() == unique_name) {
       if (update_policy == UpdatePolicy::kReplace) {
-        RemoveEntryForFrame(frame_tree_node, false);
+        RemoveTreeNodeFromParent(child.get());
+        // `child` is now deleted.
         break;
       }
       // If the document of the FrameNavigationEntry is changing, we must clear
@@ -1280,7 +1308,7 @@
 
 void NavigationEntryImpl::RemoveEntryForFrame(FrameTreeNode* frame_tree_node,
                                               bool only_if_different_position) {
-  DCHECK(!frame_tree_node->IsMainFrame());
+  CHECK(!frame_tree_node->IsMainFrame());
 
   NavigationEntryImpl::TreeNode* node = GetTreeNode(frame_tree_node);
   if (!node) {
@@ -1292,26 +1320,7 @@
   // FrameNavigationEntries and the FrameTree.
   if (!only_if_different_position ||
       !InSameTreePosition(frame_tree_node, node)) {
-    auto* frame_entry = node->frame_entry.get();
-    if (frame_entry && frame_entry->committed_origin()) {
-      // Normally default-isolated origins are tracked through their presence in
-      // session history, which is consulted whenever an origin newly requests
-      // isolation. If we remove a frame_entry, its origin won't be available
-      // to any future global walk if the same origin later wants to opt-in. So
-      // we add it to the non-opt-in list here to be spec compliant (unless it's
-      // currently opted-in, in which case this call will do nothing).
-      ChildProcessSecurityPolicyImpl::GetInstance()
-          ->AddDefaultIsolatedOriginIfNeeded(
-              frame_entry->site_instance()->GetIsolationContext(),
-              frame_entry->committed_origin().value(),
-              true /* global_ walk_or_frame_removal */);
-    }
-    NavigationEntryImpl::TreeNode* parent_node = node->parent;
-    auto it =
-        std::ranges::find(parent_node->children, node,
-                          &std::unique_ptr<NavigationEntryImpl::TreeNode>::get);
-    CHECK(it != parent_node->children.end());
-    parent_node->children.erase(it);
+    RemoveTreeNodeFromParent(node);
   }
 }
 
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in NavigationEntryImpl via duplicate frame unique_names

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can supply duplicate unique_names for a parent and child frame, bypassing production DCHECK validations. During a frame replacement navigation, a BFS lookup erroneously matches the parent instead of the child, causing the parent’s history node to be prematurely freed. The browser then dereferences a dangling raw pointer to the freed parent node, resulting in a highly exploitable Use-After-Free.

Affected files:

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/navigation_entry_impl.cc

Estimated timestamp from git blame: 2025-11-19

Description

There is a potential Use-After-Free (UAF) vulnerability in the Browser process within content::NavigationEntryImpl. The root cause stems from insufficient browser-side validation of a frame’s unique_name. RenderFrameHostImpl relies on a DCHECK to ensure unique_name is not empty, but it never validates that the name is globally unique within the frame tree.

Because of this, a compromised renderer can create a frame tree where a parent frame and its child frame share the exact same unique_name. This breaks the assumption of NavigationEntryImpl::GetTreeNode, which relies on Breadth-First Search (BFS) and unique_name matching to locate a specific frame’s TreeNode in the session history tree.

When a navigation with UpdatePolicy::kReplace commits in the child frame, the browser attempts to remove the child’s old history entry. However, because BFS visits the parent first and both frames share the same name, the search returns the parent’s TreeNode. The browser subsequently deletes the parent node. Crucially, the calling function (AddOrUpdateFrameEntry) still holds a raw stack pointer to this parent node and subsequently uses it, triggering a Use-After-Free.

Note: The following exploitation steps are potential and based on static analysis, as our setup does not currently run active proof-of-concept exploits.

Potential Steps to Trigger

  1. Compromise a Renderer: An attacker gains arbitrary code execution in a renderer process.
  2. Create Duplicate Names: The renderer sends IPC messages to create a subframe (Frame A) with unique_name = “duplicate_name”. It then creates a child of Frame A (Frame B) also with unique_name = “duplicate_name”.
  3. Populate History: The attacker triggers normal navigations in both frames so that NavigationEntryImpl creates a TreeNode for both A and B.
  4. Trigger Replacement: The attacker triggers a navigation in Frame B (the child) using location.replace(), generating a commit IPC with UpdatePolicy::kReplace and a crafted navigation_api_key string payload.

Code Walkthrough

When the replacement navigation for Frame B commits, NavigationEntryImpl::AddOrUpdateFrameEntry is called (content/browser/renderer_host/navigation_entry_impl.cc:1106).

  1. Fetch Parent Node: At line 1173, GetTreeNode(Frame B's parent) correctly returns Frame A’s TreeNode. This is stored in NavigationEntryImpl::TreeNode* parent_node, a raw pointer.
  2. Initiate Removal: At line 1182, it loops through parent_node->children and finds the entry for Frame B. Because update_policy == UpdatePolicy::kReplace, it calls RemoveEntryForFrame(frame_tree_node, false) (line 1186).
  3. The BFS Flaw: Inside RemoveEntryForFrame, GetTreeNode(Frame B) is called. GetTreeNode uses BFS. Since Frame A (parent) is higher in the tree than Frame B (child), BFS evaluates Frame A first. Because the attacker explicitly set Frame A and Frame B to have the same unique_name, TreeNode::MatchesFrame erroneously returns true for Frame A.
  4. Erroneous Deletion: GetTreeNode returns Frame A’s TreeNode instead of Frame B’s. RemoveEntryForFrame proceeds to delete Frame A’s TreeNode from the root tree (line 1314). Frame A’s memory is freed.
  5. Use-After-Free: Control returns to AddOrUpdateFrameEntry. The parent_node stack variable is now dangling. At line 1211, a new FrameNavigationEntry is instantiated. This constructor copies the attacker-controlled navigation_api_key string, allocating heap memory. The attacker can carefully size this string to reclaim the exact memory chunk just freed by Frame A’s TreeNode, forging the internal std::vector members (begin, end, capacity).
  6. Arbitrary Write: At line 1217, the code executes parent_node->children.push_back(...). This operates on the attacker’s forged std::vector inside the reclaimed memory chunk, providing a highly reliable arbitrary memory write primitive in the Browser process.

Impact

This provides a deterministic arbitrary memory write in the Browser process. An attacker can overwrite a function pointer (e.g., a vtable entry) to hijack control flow, leading to a full Sandbox Escape and Remote Code Execution (RCE).

Suggested Fix

  1. Strict Name Validation: Enforce strict browser-side validation in RenderFrameHostImpl::CreateChildFrame to ensure that frame_unique_name is globally unique across the entire FrameTree before accepting the IPC. If a duplicate is detected, terminate the renderer process via bad_message::ReceivedBadMessage.
  2. Safe Pointers: Convert the raw NavigationEntryImpl::TreeNode* parent_node pointer in AddOrUpdateFrameEntry (and similar navigation classes) to a safe reference (e.g., relying on robust identifiers rather than raw tree node pointers, or employing base::SafeRef / checking tree validity) so that destruction invalidates the usage.
  3. Robust Node Lookup: Update RemoveEntryForFrame to unconditionally verify the tree position of the returned node (removing the !only_if_different_position short-circuit) to ensure the deleted node actually corresponds to the correct FrameTreeNode topology.

Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker