Medium watch CVSS 6.5 chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse of released resource in Mobile
DescriptionUse of released resource in Mobile
ComponentMobile
Bug ClassLogic Error
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.
Tracker517482830
Fix commita9de56747eeb (chromium/src) +384/-200
CWECWE-672
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-13 (public git)
Disclosed2026-09-03
Patch gapNot computed: several commits cite this bug and it is not yet established which one is the fix.

Background

Android context menu flow
When the user long-presses a link/image in WebView or Chrome for Android, the browser builds a Java ContextMenuParams from the C++ content::ContextMenuParams plus initiator-frame identity, and later uses it to start a navigation.
`AdditionalNavigationParams`
a Java object that carries the initiator frame’s identity (frame token, process id, attribution token) and a pointer to native state, consumed later by NavigationController::LoadUrl.
`InitiatorNavigationState`
the browser-side, ref-counted native state (e.g. the frame’s PolicyContainerHost) that must outlive the context menu so the eventual navigation is attributed to the correct, still-valid initiator.
Use of released resource
dereferencing native/browser state after the owning object (here, the initiator RenderFrameHost) has been destroyed.

Root Cause Analysis

Pre-patch, BuildJavaContextMenuParams received only a ChildProcessId and an optional initiator_frame_token — bare identifiers with no ownership of the initiator frame’s backing state — and built AdditionalNavigationParams from those raw values. Because the context menu holds these params across an arbitrary user-driven delay before the navigation is issued, the initiator frame (for example a subframe) can be detached and its InitiatorNavigationState/PolicyContainerHost freed in the interim, so consuming the stored identifiers later reaches released browser-side resources.

The fix changes BuildJavaContextMenuParams to take the live content::RenderFrameHost& and route it through the new CreateJavaAdditionalNavigationParams, which calls RenderFrameHostImpl::CreateInitiatorStateFromCurrentFrame() and AddRef()s the resulting InitiatorNavigationState, embedding a keep-alive pointer in the Java object so the state survives frame detachment (as the shipped browser test asserts). Complementing the lifetime fix, ContextMenuParams.destroy() is added and invoked on every teardown/early-exit path (AwContextMenuCoordinator.dismiss, AwContextMenuHelper, ContextMenuHelper) — guarded by the new mDismissed flag — to Release() the held native state exactly once and avoid leaking or double-freeing it.

Key insight
The core mistake was carrying the initiator frame only by raw identifier while holding it across a delay in which the frame can be destroyed, so later consumption touched a released resource; the fix takes a ref-counted keep-alive on the frame’s InitiatorNavigationState at build time and adds matching destroy()/Release() calls on all dismissal paths.

Attack Path

  1. Open a context menu on a subframe The user (or attacker-influenced page) triggers a context menu whose initiator is a cross-site subframe, causing BuildJavaContextMenuParams to capture the initiator identity into AdditionalNavigationParams.
  2. Detach the initiator frame during the delay While the menu (and its stored params) is still alive, the page removes/navigates the subframe (document.querySelector('iframe').remove() in the regression test), destroying the initiator RenderFrameHost and, pre-patch, its backing InitiatorNavigationState.
  3. Consume the stale params A subsequent context-menu navigation consumes the stored identifiers via NavigationController::LoadUrl, reaching the released initiator state (use of released resource) on the pre-patch path.
  4. Memory corruption / crash The dangling access yields a browser-process use-after-free; escalation beyond the crash is (inferred).

Impact Assessment

The bug lets a page with a detachable initiator frame cause the browser process to consume released navigation-initiator state during an Android context-menu navigation, a use-after-free of browser-side resources. The advisory classifies it as “Use of released resource in Mobile” and does not report in-the-wild exploitation; any escalation from the UAF to controlled read/write or code execution is (inferred). Preconditions are an Android/WebView build and a context-menu navigation whose initiator frame is torn down before the navigation is consumed.

Changed Functions

FunctionChangeNotes
if
android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java
modified
if
chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java
modified
if
components/embedder_support/android/contextmenu/context_menu_builder.cc
modified
RenderFrameHost
components/embedder_support/android/contextmenu/context_menu_builder.h
modified
if
components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java
modified
if
content/browser/android/additional_navigation_params.cc
modified

