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 <[email protected]> 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 <[email protected]> Reviewed-by: Bo Liu <[email protected]> Reviewed-by: Calder Kitagawa <[email protected]> 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",