CVE-2026-85044
Overview
Background
- Android context menu flow
- When the user long-presses a link/image in WebView or Chrome for Android, the browser builds a Java
ContextMenuParamsfrom the C++content::ContextMenuParamsplus 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.
InitiatorNavigationState at build time and adds matching destroy()/Release() calls on all dismissal paths.Attack Path
- 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
BuildJavaContextMenuParamsto capture the initiator identity intoAdditionalNavigationParams. - 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 initiatorRenderFrameHostand, pre-patch, its backingInitiatorNavigationState. - 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. - Memory corruption / crash
The dangling access yields a browser-process use-after-free; escalation beyond the crash is
(inferred).
Impact Assessment
(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
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.java |
modified | |
ifchrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java |
modified | |
ifcomponents/embedder_support/android/contextmenu/context_menu_builder.cc |
modified | |
RenderFrameHostcomponents/embedder_support/android/contextmenu/context_menu_builder.h |
modified | |
ifcomponents/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.java |
modified | |
ifcontent/browser/android/additional_navigation_params.cc |
modified |
Files Changed
content/public/android/BUILD.gncontent/browser/BUILD.gncontent/browser/android/additional_navigation_params.handroid_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuCoordinator.javaandroid_webview/java/src/org/chromium/android_webview/contextmenu/AwContextMenuHelper.javachrome/browser/ui/android/context_menu_helper.cccontent/test/BUILD.gncomponents/embedder_support/android/java/src/org/chromium/components/embedder_support/contextmenu/ContextMenuParams.javacontent/public/browser/android/additional_navigation_params_android.hchrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.javacontent/public/android/java/src/org/chromium/content_public/browser/AdditionalNavigationParams.javacontent/browser/renderer_host/navigation_controller_android.ccchrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/ContextMenuLoadUrlParamsTest.javacomponents/embedder_support/android/contextmenu/context_menu_builder.hcomponents/embedder_support/android/contextmenu/context_menu_builder.cccontent/browser/android/additional_navigation_params.cccontent/browser/android/additional_navigation_params_browsertest.cccontent/public/android/java/src/org/chromium/content/browser/AdditionalNavigationParamsUtils.javacontent/browser/android/additional_navigation_params_utils.ccandroid_webview/browser/aw_context_menu_helper.cccontent/public/android/java/src/org/chromium/content/browser/AdditionalNavigationParamsImpl.java
Audit Directions
- Identity-by-token held across a delayLook 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 pathsAudit newly added
destroy()/Release()helpers to confirm they are called on all dismissal, error, and early-return branches, not just the happy path. - Idempotent teardownFlag teardown methods that can run twice (dismiss + destroy) without a guard like
mDismissed, which can double-release or double-free ref-counted native handles.
Patch
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)
Regression Test / PoC
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",