Files Changed

  • content/public/android/BUILD.gn
  • content/browser/BUILD.gn
  • content/browser/android/additional_navigation_params.h
  • android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java
  • android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.java
  • chrome/browser/ui/android/context_menu_helper.cc
  • content/test/BUILD.gn
  • components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java
  • content/public/browser/android/additional_navigation_params_android.h
  • chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java
  • content/public/android/java/src/org/chromium/content_public/browser/AdditionalNavigationParams.java
  • content/browser/renderer_host/navigation_controller_android.cc
  • chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
  • components/embedder_support/android/contextmenu/context_menu_builder.h
  • components/embedder_support/android/contextmenu/context_menu_builder.cc
  • content/browser/android/additional_navigation_params.cc
  • content/browser/android/additional_navigation_params_browsertest.cc
  • content/public/android/java/src/org/chromium/content/browser/AdditionalNavigationParamsUtils.java
  • content/browser/android/additional_navigation_params_utils.cc
  • android_webview/browser/aw_context_menu_helper.cc
  • content/public/android/java/src/org/chromium/content/browser/AdditionalNavigationParamsImpl.java

Audit Directions

  • Identity-by-token held across a delay
    Look for browser-side objects that store a frame/process token to resolve later; verify the referenced object is kept alive (or the lookup handles a destroyed frame) across every asynchronous gap.
  • Missing release on early-exit paths
    Audit newly added destroy()/Release() helpers to confirm they are called on all dismissal, error, and early-return branches, not just the happy path.
  • Idempotent teardown
    Flag teardown methods that can run twice (dismiss + destroy) without a guard like mDismissed, which can double-release or double-free ref-counted native handles.
From a9de56747eeb6b737003a1c4d1ef5bbca617ba69 Mon Sep 17 00:00:00 2001
From: Jenna Himawan <jhimawan@google.com>
Date: Mon, 13 Jul 2026 12:45:59 -0700
Subject: [PATCH] Plumb InitiatorNavigationState for Android context menu navigations

Bypass-Check-License: Moving files, not creating new files
Bug: 517482830
Change-Id: I7cfa48e6d171461c5ad49afd3b610ce94d575962
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8001092
Commit-Queue: Jenna Himawan <jhimawan@google.com>
Reviewed-by: Bo Liu <boliu@chromium.org>
Reviewed-by: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1661311}
---
diff --git a/android_webview/browser/aw_context_menu_helper.cc b/android_webview/browser/aw_context_menu_helper.cc
index a8c4c0d..e8cc7148 100644
--- a/android_webview/browser/aw_context_menu_helper.cc
+++ b/android_webview/browser/aw_context_menu_helper.cc
@@ -6,7 +6,7 @@
 
 #include "android_webview/browser_jni_headers/AwContextMenuHelper_jni.h"
 #include "components/embedder_support/android/contextmenu/context_menu_builder.h"
-#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/render_frame_host.h"
 #include "ui/android/view_android.h"
 
 using base::android::JavaRef;
@@ -33,9 +33,8 @@
   gfx::NativeView view = GetWebContents().GetNativeView();
   Java_AwContextMenuHelper_showContextMenu(
       env, java_obj_,
-      context_menu::BuildJavaContextMenuParams(
-          params, nullptr, render_frame_host.GetProcess()->GetID(),
-          render_frame_host.GetFrameToken().value()),
+      context_menu::BuildJavaContextMenuParams(params, nullptr,
+                                               render_frame_host),
       view->GetContainerView());
 }
 
diff --git a/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java b/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java
index c3520c0e..adef660 100644
--- a/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java
+++ b/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java
@@ -68,6 +68,7 @@
     private WebContentsObserver mWebContentsObserver;
     private final boolean mIsDragDropEnabled;
     private final boolean mUsePopupWindow;
+    private boolean mDismissed;
 
     AwContextMenuCoordinator(
             WindowAndroid windowAndroid,
@@ -97,6 +98,9 @@
     }
 
     public void dismiss() {
+        if (mDismissed) return;
+        mDismissed = true;
+
         if (mWebContentsObserver != null) {
             mWebContentsObserver.observe(null);
         }
@@ -115,6 +119,8 @@
             mCurrentPopulator.onMenuClosed();
             mCurrentPopulator = null;
         }
+
+        mParams.destroy();
     }
 
     void displayMenu() {
diff --git a/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.java b/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.java
index c4c61e2..591f796 100644
--- a/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.java
+++ b/android_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.java
@@ -64,6 +64,7 @@
                 || windowAndroid.getActivity().get() == null
                 || windowAndroid.getContext().get() == null) {
             Log.w(TAG, "Could not create context menu");
+            params.destroy();
             return false;
         }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java
