CVE-2026-87463
Overview
Background
- Multi-network CCT
- Chrome Custom Tabs feature (Android) where a bound
WebContentsmust send its network requests over a specific target network (stored inWebContents::GetTargetNetwork), e.g. captive-portal login. - `URLLoaderFactoryBuilder` / `WillCreateURLLoaderFactory`
- the builder that assembles the chain of
URLLoaderFactoryobjects for a request context, and theContentBrowserClienthook where the browser can inject interceptors/overrides before the factory is finalized. - `MaybeProxyNetworkBoundRequest` (old) / `MaybeSetTargetNetwork` (new)
- the
ChromeContentBrowserClientmethod that binds requests tobound_network; the old form created a separatenetwork_bound_network_context_, the new form setstarget_networkon the builder. - `is_for_network_service` / `RequiresFreshFactory`
- a new parameter telling the client whether the factory being built is actually backed by the network service, and a new builder query for whether a fresh factory (rather than a shared one) must be created to honor the target network.
Root Cause Analysis
To bind CCT requests to a target network, ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest created and cached a separate network_bound_network_context_ and spliced a proxying URLLoaderFactory (via factory_builder.Append() or a URLLoaderFactoryOverride) into whatever factory chain WillCreateURLLoaderFactory was assembling. Two problems follow from the diff. First, that separate NetworkContext was configured with a freshly minted, default cert_verifier_params — content::GetCertVerifierParams(cert_verifier::mojom::CertVerifierCreationParams::New()) — rather than the profile’s configured certificate-verification parameters, so certificate authorization for network-bound requests did not match the profile’s real policy. Second, the proxy was installed unconditionally, with no signal for whether the factory being built was even backed by the network service; target_network can only be honored by the network service, so applying it to non-network-service factories was meaningless or incorrect.
The fix replaces the separate context with factory_builder.SetTargetNetwork(bound_network), so the standard network-service-backed factory (using the storage partition’s proper NetworkContext and its correct cert verification) binds the network on demand. It gates this on a new is_for_network_service parameter threaded through WillCreateURLLoaderFactory, and uses RequiresFreshFactory() in the navigation and download loaders to build a fresh factory (instead of the browser’s shared one) when a target network is required.
NetworkContext with default certificate-verification parameters into the factory chain regardless of whether that chain was network-service-backed; the fix routes binding through the standard network-service factory via SetTargetNetwork, restoring the profile’s certificate verification and gating on is_for_network_service.Attack Path
- Launch a network-bound Custom Tab
An app starts a multi-network CCT that supplies a target network (as exercised by
testNetworkBoundCustomTabIntent, which puts aNetworkinCustomTabsIntent.EXTRA_NETWORK), so the boundWebContentshas a non-invalidbound_network. - Requests routed through the separate context
Under the old code,
MaybeProxyNetworkBoundRequestbuilds/cachesnetwork_bound_network_context_and proxies the request through it, and that context was created with defaultcert_verifier_paramsrather than the profile’s configured certificate-verification parameters. - Certificate authorization mismatch Because the network-bound context does not carry the profile’s real cert-verifier configuration, certificate verification for the bound request applies incorrect authorization state relative to a normal request in the same profile (the “incorrect authorization in Certificate” the advisory describes).
- Broadened misapplication (inferred)
Without an
is_for_network_servicesignal, the binding path could also be reached for factory chains not backed by the network service, applying network-bound handling where it does not belong; the fix’sis_for_network_servicegate removes this case.
Impact Assessment
NetworkContext that used default certificate-verification parameters instead of the profile’s configured ones, correcting certificate authorization for multi-network CCT requests and confining network binding to network-service-backed factories. The context is the browser process on Android, and the precondition is a multi-network Custom Tab bound to a target network. The advisory classifies this as incorrect authorization in Certificate; it reports no in-the-wild exploitation, and any concrete downstream security consequence of the mismatched verification (e.g., accepting a certificate that policy would reject) is (inferred) beyond what the diff shows.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified | |
get_network_bound_network_context_for_testingchrome/browser/chrome_content_browser_client.h |
modified | |
get_target_network_for_network_bound_network_context_for_testingchrome/browser/chrome_content_browser_client.h |
modified |
Files Changed
chrome/browser/multi_network_browser_test.cccontent/browser/service_worker/service_worker_client.ccservices/network/network_context_unittest.cccontent/browser/preloading/prefetch/prefetch_url_loader_interceptor_unittest.ccnet/url_request/url_request_context.hcontent/browser/loader/url_loader_factory_utils.hchrome/browser/chrome_content_browser_client.handroid_webview/browser/aw_content_browser_client.hcontent/browser/service_worker/service_worker_context_wrapper.cccontent/public/browser/content_browser_client.cccontent/browser/preloading/prefetch/nav_prefetch_browsertest.ccservices/network/test/test_network_context.hservices/network/public/cpp/url_loader_factory_builder_unittest.ccchrome/browser/OWNERScontent/browser/loader/url_loader_factory_utils.cccontent/browser/download/download_manager_impl.ccservices/network/public/cpp/url_loader_factory_builder.ccchrome/browser/chrome_content_browser_client.cccontent/test/url_loader_interceptor_browsertest.ccchrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.javachrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.javanet/url_request/url_request_context.cccontent/browser/preloading/prefetch/prefetch_test_util_internal.hservices/network/network_context.handroid_webview/browser/aw_content_browser_client.cccontent/browser/preloading/prefetch/prefetch_network_context_unittest.cccontent/browser/web_contents/web_contents_impl.hservices/network/public/cpp/url_loader_factory_builder.hcontent/browser/preloading/prefetch/prefetch_test_util_internal.cccontent/browser/loader/navigation_url_loader_impl.ccservices/network/url_loader.ccservices/network/public/mojom/network_context.mojomcontent/public/browser/content_browser_client.hchrome/test/BUILD.gnservices/network/url_loader_factory.ccchrome/browser/chrome_content_browser_client_browsertest.ccservices/network/network_context.cc
Audit Directions
- Ad-hoc `NetworkContext` creation with default security paramsLook for code that spins up its own
NetworkContext/NetworkContextParamsand passes freshly constructedCertVerifierCreationParams(or other verification/policy params) rather than deriving them from the profile viaConfigureNetworkContextParams. - Interceptors applied without a network-service backing checkAudit
WillCreateURLLoaderFactoryimplementations and factory-chain interceptors that assume the network service backs the factory; confirm they honor theis_for_network_servicesignal before applying network-only behavior. - Renderer/embedder-supplied network targetingReview paths that bind requests to an externally supplied network handle (CCT
EXTRA_NETWORK,WebContents::GetTargetNetwork) to ensure the binding uses the correctly configured factory and cannot downgrade certificate verification.
Patch
From 925f969b44885626d960df90670845f2d7a3963e Mon Sep 17 00:00:00 2001 From: Stefano Duo <[email protected]> Date: Thu, 06 Aug 2026 11:02:35 -0700 Subject: [PATCH] Refactor multi-network CCT to rely on net::UrlRequest's target_network In multi-network Chrome Custom Tabs (CCT), network requests originating from a bound WebContents must be bound to a specific target network (stored in WebContents::GetTargetNetwork). Previously, this binding was accomplished by creating and maintaining a separate, network-bound NetworkContext (network_bound_network_context_) inside ChromeContentBrowserClient, then routing requests to this separate network-context. This approach was complex and memory-intensive. This CL refactors the target network binding mechanism by plumbing a target_network parameter directly into network.mojom.URLLoaderFactoryParams, built on top of the new target_network parameter needed to create a net::URLRequest. Under this architecture, standard network factories can bind requests to a specified network directly on demand, without requiring a separate NetworkContext. The plumbing between WebContents' target_network, and network.mojom.URLLoaderFactoryParams's new target_network parameter is still implemented via WillCreateURLLoaderFactory, but it now relies on two new URLLoaderFactoryBuilder methods: 1. SetTargetNetwork - stores the target_network and later uses it when building the final URLLoaderFactory 2. RequiresFreshFactory - used to figure out whether a pre-existing shared URLLoaderFactory can be used, or a fresh one has to be created. This is necessary to target a network for navigations (and downloads). Navigations usually use the browser process shared URLLoaderFactory. This is not possible for multi-network CCT because it requires an URLLoaderFactory that target a network and the browser shared URLLoaderFactory does not. In this scenario we instead create a new URLLoaderFactory from scratch target_network can only be consumed by the network service. With that in mind, SetTargetNetwork should only be called when URLLoaderFactoryBuilder will build a fresh URLLoaderFactory within ConnectTerminal. This is enforced by: 1. Calling SetTargetNetwork only when we know the network service is going to back the URLLoaderFactory that will be created. 2. Explicitly crashing in the ConnectTerminal variants that do not connect to a URLLoaderFactory built from scratch. 3. Adding a new is_for_network_service parameter to WillCreateURLLoaderFactory Bug: 449098586, 488878730, 517776674 Change-Id: I7a2c7da0152f853c479fb0c9a2d6f5ea18abb613 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904261 Reviewed-by: Adam Rice <[email protected]> Commit-Queue: Stefano Duo <[email protected]> Reviewed-by: Victor Vianna <[email protected]> Auto-Submit: Stefano Duo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1675111} --- diff --git a/android_webview/browser/aw_content_browser_client.cc b/android_webview/browser/aw_content_browser_client.cc index 7ea6cee..80d295d 100644 --- a/android_webview/browser/aw_content_browser_client.cc +++ b/android_webview/browser/aw_content_browser_client.cc @@ -1199,7 +1199,8 @@ bool* bypass_redirect_checks, bool* disable_secure_dns, network::mojom::URLLoaderFactoryOverridePtr* factory_override, - scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) { + scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner, + bool is_for_network_service) { TRACE_EVENT0("android_webview", "AwContentBrowserClient::WillCreateURLLoaderFactory"); DCHECK_CURRENTLY_ON(BrowserThread::UI); diff --git a/android_webview/browser/aw_content_browser_client.h b/android_webview/browser/aw_content_browser_client.h index a2adea2..84c29a2 100644 --- a/android_webview/browser/aw_content_browser_client.h +++ b/android_webview/browser/aw_content_browser_client.h @@ -247,8 +247,8 @@ bool* bypass_redirect_checks, bool* disable_secure_dns, network::mojom::URLLoaderFactoryOverridePtr* factory_override, - scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) - override; + scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner, + bool is_for_network_service) override; content::ContentBrowserClient::WebSocketOptions GetWebSocketOptions( content::RenderFrameHost* frame) override; bool WillCreateRestrictedCookieManager( diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java index 31a89d5..bc516cbb 100644 --- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java @@ -901,6 +901,39 @@ @Test @SmallTest + @MinAndroidSdkLevel(Build.VERSION_CODES.M) + public void testNetworkBoundCustomTabIntent() throws Exception { + CustomTabsConnection realConnection = CustomTabsConnection.getInstance(); + CustomTabsConnection mockConnection = Mockito.spy(realConnection); + CustomTabsConnection.setInstanceForTesting(mockConnection); + + // This Network object has to be sent via an Intent extra. With that in mind, it's much + // easier to create a "real" Network object, instead of mocking it. This requires a bit of + // "magic". + long fakeNetId = 99999; + long magic = 0xcafed00dL; + long fakeNetworkHandle = (fakeNetId << 32) | magic; + android.net.Network network = android.net.Network.fromNetworkHandle(fakeNetworkHandle); + doReturn(network).when(mockConnection).extractTargetNetwork(any(), any()); + + Intent intent = + CustomTabsIntentTestUtils.createMinimalCustomTabIntent( + ApplicationProvider.getApplicationContext(), mTestPage); + intent.putExtra(CustomTabsIntent.EXTRA_NETWORK, network); + + // We need a session to make it valid. + var token = SessionHolder.getSessionHolderFromIntent(intent); + realConnection.newSession(token.getSessionAsCustomTab()); + + // Launch. It should attempt to load mTestPage but fail due to invalid network. + mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); + + Tab tab = getActivity().getActivityTab(); + assertTrue(tab.isShowingErrorPage()); + } + + @Test + @SmallTest public void testRecordRetainableSession_WithCctSession() throws Exception { Activity emptyActivity = startBlankUiTestActivity(); diff --git a/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java b/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java index 7b6dbad..3ae92cf8 100644 --- a/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java +++ b/chrome/browser/android/content/java/src/org/chromium/chrome/browser/content/WebContentsFactory.java @@ -62,8 +62,8 @@ * @param initiallyHidden Whether or not the {@link WebContents} should be initially hidden. * @param initializeRenderer Whether or not the {@link WebContents} should initialize renderer. * @param targetNetwork target bound network, also refer to the documentation of {@link - * ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest} on how to use targetNetwork at - * the native layer. + * ChromeContentBrowserClient::MaybeSetTargetNetwork} on how to use targetNetwork at the + * native layer. * @return A newly created {@link WebContents} object. */ public static WebContents createWebContents( diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index 5324106..97d767c 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -1729,74 +1729,15 @@ g_disable_advanced_protection_caching_for_tests = true; } -void ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest( - content::BrowserContext* browser_context, +void ChromeContentBrowserClient::MaybeSetTargetNetwork( net::handles::NetworkHandle bound_network, network::URLLoaderFactoryBuilder& factory_builder, - network::mojom::URLLoaderFactoryOverridePtr* factory_override, - const net::IsolationInfo& isolation_info) { - if (bound_network == net::handles::kInvalidNetworkHandle) { + bool is_for_network_service) { + if (bound_network == net::handles::kInvalidNetworkHandle || + !is_for_network_service) { return; } - - // We support one network-bound NetworkContext at most. If a new one is - // needed, make sure to clean up the previous one first. - if (bound_network != target_network_for_network_bound_network_context_) { - network_bound_network_context_ = - mojo::Remote<network::mojom::NetworkContext>(); - network::mojom::NetworkContextParamsPtr context_params = - network::mojom::NetworkContextParams::New(); - context_params->bound_network = bound_network; - context_params->cert_verifier_params = content::GetCertVerifierParams( - cert_verifier::mojom::CertVerifierCreationParams::New()); - context_params->enable_domain_reliability = false; - ConfigureNetworkContextParams( - browser_context, true, base::FilePath(), context_params.get(), - cert_verifier::mojom::CertVerifierCreationParams::New().get()); - content::CreateNetworkContextInNetworkService( - network_bound_network_context_.BindNewPipeAndPassReceiver(), - std::move(context_params)); - target_network_for_network_bound_network_context_ = bound_network; - } - - // TLDR; if `factory_override` != nullptr, this is being called for the - // creation of a 2-layer URLLoaderFactory (see - // network.mojom.URLLoaderFactoryOverride documentation). In this case, we - // want to substitute the internal (defined by - // factory_override->overriding_factory, with a URLLoaderFactory that targets - // `bound_network`. If `factory_override` == nullptr, this is a single-layer - // URLLoaderFactory. In this case, we want the last URLLoaderFactory in the - // `factory_builder` chain to be a URLLoaderFactory that targets - // `bound_network`. - mojo::PendingReceiver<network::mojom::URLLoaderFactory> proxied_receiver; - mojo::PendingRemote<network::mojom::URLLoaderFactory> bypassed_remote; - if (!factory_override) { - // Hijack the receiver end returned by network::URLLoaderFactoryBuilder. - // This will be then redirected to a network-bound URLLoaderFactory. - std::tie(proxied_receiver, bypassed_remote) = factory_builder.Append(); - } else { - // Hijack the remote end stored in network::mojom::URLLoaderFactoryOverride. - // This will be then redirected to a network-bound URLLoaderFactory. - *factory_override = network::mojom::URLLoaderFactoryOverride::New(); - proxied_receiver = - (*factory_override) - ->overriding_factory.InitWithNewPipeAndPassReceiver(); - (*factory_override)->overridden_factory_receiver = - bypassed_remote.InitWithNewPipeAndPassReceiver(); - (*factory_override)->skip_cors_enabled_scheme_check = true; - } - - // Create a network-bound URLLoaderFactory and redirect the receiver end of - // the hijacked remote to this. - network::mojom::URLLoaderFactoryParamsPtr params = - network::mojom::URLLoaderFactoryParams::New(); - params->process_id = network::OriginatingProcessId::browser(); - params->is_trusted = true; - params->isolation_info = isolation_info; - // Disable CORS wrapping, this is already handled by the caller. - params->disable_web_security = true; - network_bound_network_context_->CreateURLLoaderFactory( - std::move(proxied_receiver), std::move(params)); + factory_builder.SetTargetNetwork(bound_network); } std::unique_ptr<content::BrowserMainParts> @@ -6666,7 +6607,8 @@ bool* bypass_redirect_checks, bool* disable_secure_dns, network::mojom::URLLoaderFactoryOverridePtr* factory_override, - scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) { + scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner, + bool is_for_network_service) { #if BUILDFLAG(ENABLE_EXTENSIONS_CORE) auto* web_request_api = extensions::BrowserContextKeyedAPIFactory<extensions::WebRequestAPI>::Get( @@ -6729,12 +6671,8 @@ #endif // BUILDFLAG (ENABLE_EXTENSIONS_CORE) #endif // BUILDFLAG(ENABLE_GUEST_VIEW) - // WARNING: This must be the last interceptor in the chain as the proxying - // URLLoaderFactory installed by this needs to be the one actually sending - // packets over the network (to effectively target `bound_network`). - MaybeProxyNetworkBoundRequest( - browser_context, GetBoundNetworkFromRenderFrameHost(frame), - factory_builder, factory_override, isolation_info); + MaybeSetTargetNetwork(GetBoundNetworkFromRenderFrameHost(frame), + factory_builder, is_for_network_service); #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \ BUILDFLAG(IS_CHROMEOS) diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h index f37d9b89..8b14900 100644 --- a/chrome/browser/chrome_content_browser_client.h +++ b/chrome/browser/chrome_content_browser_client.h @@ -702,8 +702,8 @@ bool* bypass_redirect_checks, bool* disable_secure_dns, network::mojom::URLLoaderFactoryOverridePtr* factory_override, - scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner) - override; + scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner, + bool is_for_network_service) override; std::vector<std::unique_ptr<content::URLLoaderRequestInterceptor>> WillCreateURLLoaderRequestInterceptors( content::NavigationUIData* navigation_ui_data, @@ -1346,29 +1346,15 @@ std::unique_ptr<ScopedKeepAlive> keep_alive_handle); #endif - // If `bound_network` != net::base::kInvalidNetworkHandle, this will make sure - // tha the chain of URLLoaderFactories will end up with a network factory that - // knows how to target `bound_network` (see - // network.mojom.NetworkContextParams::bound_network documentation). - // WARNING: This must be the last interceptor in the chain as the proxying - // URLLoaderFactory installed by this needs to be the one actually sending - // packets over the network (to effectively target `bound_network`). - void MaybeProxyNetworkBoundRequest( - content::BrowserContext* browser_context, - net::handles::NetworkHandle bound_network, - network::URLLoaderFactoryBuilder& factory_builder, - network::mojom::URLLoaderFactoryOverridePtr* factory_override, - const net::IsolationInfo& isolation_info); - - mojo::Remote<network::mojom::NetworkContext>& - get_network_bound_network_context_for_testing() { - return network_bound_network_context_; - } - - net::handles::NetworkHandle - get_target_network_for_network_bound_network_context_for_testing() const { - return target_network_for_network_bound_network_context_; - } + // If `factory_builder` will be backed by the network service + // (`is_for_network_service` == true), and + // `bound_network` != net::handles::kInvalidNetworkHandle, this makes sure + // that the chain of URLLoaderFactories will end up with a network factory + // that knows how to target `bound_network` (see + // network.mojom.URLLoaderFactoryParams::target_network). + void MaybeSetTargetNetwork(net::handles::NetworkHandle bound_network, + network::URLLoaderFactoryBuilder& factory_builder, + bool is_for_network_service); // True if the Gaia origin should be isolated in a dedicated process. static bool DoesGaiaOriginRequireDedicatedProcess(); @@ -1421,28 +1407,6 @@ std::string GetChildProcessSuffix(int child_flags) override; #endif // BUILDFLAG(IS_MAC) - // NetworkContext used by WebContents targeting a network. Currently, due to - // the intended use-case (captive portal login over CCT), we support only a - // single additional NetworkContext: this simplifies the lifetime handling by - // *a lot*. - // Whenever a WebContent targeting a new network issues a load request, the - // previous NetworkContext will be destroyed and a new one will be created. - // This can lead to "trashing" when multiple WebContents, targeting different - // networks, are loading resources simultaneuously, as they will keep - // destroying each other's NetworkContext. This is a known limitation of the - // current design, but, as mentioned above, this should not happened in the - // intended use-case (there is no expectation that a user will connect to - // multiple captive portals at once). - mojo::Remote<network::mojom::NetworkContext> network_bound_network_context_; - - // Network to which `network_bound_network_context_` is bound to. If == - // net::handles::kInvalidNetworkHandle, `network_bound_network_context_` is - // currently in a pending remote state (i.e., no underlying NetworkContext - // exists). - net::handles::NetworkHandle - target_network_for_network_bound_network_context_ = - net::handles::kInvalidNetworkHandle; - // Tracks whether the browser was started in "minimal" mode (as opposed to // full browser mode), where most subsystems are not initialized. bool is_minimal_mode_ = false; diff --git a/content/browser/download/download_manager_impl.cc b/content/browser/download/download_manager_impl.cc index 63ded353..0fbabce 100644 --- a/content/browser/download/download_manager_impl.cc +++ b/content/browser/download/download_manager_impl.cc @@ -417,7 +417,19 @@ ukm::kInvalidSourceIdObj, factory_builder, /*header_client=*/nullptr, /*bypass_redirect_checks=*/nullptr, /*disable_secure_dns=*/nullptr, /*factory_override=*/nullptr, - /*navigation_response_task_runner=*/nullptr); + /*navigation_response_task_runner=*/nullptr, + /*is_for_network_service=*/true); + } + + if (factory_builder.RequiresFreshFactory()) { + network::mojom::URLLoaderFactoryParamsPtr params = + storage_partition->CreateURLLoaderFactoryParams(); + auto factory = + std::move(factory_builder) + .Finish<mojo::PendingRemote<network::mojom::URLLoaderFactory>>( + storage_partition->GetNetworkContext(), std::move(params)); + return std::make_unique<network::WrapperPendingSharedURLLoaderFactory>( + std::move(factory)); } return std::make_unique<network::PendingSharedURLLoaderFactoryWithBuilder>( diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc index bfd819c..80800b7 100644 --- a/content/browser/loader/navigation_url_loader_impl.cc +++ b/content/browser/loader/navigation_url_loader_impl.cc @@ -2265,7 +2265,8 @@ frame_tree_node->navigation_request()->GetNavigationId(), ukm_id, factory_builder, &header_client, bypass_redirect_checks, /*disable_secure_dns=*/nullptr, /*factory_override=*/nullptr, - GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); + GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse}), + /*is_for_network_service=*/true); auto devtools_params = devtools_instrumentation::WillCreateURLLoaderFactoryParams::ForFrame( @@ -2299,7 +2300,8 @@ std::move(cookie_overrides), network::GetNoOpNetworkRestrictionsId())); } else { - if (!devtools_cookie_overrides.empty() || !cookie_overrides.empty()) { + if (!devtools_cookie_overrides.empty() || !cookie_overrides.empty() || + factory_builder.RequiresFreshFactory()) { network::mojom::URLLoaderFactoryParamsPtr params = storage_partition->CreateURLLoaderFactoryParams(); params->devtools_cookie_setting_overrides = diff --git a/content/browser/loader/url_loader_factory_utils.cc b/content/browser/loader/url_loader_factory_utils.cc index 11fc419..9a59803 100644 --- a/content/browser/loader/url_loader_factory_utils.cc +++ b/content/browser/loader/url_loader_factory_utils.cc @@ -58,7 +58,8 @@ DisableSecureDnsOption disable_secure_dns_option, StoragePartitionImpl* storage_partition, std::optional<URLLoaderFactoryTypes> url_loader_factory, - int process_id) + int process_id, + bool is_for_network_service) : network_context_(network_context), factory_params_(std::move(factory_params)), header_client_option_(header_client_option), @@ -66,7 +67,8 @@ ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/chrome_content_browser_client_browsertest.cc b/chrome/browser/chrome_content_browser_client_browsertest.cc
index 3442710..973f934 100644
--- a/chrome/browser/chrome_content_browser_client_browsertest.cc
+++ b/chrome/browser/chrome_content_browser_client_browsertest.cc
@@ -1890,95 +1890,6 @@
};
IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
- UnboundRequestDoesNothing) {
-#if BUILDFLAG(IS_ANDROID)
- network::URLLoaderFactoryBuilder factory_builder;
- client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(),
- net::handles::kInvalidNetworkHandle,
- factory_builder, nullptr);
- EXPECT_EQ(
- client()
- ->get_target_network_for_network_bound_network_context_for_testing(),
- net::handles::kInvalidNetworkHandle);
- EXPECT_FALSE(
- client()->get_network_bound_network_context_for_testing().is_bound());
-#else // !BUILDFLAG(IS_ANDROID)
- GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
- BoundRequestCreatesNetworkContext) {
-#if BUILDFLAG(IS_ANDROID)
- constexpr net::handles::NetworkHandle network = 1;
- network::URLLoaderFactoryBuilder factory_builder;
- client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(), network,
- factory_builder, nullptr);
- EXPECT_EQ(
- client()
- ->get_target_network_for_network_bound_network_context_for_testing(),
- network);
- EXPECT_TRUE(
- client()->get_network_bound_network_context_for_testing().is_bound());
- EXPECT_TRUE(
- client()->get_network_bound_network_context_for_testing().is_connected());
- {
- base::RunLoop run_loop;
- client()
- ->get_network_bound_network_context_for_testing()
- ->GetBoundNetworkForTesting(base::BindOnce(
- [](base::OnceClosure callback,
- net::handles::NetworkHandle bound_network) {
- EXPECT_EQ(bound_network, network);
- std::move(callback).Run();
- },
- run_loop.QuitClosure()));
- run_loop.Run();
- }
-#else // !BUILDFLAG(IS_ANDROID)
- GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
- BoundRequestWithOverrideCreatesNetworkContext) {
-#if BUILDFLAG(IS_ANDROID)
- constexpr net::handles::NetworkHandle network = 1;
- network::URLLoaderFactoryBuilder factory_builder;
- network::mojom::URLLoaderFactoryOverridePtr factory_override;
- EXPECT_FALSE(factory_override);
- client()->MaybeProxyNetworkBoundRequest(browser()->GetProfile(), network,
- factory_builder, &factory_override);
- EXPECT_EQ(
- client()
- ->get_target_network_for_network_bound_network_context_for_testing(),
- network);
- EXPECT_TRUE(
- client()->get_network_bound_network_context_for_testing().is_bound());
- EXPECT_TRUE(
- client()->get_network_bound_network_context_for_testing().is_connected());
- EXPECT_TRUE(factory_override->overriding_factory);
- mojo::Remote<network::mojom::URLLoaderFactory> overridden_factory;
- overridden_factory.Bind(std::move(factory_override->overriding_factory));
- {
- base::RunLoop run_loop;
- client()
- ->get_network_bound_network_context_for_testing()
- ->GetBoundNetworkForTesting(base::BindOnce(
- [](base::OnceClosure callback,
- net::handles::NetworkHandle bound_network) {
- EXPECT_EQ(bound_network, network);
- std::move(callback).Run();
- },
- run_loop.QuitClosure()));
- run_loop.Run();
- }
-#else // !BUILDFLAG(IS_ANDROID)
- GTEST_SKIP() << "proxying bound requests is supported only on Android";
-#endif // BUILDFLAG(IS_ANDROID)
-}
-
-IN_PROC_BROWSER_TEST_F(TopChromeChromeContentBrowserClientTest,
ShouldReuseRendererWhenTopChromePagesPresent) {
const GURL top_chrome_url(chrome::kChromeUITabSearchURL);
const GURL top_chrome_url2(chrome::kChromeUIReadLaterURL);
diff --git a/chrome/browser/multi_network_browser_test.cc b/chrome/browser/multi_network_browser_test.cc
new file mode 100644
index 0000000..5fa4ae4
--- /dev/null
+++ b/chrome/browser/multi_network_browser_test.cc
@@ -0,0 +1,1144 @@
+// 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 <algorithm>
+#include <map>
+#include <vector>
+
+#include "base/command_line.h"
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "base/test/run_until.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
+#include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/tab_list/tab_list_interface.h"
+#include "chrome/test/base/chrome_test_utils.h"
+#include "chrome/test/base/platform_browser_test.h"
+#include "components/download/public/common/download_url_parameters.h"
+#include "content/public/browser/download_manager.h"
+#include "content/public/browser/download_request_utils.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
+#include "content/public/common/content_client.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/download_test_observer.h"
+#include "content/public/test/test_navigation_observer.h"
+#include "extensions/buildflags/buildflags.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
+#include "net/base/isolation_info.h"
+#include "net/base/network_handle.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/test/embedded_test_server/install_default_websocket_handlers.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/network_switches.h"
+#include "services/network/public/cpp/url_loader_factory_builder.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+#include "services/network/public/mojom/network_service.mojom.h"
+#include "services/network/public/mojom/network_service_test.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
+
+namespace {
+
+using URLLoaderFactoryType =
+ content::ContentBrowserClient::URLLoaderFactoryType;
+
+#if !BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+// URLLoaderFactories for filesystem: and data: schemes are not backed by the
+// network service. With that in mind, they should never be using the target
+// network.
+constexpr size_t kExpectedNonNetworkFactories = 2u;
+#else
+// On Desktop Android and Desktop platforms, when extensions are enabled,
+// URLLoaderFactories for chrome-extension: scheme are also not backed by the
+// network service, this means they will also not be using the target network.
+constexpr size_t kExpectedNonNetworkFactories = 3u;
+#endif
+
+class TestChromeContentBrowserClient : public ChromeContentBrowserClient {
+ public:
+ TestChromeContentBrowserClient()
+ : old_client_(content::SetBrowserClientForTesting(this)) {}
+ ~TestChromeContentBrowserClient() override {
+ content::SetBrowserClientForTesting(old_client_);
+ }
+
+ void WillCreateURLLoaderFactory(
+ content::BrowserContext* browser_context,
+ content::RenderFrameHost* frame,
+ int render_process_id,
+ URLLoaderFactoryType type,
+ const url::Origin& request_initiator,
+ const net::IsolationInfo& isolation_info,
+ std::optional<int64_t> navigation_id,
+ ukm::SourceIdObj ukm_source_id,
+ network::URLLoaderFactoryBuilder& factory_builder,
+ mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient>*
+ header_client,
+ bool* bypass_redirect_checks,
+ bool* disable_secure_dns,
+ network::mojom::URLLoaderFactoryOverridePtr* factory_override,
+ scoped_refptr<base::SequencedTaskRunner> navigation_response_task_runner,
+ bool is_for_network_service) override {
+ ChromeContentBrowserClient::WillCreateURLLoaderFactory(
+ browser_context, frame, render_process_id, type, request_initiator,
+ isolation_info, navigation_id, ukm_source_id, factory_builder,
+ header_client, bypass_redirect_checks, disable_secure_dns,
+ factory_override, navigation_response_task_runner,
+ is_for_network_service);
+ factory_bound_networks_[type].push_back(
+ factory_builder.target_network_for_testing());
+ factory_is_for_network_service_[type].push_back(is_for_network_service);
+ }
+
+ bool HasBoundNetwork(URLLoaderFactoryType type,
+ net::handles::NetworkHandle network) const {
+ return CountBoundNetwork(type, network) > 0;
+ }
+
+ size_t CountBoundNetwork(URLLoaderFactoryType type,
+ net::handles::NetworkHandle network) const {
+ auto it = factory_bound_networks_.find(type);
+ if (it != factory_bound_networks_.end()) {
+ return std::ranges::count(it->second, network);
+ }
+ return 0;
+ }
+
+ size_t CountBoundNetwork(net::handles::NetworkHandle network) const {
+ size_t count = 0;
+ for (const auto& [type, networks] : factory_bound_networks_) {
+ count += std::ranges::count(networks, network);
+ }
+ return count;
+ }
+
+ bool HasIsForNetworkService(URLLoaderFactoryType type, bool expected) const {
+ auto it = factory_is_for_network_service_.find(type);
+ if (it != factory_is_for_network_service_.end()) {
+ return std::ranges::contains(it->second, expected);
+ }
+ return false;
+ }
+
+ private:
+ raw_ptr<content::ContentBrowserClient> old_client_;
+ std::map<URLLoaderFactoryType, std::vector<net::handles::NetworkHandle>>
+ factory_bound_networks_;
+ std::map<URLLoaderFactoryType, std::vector<bool>>
+ factory_is_for_network_service_;
+};
+
+constexpr auto kAllURLLoaderFactoryTypes = std::to_array({
+ URLLoaderFactoryType::kNavigation,
+ URLLoaderFactoryType::kDownload,
+ URLLoaderFactoryType::kDocumentSubResource,
+ URLLoaderFactoryType::kWorkerMainResource,
+ URLLoaderFactoryType::kWorkerSubResource,
+ URLLoaderFactoryType::kServiceWorkerScript,
+ URLLoaderFactoryType::kServiceWorkerSubResource,
+ URLLoaderFactoryType::kPrefetch,
+ URLLoaderFactoryType::kDevTools,
+ URLLoaderFactoryType::kEarlyHints,
+});
+
+struct ExpectedFactoryCounts {
+ size_t navigation = 0;
+ size_t download = 0;
+ size_t document_subresource = 0;
+ size_t worker_main_resource = 0;
+ size_t worker_subresource = 0;
+ size_t service_worker_script = 0;
+ size_t service_worker_subresource = 0;
+ size_t prefetch = 0;
+ size_t devtools = 0;
+ size_t early_hints = 0;
+ // The number of URLLoaderFactories that are not targeting a specific network
+ // is always expected to be the ones for schemes that are not supported by the
+ // network service. There is no need to specify this in each test.
+ size_t invalid_network_handle = kExpectedNonNetworkFactories;
+
+ size_t count_for(URLLoaderFactoryType type) const {
+ switch (type) {
+ case URLLoaderFactoryType::kNavigation:
+ return navigation;
+ case URLLoaderFactoryType::kDownload:
+ return download;
+ case URLLoaderFactoryType::kDocumentSubResource:
+ return document_subresource;
+ case URLLoaderFactoryType::kWorkerMainResource:
+ return worker_main_resource;
+ case URLLoaderFactoryType::kWorkerSubResource:
+ return worker_subresource;
+ case URLLoaderFactoryType::kServiceWorkerScript:
+ return service_worker_script;
+ case URLLoaderFactoryType::kServiceWorkerSubResource:
... (truncated)