index 0e42eb0..84bb744d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java
@@ -85,6 +85,8 @@
         dismissContextMenu();
         if (mCurrentNativeDelegate != null) mCurrentNativeDelegate.destroy();
         if (mPopulatorFactory != null) mPopulatorFactory.onDestroy();
+        destroyContextMenuParams(mCurrentContextMenuParams);
+        mCurrentContextMenuParams = null;
         var removedValue = sContextMenuHelperMap.remove(mNativeContextMenuHelper);
         assert removedValue == this;
         mNativeContextMenuHelper = 0;
@@ -94,6 +96,8 @@
     private void setPopulatorFactory(ContextMenuPopulatorFactory populatorFactory) {
         dismissContextMenu();
         if (mCurrentNativeDelegate != null) mCurrentNativeDelegate.destroy();
+        destroyContextMenuParams(mCurrentContextMenuParams);
+        mCurrentContextMenuParams = null;
         mCurrentPopulator = null;
         if (mPopulatorFactory != null) mPopulatorFactory.onDestroy();
         mPopulatorFactory = populatorFactory;
@@ -101,6 +105,7 @@
 
     /**
      * Starts showing a context menu for {@code view} based on {@code params}.
+     *
      * @param params The {@link ContextMenuParams} that indicate what menu items to show.
      * @param renderFrameHost {@link RenderFrameHost} to get the encoded images from.
      * @param view container view for the menu.
@@ -112,7 +117,10 @@
             RenderFrameHost renderFrameHost,
             View view,
             float topContentOffsetPx) {
-        if (params.isFile()) return;
+        if (params.isFile()) {
+            destroyContextMenuParams(params);
+            return;
+        }
 
         final WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
 
@@ -124,6 +132,7 @@
                 || mPopulatorFactory == null
                 || !mPopulatorFactory.isEnabled()
                 || mCurrentContextMenu != null) {
+            destroyContextMenuParams(params);
             return;
         }
 
@@ -162,6 +171,8 @@
                         // Has no effect if the classification already succeeded.
                         mChipDelegate.onMenuClosed();
                     }
+                    destroyContextMenuParams(mCurrentContextMenuParams);
+                    mCurrentContextMenuParams = null;
                     if (mNativeContextMenuHelper == 0) return;
                     ContextMenuHelperJni.get().onContextMenuClosed(mNativeContextMenuHelper);
                 };
@@ -263,6 +274,12 @@
         return assertNonNull(sContextMenuHelperMap.get(nativeContextMenuHelper));
     }
 
+    private static void destroyContextMenuParams(@Nullable ContextMenuParams params) {
+        if (params != null) {
+            params.destroy();
+        }
+    }
+
     @NativeMethods
     interface Natives {
         void onContextMenuClosed(long nativeContextMenuHelper);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
index 03cb17b..c812319 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.java
@@ -9,7 +9,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
 import android.content.Context;
@@ -195,7 +194,6 @@
         AdditionalNavigationParams navigationParams =
                 sOpenNewTabLoadUrlParams.getAdditionalNavigationParams();
         assertNotNull(navigationParams);
-        assertTrue(navigationParams.getInitiatorProcessId().isValid());
     }
 
     /**
diff --git a/chrome/browser/ui/android/context_menu_helper.cc b/chrome/browser/ui/android/context_menu_helper.cc
index 2d50e5fc..2205a9b 100644
--- a/chrome/browser/ui/android/context_menu_helper.cc
+++ b/chrome/browser/ui/android/context_menu_helper.cc
@@ -16,7 +16,6 @@
 #include "components/embedder_support/android/contextmenu/context_menu_builder.h"
 #include "content/public/browser/context_menu_params.h"
 #include "content/public/browser/render_frame_host.h"
-#include "content/public/browser/render_process_host.h"
 #include "extensions/buildflags/buildflags.h"
 #include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h"
 #include "ui/android/view_android.h"
@@ -72,10 +71,8 @@
 
   Java_ContextMenuHelper_showContextMenu(
       env, GetJavaObject(env),
-      context_menu::BuildJavaContextMenuParams(
-          context_menu_params_, model_ptr,
-          render_frame_host.GetProcess()->GetID(),
-          render_frame_host.GetFrameToken().value()),
+      context_menu::BuildJavaContextMenuParams(context_menu_params_, model_ptr,
+                                               render_frame_host),
       render_frame_host.GetJavaRenderFrameHost(), view->GetContainerView(),
       view->content_offset() * view->GetDipScale());
 }
diff --git a/components/embedder_support/android/contextmenu/context_menu_builder.cc b/components/embedder_support/android/contextmenu/context_menu_builder.cc
index 4d81529..6ca6676a 100644
--- a/components/embedder_support/android/contextmenu/context_menu_builder.cc
+++ b/components/embedder_support/android/contextmenu/context_menu_builder.cc
@@ -7,10 +7,12 @@
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
 #include "base/android/unguessable_token_android.h"
+#include "base/check_op.h"
 #include "base/unguessable_token.h"
 #include "content/public/browser/android/additional_navigation_params_android.h"
 #include "content/public/browser/android/impression_android.h"
 #include "content/public/browser/context_menu_params.h"
+#include "content/public/browser/render_frame_host.h"
 #include "third_party/blink/public/common/context_menu_data/context_menu_data.h"
 #include "third_party/blink/public/mojom/annotation/annotation.mojom-shared.h"
 #include "ui/menus/android/menu_model_bridge.h"
@@ -26,8 +28,7 @@
 base::android::ScopedJavaGlobalRef<jobject> BuildJavaContextMenuParams(
     const content::ContextMenuParams& params,
     ui::MenuModel* menu_model,
-    content::ChildProcessId initiator_process_id,
-    std::optional<base::UnguessableToken> initiator_frame_token) {
+    content::RenderFrameHost& initiator_frame_host) {
   GURL sanitizedReferrer =
       (params.frame_url.is_empty() ? params.page_url : params.frame_url)
           .GetAsReferrer();
@@ -37,18 +38,13 @@
   std::u16string title_text =
       (params.title_text.empty() ? params.alt_text : params.title_text);
 
-  std::optional<base::UnguessableToken> attribution_src_token;
-  if (initiator_frame_token && params.impression) {
-    attribution_src_token = params.impression->attribution_src_token.value();
-  }
-
-  base::android::ScopedJavaLocalRef<jobject> additional_navigation_params;
-  if (initiator_frame_token) {
-    additional_navigation_params =
-        content::CreateJavaAdditionalNavigationParams(
-            env, initiator_frame_token.value(), initiator_process_id,
-            attribution_src_token);
-  }
+  std::optional<base::UnguessableToken> attribution_src_token =
+      params.impression
+          ? std::make_optional(params.impression->attribution_src_token.value())
+          : std::nullopt;
+  base::android::ScopedJavaLocalRef<jobject> additional_navigation_params =
+      content::CreateJavaAdditionalNavigationParams(env, initiator_frame_host,
+                                                    attribution_src_token);
 
   ui::MenuModelBridge* menu_model_bridge =
       new ui::MenuModelBridge(menu_model ? menu_model->AsWeakPtr() : nullptr);
diff --git a/components/embedder_support/android/contextmenu/context_menu_builder.h b/components/embedder_support/android/contextmenu/context_menu_builder.h
index 2bbdac3..364ca7b 100644
--- a/components/embedder_support/android/contextmenu/context_menu_builder.h
+++ b/components/embedder_support/android/contextmenu/context_menu_builder.h
@@ -5,24 +5,21 @@
 #ifndef COMPONENTS_EMBEDDER_SUPPORT_ANDROID_CONTEXTMENU_CONTEXT_MENU_BUILDER_H_
 #define COMPONENTS_EMBEDDER_SUPPORT_ANDROID_CONTEXTMENU_CONTEXT_MENU_BUILDER_H_
 
-#include <optional>
-
 #include "base/android/scoped_java_ref.h"
-#include "base/unguessable_token.h"
-#include "content/public/common/child_process_id.h"
 #include "ui/menus/android/menu_model_bridge.h"
 
 namespace content {
 struct ContextMenuParams;
-}
+class RenderFrameHost;
+}  // namespace content
 
 namespace context_menu {
 
+// Builds Java ContextMenuParams.
 base::android::ScopedJavaGlobalRef<jobject> BuildJavaContextMenuParams(
     const content::ContextMenuParams& params,
     ui::MenuModel* menu_model,
-    content::ChildProcessId initiator_process_id,
-    std::optional<base::UnguessableToken> initiator_frame_token);
+    content::RenderFrameHost& initiator_frame_host);
 
 content::ContextMenuParams* ContextMenuParamsFromJavaObject(
     const base::android::JavaRef<jobject>& jcontext_menu_params);
diff --git a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java
index 5d67e89..ed2b2031 100644
--- a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java
+++ b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java
@@ -215,6 +215,13 @@
         return mAdditionalNavigationParams;
     }
 
+    /** Releases resources associated with this Context Menu. */
+    public void destroy() {
+        if (mAdditionalNavigationParams != null) {
+            mAdditionalNavigationParams.destroy();
+        }
+    }
+
     /** Returns the {@link MenuModelBridge} associated with this context menu. */
     public MenuModelBridge getMenuModelBridge() {
         return mMenuModelBridge;
diff --git a/content/browser/android/additional_navigation_params.cc b/content/browser/android/additional_navigation_params.cc
new file mode 100644
index 0000000..03bbb8916
--- /dev/null
+++ b/content/browser/android/additional_navigation_params.cc
@@ -0,0 +1,112 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/android/additional_navigation_params.h"
+
+#include "base/android/jni_string.h"
+#include "base/android/unguessable_token_android.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/common/android/child_process_id.h"
+#include "content/public/android/content_jni_headers/AdditionalNavigationParamsImpl_jni.h"
+#include "content/public/browser/initiator_navigation_state.h"
+
+namespace content {
+
+base::android::ScopedJavaLocalRef<jobject> CreateJavaAdditionalNavigationParams(
+    JNIEnv* env,
+    RenderFrameHost& initiator_frame_host,
+    std::optional<base::UnguessableToken> attribution_src_token) {
+  auto* rfhi = static_cast<RenderFrameHostImpl*>(&initiator_frame_host);
+  scoped_refptr<InitiatorNavigationState> initiator_navigation_state =
+      rfhi->CreateInitiatorStateFromCurrentFrame();
+  int64_t native_state_ptr = 0;
+  if (initiator_navigation_state) {
+    initiator_navigation_state->AddRef();
+    native_state_ptr =
+        reinterpret_cast<int64_t>(initiator_navigation_state.get());
+  }
+  return Java_AdditionalNavigationParamsImpl_Constructor(
+      env, initiator_frame_host.GetFrameToken().value(),
+      initiator_frame_host.GetProcess()->GetID(), attribution_src_token,
+      native_state_ptr);
+}
+
+void JNI_AdditionalNavigationParamsImpl_Destroy(JNIEnv* env,
+                                                int64_t native_state) {
+  if (native_state) {
+    auto* state = reinterpret_cast<InitiatorNavigationState*>(native_state);
+    state->Release();
+  }
+}
+
+std::optional<blink::LocalFrameToken>
+GetInitiatorFrameTokenFromJavaAdditionalNavigationParams(
+    JNIEnv* env,
+    const base::android::JavaRef<jobject>& j_object) {
+  if (!j_object) {
+    return std::nullopt;
+  }
+  std::optional<base::UnguessableToken> optional_token =
+      Java_AdditionalNavigationParamsImpl_getInitiatorFrameToken(env, j_object);
+  if (optional_token) {
+    return blink::LocalFrameToken(optional_token.value());
+  }
+  return std::nullopt;
+}
+
+content::ChildProcessId GetInitiatorProcessIdFromJavaAdditionalNavigationParams(
+    JNIEnv* env,
+    const base::android::JavaRef<jobject>& j_object) {
+  if (!j_object) {
+    return content::ChildProcessId();
+  }
+  return Java_AdditionalNavigationParamsImpl_getInitiatorProcessId(env,
+                                                                   j_object);
+}
+
+std::optional<blink::AttributionSrcToken>
+GetAttributionSrcTokenFromJavaAdditionalNavigationParams(
+    JNIEnv* env,
+    const base::android::JavaRef<jobject>& j_object) {
+  if (!j_object) {
+    return std::nullopt;
+  }
+  std::optional<base::UnguessableToken> optional_token =
+      Java_AdditionalNavigationParamsImpl_getAttributionSrcToken(env, j_object);
+  if (optional_token) {
+    return blink::AttributionSrcToken(optional_token.value());
+  }
+  return std::nullopt;
+}
+
+scoped_refptr<InitiatorNavigationState>
+TakeNativeStateFromJavaAdditionalNavigationParams(
+    JNIEnv* env,
+    const base::android::JavaRef<jobject>& j_object) {
+  if (!j_object) {
+    return nullptr;
+  }
+  int64_t native_ptr =
+      Java_AdditionalNavigationParamsImpl_takeNativeState(env, j_object);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/android/additional_navigation_params_browsertest.cc b/content/browser/android/additional_navigation_params_browsertest.cc
new file mode 100644
index 0000000..afad9807
--- /dev/null
+++ b/content/browser/android/additional_navigation_params_browsertest.cc
@@ -0,0 +1,98 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/android/additional_navigation_params.h"
+
+#include "base/android/jni_android.h"
+#include "base/functional/callback_helpers.h"
+#include "base/run_loop.h"
+#include "base/test/run_until.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/android/additional_navigation_params_android.h"
+#include "content/public/browser/initiator_navigation_state.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/common/child_process_id.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/test_utils.h"
+#include "content/shell/browser/shell.h"
+#include "net/dns/mock_host_resolver.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
+#include "url/gurl.h"
+
+namespace content {
+
+class AdditionalNavigationParamsBrowserTest : public ContentBrowserTest {
+ public:
+  void SetUpOnMainThread() override {
+    host_resolver()->AddRule("*", "127.0.0.1");
+    ASSERT_TRUE(embedded_test_server()->Start());
+    ContentBrowserTest::SetUpOnMainThread();
+  }
+
+  WebContents* web_contents() { return shell()->web_contents(); }
+};
+
+// AdditionalNavigationParams carries the initiator frame token across an
+// arbitrary delay before it is consumed by NavigationController::LoadUrl. The
+// initiator frame may be detached during that delay, so creating the params
+// must keep the frame's PolicyContainerHost reachable for the lifetime of the
+// returned keep-alive handle.
+IN_PROC_BROWSER_TEST_F(AdditionalNavigationParamsBrowserTest,
+                       InitiatorPolicyContainerHostKeptAliveAfterFrameDetach) {
+  ASSERT_TRUE(NavigateToURL(
+      shell(), embedded_test_server()->GetURL(
+                   "a.com", "/cross_site_iframe_factory.html?a.com(b.com)")));
+
+  RenderFrameHostImpl* child_rfh =
+      static_cast<RenderFrameHostImpl*>(ChildFrameAt(web_contents(), 0));
+  ASSERT_TRUE(child_rfh);
+
+  const blink::LocalFrameToken child_token = child_rfh->GetFrameToken();
+  const ChildProcessId child_process_id = child_rfh->GetProcess()->GetID();
+
+  base::android::ScopedJavaLocalRef<jobject> java_params =
+      CreateJavaAdditionalNavigationParams(
+          base::android::AttachCurrentThread(), *child_rfh,
+          /*attribution_src_token=*/std::nullopt);
+  ASSERT_TRUE(java_params);
+
+  // Detach the initiator frame while still holding the params.
+  RenderFrameDeletedObserver deleted_observer(child_rfh);
+  ASSERT_TRUE(
+      ExecJs(web_contents(), "document.querySelector('iframe').remove();"));
+  ASSERT_TRUE(deleted_observer.WaitUntilDeleted());
+  ASSERT_FALSE(
+      RenderFrameHostImpl::FromFrameToken(child_process_id, child_token));
+
+  // Verify round-trip extraction of frame token and process ID even after frame
+  // detachment.
+  std::optional<blink::LocalFrameToken> extracted_frame_token =
+      GetInitiatorFrameTokenFromJavaAdditionalNavigationParams(
+          base::android::AttachCurrentThread(), java_params);
+  EXPECT_TRUE(extracted_frame_token.has_value());
+  EXPECT_EQ(extracted_frame_token.value(), child_token);
+
+  ChildProcessId extracted_process_id =
+      GetInitiatorProcessIdFromJavaAdditionalNavigationParams(
+          base::android::AttachCurrentThread(), java_params);
+  EXPECT_FALSE(extracted_process_id.is_null());
+  EXPECT_EQ(extracted_process_id, child_process_id);
+
+  // We can get the state from the Java object.
+  scoped_refptr<InitiatorNavigationState> taken_state =
+      TakeNativeStateFromJavaAdditionalNavigationParams(
+          base::android::AttachCurrentThread(), java_params);
+  EXPECT_TRUE(taken_state);
+
+  // Clean up the Java object to satisfy LifetimeAssert and release native
+  // state.
+  DestroyJavaAdditionalNavigationParams(base::android::AttachCurrentThread(),
+                                        java_params);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index a2b70da..6dfdf63 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2299,6 +2299,7 @@
     sources += [
       "../browser/accessibility/android_granularity_movement_browsertest.cc",
       "../browser/accessibility/ime_accessibility_browsertest.cc",
+      "../browser/android/additional_navigation_params_browsertest.cc",
       "../browser/android/nfc_host_browsertest.cc",
       "../browser/android/render_widget_host_connector_browsertest.cc",
       "../browser/android/render_widget_host_connector_browsertest.h",
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.