// Copyright 2014 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <stdint.h> #include <array> #include <memory> #include <optional> #include <string> #include <string_view> #include "base/command_line.h" #include "base/containers/contains.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/memory/ptr_util.h" #include "base/memory/raw_ptr.h" #include "base/metrics/statistics_recorder.h" #include "base/run_loop.h" #include "base/strings/strcat.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/task/single_thread_task_runner.h" #include "base/test/bind.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_mock_time_task_runner.h" #include "base/test/test_timeouts.h" #include "base/threading/thread_restrictions.h" #include "base/uuid.h" #include "build/build_config.h" #include "cc/test/pixel_test_utils.h" #include "components/ukm/test_ukm_recorder.h" #include "content/browser/browser_url_handler_impl.h" #include "content/browser/child_process_security_policy_impl.h" #include "content/browser/renderer_host/navigation_request.h" #include "content/browser/renderer_host/navigation_state_keep_alive.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/spare_render_process_host_manager_impl.h" #include "content/browser/site_instance_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/content_navigation_policy.h" #include "content/common/features.h" #include "content/common/frame_messages.mojom.h" #include "content/common/navigation_client.mojom-forward.h" #include "content/common/navigation_client.mojom.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_message_filter.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_url_handler.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/download_manager_delegate.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/network_service_util.h" #include "content/public/browser/page_navigator.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/storage_partition_config.h" #include "content/public/browser/web_contents.h" #include "content/public/common/content_client.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "content/public/common/result_codes.h" #include "content/public/common/url_constants.h" #include "content/public/test/back_forward_cache_util.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_content_browser_client.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/content_mock_cert_verifier.h" #include "content/public/test/download_test_observer.h" #include "content/public/test/fenced_frame_test_util.h" #include "content/public/test/hit_test_region_observer.h" #include "content/public/test/navigation_handle_observer.h" #include "content/public/test/no_renderer_crashes_assertion.h" #include "content/public/test/slow_http_response.h" #include "content/public/test/test_frame_navigation_observer.h" #include "content/public/test/test_navigation_observer.h" #include "content/public/test/test_navigation_throttle.h" #include "content/public/test/test_navigation_throttle_inserter.h" #include "content/public/test/test_utils.h" #include "content/public/test/url_loader_monitor.h" #include "content/shell/browser/shell.h" #include "content/shell/browser/shell_download_manager_delegate.h" #include "content/test/content_browser_test_utils_internal.h" #include "content/test/did_commit_navigation_interceptor.h" #include "content/test/fake_network_url_loader_factory.h" #include "content/test/render_document_feature.h" #include "content/test/task_runner_deferring_throttle.h" #include "content/test/test_render_frame_host_factory.h" #include "ipc/ipc_security_test_util.h" #include "mojo/public/cpp/bindings/pending_associated_remote.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "net/base/features.h" #include "net/base/load_flags.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/controllable_http_response.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_response.h" #include "net/test/url_request/url_request_failed_job.h" #include "services/metrics/public/cpp/ukm_source_id.h" #include "services/network/public/cpp/features.h" #include "services/network/public/cpp/web_sandbox_flags.h" #include "services/network/public/mojom/url_loader.mojom.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/loader/url_loader_throttle.h" #include "third_party/blink/public/mojom/frame/remote_frame.mojom-test-utils.h" #include "third_party/blink/public/mojom/frame/sudden_termination_disabler_type.mojom-shared.h" #include "third_party/blink/public/mojom/navigation/navigation_params.mojom-shared.h" #include "ui/base/page_transition_types.h" #include "url/gurl.h" #include "url/url_constants.h" #include "url/url_util.h" namespace content { namespace { class InterceptAndCancelDidCommitProvisionalLoad : public DidCommitNavigationInterceptor { public: explicit InterceptAndCancelDidCommitProvisionalLoad(WebContents* web_contents) : DidCommitNavigationInterceptor(web_contents) {} ~InterceptAndCancelDidCommitProvisionalLoad() override {} void Wait(size_t number_of_messages) { while (intercepted_messages_.size() < number_of_messages) { loop_ = std::make_unique<base::RunLoop>(); loop_->Run(); } } const std::vector<raw_ptr<NavigationRequest, VectorExperimental>>& intercepted_navigations() const { return intercepted_navigations_; } const std::vector<mojom::DidCommitProvisionalLoadParamsPtr>& intercepted_messages() const { return intercepted_messages_; } protected: bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr* params, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { intercepted_navigations_.push_back(navigation_request); intercepted_messages_.push_back(std::move(*params)); if (loop_) loop_->Quit(); // Do not send the message to the RenderFrameHostImpl. return false; } // Note: Do not dereference the intercepted_navigations_, they are used as // indices in the RenderFrameHostImpl and not for themselves. std::vector<raw_ptr<NavigationRequest, VectorExperimental>> intercepted_navigations_; std::vector<mojom::DidCommitProvisionalLoadParamsPtr> intercepted_messages_; std::unique_ptr<base::RunLoop> loop_; }; class RenderFrameHostImplForHistoryBackInterceptor : public RenderFrameHostImpl { public: using RenderFrameHostImpl::RenderFrameHostImpl; void GoToEntryAtOffset(int32_t offset, bool has_user_gesture, std::optional<blink::scheduler::TaskAttributionId> soft_navigation_heuristics_task_id) override { if (quit_handler_) std::move(quit_handler_).Run(); } void set_quit_handler(base::OnceClosure handler) { quit_handler_ = std::move(handler); } private: friend class RenderFrameHostFactoryForHistoryBackInterceptor; base::OnceClosure quit_handler_; }; class RenderFrameHostFactoryForHistoryBackInterceptor : public TestRenderFrameHostFactory { protected: std::unique_ptr<RenderFrameHostImpl> CreateRenderFrameHost( SiteInstance* site_instance, scoped_refptr<RenderViewHostImpl> render_view_host, RenderFrameHostDelegate* delegate, FrameTree* frame_tree, FrameTreeNode* frame_tree_node, int32_t routing_id, mojo::PendingAssociatedRemote<mojom::Frame> frame_remote, const blink::LocalFrameToken& frame_token, const blink::DocumentToken& document_token, base::UnguessableToken devtools_frame_token, bool renderer_initiated_creation, RenderFrameHostImpl::LifecycleStateImpl lifecycle_state, scoped_refptr<BrowsingContextState> browsing_context_state) override { return base::WrapUnique(new RenderFrameHostImplForHistoryBackInterceptor( site_instance, std::move(render_view_host), delegate, frame_tree, frame_tree_node, routing_id, std::move(frame_remote), frame_token, document_token, devtools_frame_token, renderer_initiated_creation, lifecycle_state, std::move(browsing_context_state), frame_tree_node->frame_owner_element_type(), frame_tree_node->parent(), frame_tree_node->fenced_frame_status())); } }; // Simulate embedders of content/ keeping track of the current visible URL using // NavigateStateChanged() and GetVisibleURL() API. class EmbedderVisibleUrlTracker : public WebContentsDelegate { public: const GURL& url() { return url_; } // WebContentsDelegate's implementation: void NavigationStateChanged(WebContents* source, InvalidateTypes changed_flags) override { if (!(changed_flags & INVALIDATE_TYPE_URL)) return; url_ = source->GetVisibleURL(); if (on_url_invalidated_) std::move(on_url_invalidated_).Run(); } void WaitUntilUrlInvalidated() { base::RunLoop loop; on_url_invalidated_ = loop.QuitClosure(); loop.Run(); } private: GURL url_; base::OnceClosure on_url_invalidated_; }; // Helper class. Immediately run a callback when a navigation starts. class DidStartNavigationCallback final : public WebContentsObserver { public: explicit DidStartNavigationCallback( WebContents* web_contents, base::OnceCallback<void(NavigationHandle*)> callback) : WebContentsObserver(web_contents), callback_(std::move(callback)) {} ~DidStartNavigationCallback() override = default; private: void DidStartNavigation(NavigationHandle* navigation_handle) override { if (callback_) std::move(callback_).Run(navigation_handle); } base::OnceCallback<void(NavigationHandle*)> callback_; }; // Helper class. Immediately run a callback when a navigation finishes. class DidFinishNavigationCallback final : public WebContentsObserver { public: explicit DidFinishNavigationCallback( WebContents* web_contents, base::OnceCallback<void(NavigationHandle*)> callback) : WebContentsObserver(web_contents), callback_(std::move(callback)) {} ~DidFinishNavigationCallback() override = default; private: void DidFinishNavigation(NavigationHandle* navigation_handle) override { if (callback_) std::move(callback_).Run(navigation_handle); } base::OnceCallback<void(NavigationHandle*)> callback_; }; const char* non_cacheable_html_response = "HTTP/1.1 200 OK\n" "cache-control: no-cache, no-store, must-revalidate\n" "content-type: text/html; charset=UTF-8\n" "\n" "HTML content."; // Insert a navigation throttle blocking every navigation in its // WillProcessResponse handler. std::unique_ptr<content::TestNavigationThrottleInserter> BlockNavigationWillProcessResponse(WebContentsImpl* web_content) { return std::make_unique<content::TestNavigationThrottleInserter>( web_content, base::BindLambdaForTesting( [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { auto throttle = std::make_unique<TestNavigationThrottle>(handle); throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE, TestNavigationThrottle::SYNCHRONOUS, NavigationThrottle::BLOCK_RESPONSE); return throttle; })); } void WaitForHistogramRecordedInChildProcess(std::string name) { base::HistogramTester histogram_tester; while (true) { if (!histogram_tester.GetAllSamples(name).empty()) { return; } // Retry fetching the histogram since it's not populated yet. content::FetchHistogramsFromChildProcesses(); base::StatisticsRecorder::ImportProvidedHistogramsSync(); base::RunLoop().RunUntilIdle(); } } } // namespace // Test about navigation. // If you don't need a custom embedded test server, please use the next class // below (NavigationBrowserTest), it will automatically start the // default server. class NavigationBaseBrowserTest : public ContentBrowserTest { public: NavigationBaseBrowserTest() {} void PreRunTestOnMainThread() override { ContentBrowserTest::PreRunTestOnMainThread(); test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); } const ukm::TestAutoSetUkmRecorder& test_ukm_recorder() const { return *test_ukm_recorder_; } protected: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); } WebContentsImpl* web_contents() const { return static_cast<WebContentsImpl*>(shell()->web_contents()); } FrameTreeNode* main_frame() { return web_contents()->GetPrimaryFrameTree().root(); } RenderFrameHostImpl* current_frame_host() { return main_frame()->current_frame_host(); } private: std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_; }; class NavigationBrowserTest : public NavigationBaseBrowserTest { protected: void SetUpOnMainThread() override { NavigationBaseBrowserTest::SetUpOnMainThread(); ASSERT_TRUE(embedded_test_server()->Start()); } }; class NavigationGoToEntryAtOffsetBrowserTest : public NavigationBrowserTest { public: void SetQuitHandlerForGoToEntryAtOffset(base::OnceClosure handler) { RenderFrameHostImplForHistoryBackInterceptor* render_frame_host = static_cast<RenderFrameHostImplForHistoryBackInterceptor*>( current_frame_host()); render_frame_host->set_quit_handler(std::move(handler)); } private: RenderFrameHostFactoryForHistoryBackInterceptor render_frame_host_factory_; }; class NetworkIsolationNavigationBrowserTest : public ContentBrowserTest { public: NetworkIsolationNavigationBrowserTest() = default; protected: void SetUpOnMainThread() override { ASSERT_TRUE(embedded_test_server()->Start()); ContentBrowserTest::SetUpOnMainThread(); } }; class NavigationBrowserTestReferrerPolicy : public NavigationBrowserTest, public ::testing::WithParamInterface<network::mojom::ReferrerPolicy> { protected: network::mojom::ReferrerPolicy GetReferrerPolicy() const { return GetParam(); } }; INSTANTIATE_TEST_SUITE_P( All, NavigationBrowserTestReferrerPolicy, ::testing::Values( network::mojom::ReferrerPolicy::kAlways, network::mojom::ReferrerPolicy::kDefault, network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade, network::mojom::ReferrerPolicy::kNever, network::mojom::ReferrerPolicy::kOrigin, network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin, network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin, network::mojom::ReferrerPolicy::kSameOrigin, network::mojom::ReferrerPolicy::kStrictOrigin)); // Ensure that browser initiated basic navigations work. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserInitiatedNavigations) { // Perform a navigation with no live renderer. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_FALSE(observer.last_initiator_origin().has_value()); EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, observer.last_initiator_process_id()); } RenderFrameHost* initial_rfh = current_frame_host(); // Perform a same site navigation. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_FALSE(observer.last_initiator_origin().has_value()); EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, observer.last_initiator_process_id()); } RenderFrameHost* second_rfh = current_frame_host(); if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument // is enabled, the navigation will result in a new RFH. EXPECT_NE(initial_rfh, second_rfh); } else { EXPECT_EQ(initial_rfh, second_rfh); } // Perform a cross-site navigation. { TestNavigationObserver observer(web_contents()); GURL url = embedded_test_server()->GetURL("foo.com", "/title3.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_FALSE(observer.last_initiator_origin().has_value()); EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, observer.last_initiator_process_id()); } // The RenderFrameHost should have changed. EXPECT_NE(second_rfh, current_frame_host()); // Check the UKM for navigation responses received. EXPECT_EQ(3u, test_ukm_recorder() .GetEntriesByName("Navigation.ReceivedResponse") .size()); } // Ensure that renderer initiated same-site navigations work. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedSameSiteNavigation) { // Perform a navigation with no live renderer. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_FALSE(observer.last_initiator_origin().has_value()); EXPECT_FALSE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, observer.last_initiator_process_id()); } RenderFrameHost* initial_rfh = current_frame_host(); auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken(); // Simulate clicking on a same-site link. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/title2.html")); EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteLink();")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_EQ(current_frame_host()->GetLastCommittedOrigin(), observer.last_initiator_origin()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { // If same-site ProactivelySwapBrowsingInstance or main-frame // RenderDocument is enabled, the navigation will result in a new RFH, so // we need to compare with |initial_rfh|. EXPECT_NE(current_frame_host(), initial_rfh); EXPECT_EQ(initial_rfh_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initial_rfh_global_token.child_id, observer.last_initiator_process_id()); } else { EXPECT_EQ(current_frame_host(), initial_rfh); EXPECT_EQ(current_frame_host()->GetFrameToken(), observer.last_initiator_frame_token().value()); EXPECT_EQ(current_frame_host()->GetProcess()->GetDeprecatedID(), observer.last_initiator_process_id()); } } RenderFrameHost* second_rfh = current_frame_host(); if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) { // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument // is enabled, the navigation will result in a new RFH. EXPECT_NE(initial_rfh, second_rfh); } else { EXPECT_EQ(initial_rfh, second_rfh); } } // Ensure that renderer initiated cross-site navigations work. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedCrossSiteNavigation) { // Perform a navigation with no live renderer. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); } RenderFrameHost* initial_rfh = current_frame_host(); url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin(); auto initial_rfh_global_token = initial_rfh->GetGlobalFrameToken(); // Simulate clicking on a cross-site link. { TestNavigationObserver observer(web_contents()); const char kReplacePortNumber[] = "setPortNumber(%d);"; uint16_t port_number = embedded_test_server()->port(); GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html"); EXPECT_EQ(true, EvalJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteLink();")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_EQ(initial_origin, observer.last_initiator_origin().value()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(initial_rfh_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initial_rfh_global_token.child_id, observer.last_initiator_process_id()); } // The RenderFrameHost should have changed unless full site isolation and // proactive BrowsingInstance swaps are both disabled. if (!AreAllSitesIsolatedForTesting() && !CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) { EXPECT_EQ(initial_rfh, current_frame_host()); } else { EXPECT_NE(initial_rfh, current_frame_host()); } } // Ensure navigation failures are handled. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FailedNavigation) { // Perform a navigation with no live renderer. { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); // Check the UKM for navigation responses received. EXPECT_EQ(1u, test_ukm_recorder() .GetEntriesByName("Navigation.ReceivedResponse") .size()); } // Now navigate to an unreachable url. { TestNavigationObserver observer(web_contents()); GURL error_url(embedded_test_server()->GetURL("/close-socket")); GetIOThreadTaskRunner({})->PostTask( FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler)); EXPECT_FALSE(NavigateToURL(shell(), error_url)); EXPECT_EQ(error_url, observer.last_navigation_url()); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType()); // No response on an unreachable URL, so the ReceivedResponse event should // not have increased. EXPECT_EQ(1u, test_ukm_recorder() .GetEntriesByName("Navigation.ReceivedResponse") .size()); } } // Ensure that browser initiated navigations to view-source URLs works. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ViewSourceNavigation_BrowserInitiated) { TestNavigationObserver observer(web_contents()); GURL url(embedded_test_server()->GetURL("/title1.html")); GURL view_source_url(content::kViewSourceScheme + std::string(":") + url.spec()); EXPECT_TRUE(NavigateToURL(shell(), view_source_url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); } // Ensure that content initiated navigations to view-sources URLs are blocked. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ViewSourceNavigation_RendererInitiated) { TestNavigationObserver observer(web_contents()); GURL kUrl(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), kUrl)); EXPECT_EQ(kUrl, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "Not allowed to load local resource: view-source:about:blank"); EXPECT_EQ(true, EvalJs(web_contents(), "clickViewSourceLink();")); ASSERT_TRUE(console_observer.Wait()); // Original page shouldn't navigate away. EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL()); EXPECT_FALSE(shell() ->web_contents() ->GetController() .GetLastCommittedEntry() ->IsViewSourceMode()); } // Ensure that content initiated navigations to googlechrome: URLs are blocked. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GoogleChromeNavigation_RendererInitiated) { TestNavigationObserver observer(web_contents()); GURL kUrl(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), kUrl)); EXPECT_EQ(kUrl, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "Not allowed to load local resource: googlechrome://"); EXPECT_EQ(true, EvalJs(web_contents(), "clickGoogleChromeLink();")); ASSERT_TRUE(console_observer.Wait()); // Original page shouldn't navigate away. EXPECT_EQ(kUrl, web_contents()->GetLastCommittedURL()); } // Ensure that closing a page by running its beforeunload handler doesn't hang // if there's an ongoing navigation. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, UnloadDuringNavigation) { WebContentsDestroyedWatcher close_observer(web_contents()); GURL url("chrome://resources/css/tabs.css"); NavigationHandleObserver handle_observer(web_contents(), url); shell()->LoadURL(url); web_contents()->DispatchBeforeUnload(false /* auto_cancel */); close_observer.Wait(); EXPECT_EQ(net::ERR_ABORTED, handle_observer.net_error_code()); } // Ensure that the referrer of a navigation is properly sanitized. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SanitizeReferrer) { const GURL kInsecureUrl(embedded_test_server()->GetURL("/title1.html")); const Referrer kSecureReferrer( GURL("https://secure-url.com"), network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade); // Navigate to an insecure url with a secure referrer with a policy of no // referrer on downgrades. The referrer url should be rewritten right away. NavigationController::LoadURLParams load_params(kInsecureUrl); load_params.referrer = kSecureReferrer; TestNavigationManager manager(web_contents(), kInsecureUrl); web_contents()->GetController().LoadURLWithParams(load_params); EXPECT_TRUE(manager.WaitForRequestStart()); // The referrer should have been sanitized. ASSERT_TRUE(main_frame()->navigation_request()); EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url); // The navigation should commit without being blocked. EXPECT_TRUE(manager.WaitForResponse()); ASSERT_TRUE(manager.WaitForNavigationFinished()); EXPECT_EQ(kInsecureUrl, web_contents()->GetLastCommittedURL()); } // Ensure the correctness of a navigation request's referrer. This is a // regression test for https://crbug.com/1004083. IN_PROC_BROWSER_TEST_P(NavigationBrowserTestReferrerPolicy, ReferrerPolicy) { const GURL kDestination(embedded_test_server()->GetURL("/title1.html")); const GURL kReferrerURL(embedded_test_server()->GetURL("/referrer-page")); const url::Origin kReferrerOrigin = url::Origin::Create(kReferrerURL); // It is possible that the referrer URL does not match what the policy // demands (e.g., non-empty URL and kNever policy), so we'll test that the // correct referrer is generated, and that the navigation succeeds. const Referrer referrer(kReferrerURL, GetReferrerPolicy()); // Navigate to a resource whose destination URL is same-origin with the // navigation's referrer. The final referrer should be generated correctly. NavigationController::LoadURLParams load_params(kDestination); load_params.referrer = referrer; TestNavigationManager manager(web_contents(), kDestination); web_contents()->GetController().LoadURLWithParams(load_params); EXPECT_TRUE(manager.WaitForRequestStart()); // The referrer should have been sanitized. ASSERT_TRUE(main_frame()->navigation_request()); switch (GetReferrerPolicy()) { case network::mojom::ReferrerPolicy::kAlways: case network::mojom::ReferrerPolicy::kDefault: case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade: case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin: case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin: case network::mojom::ReferrerPolicy::kSameOrigin: EXPECT_EQ(kReferrerURL, main_frame()->navigation_request()->GetReferrer().url); break; case network::mojom::ReferrerPolicy::kNever: EXPECT_EQ(GURL(), main_frame()->navigation_request()->GetReferrer().url); break; case network::mojom::ReferrerPolicy::kOrigin: case network::mojom::ReferrerPolicy::kStrictOrigin: EXPECT_EQ(kReferrerOrigin.GetURL(), main_frame()->navigation_request()->GetReferrer().url); break; } // The navigation should commit without being blocked. EXPECT_TRUE(manager.WaitForResponse()); ASSERT_TRUE(manager.WaitForNavigationFinished()); EXPECT_EQ(kDestination, web_contents()->GetLastCommittedURL()); } // Test to verify that an exploited renderer process trying to upload a file // it hasn't been explicitly granted permissions to is correctly terminated. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, PostUploadIllegalFilePath) { GURL form_url( embedded_test_server()->GetURL("/form_that_posts_to_echoall.html")); EXPECT_TRUE(NavigateToURL(shell(), form_url)); // Prepare a file for the upload form. base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedTempDir temp_dir; base::FilePath file_path; std::string file_content("test-file-content"); ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path)); ASSERT_TRUE(base::WriteFile(file_path, file_content)); base::RunLoop run_loop; // Fill out the form to refer to the test file. std::unique_ptr<FileChooserDelegate> delegate( new FileChooserDelegate(file_path, run_loop.QuitClosure())); web_contents()->SetDelegate(delegate.get()); EXPECT_TRUE( ExecJs(web_contents(), "document.getElementById('file').click();")); run_loop.Run(); // Ensure that the process is allowed to access to the chosen file and // does not have access to the other file name. EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile( current_frame_host()->GetProcess()->GetDeprecatedID(), file_path)); // Revoke the access to the file and submit the form. The renderer process // should be terminated. RenderProcessHostBadIpcMessageWaiter process_kill_waiter( current_frame_host()->GetProcess()); ChildProcessSecurityPolicyImpl* security_policy = ChildProcessSecurityPolicyImpl::GetInstance(); security_policy->RevokeAllPermissionsForFile( current_frame_host()->GetProcess()->GetDeprecatedID(), file_path); // Use EvalJs and respond back to the browser process before doing the actual // submission. This will ensure that the process termination is guaranteed to // arrive after the response from the executed JavaScript. EXPECT_EQ( true, EvalJs( shell(), "setTimeout(() => document.getElementById('file-form').submit(), 0);" "true;")); EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, process_kill_waiter.Wait()); } // Test case to verify that redirects to data: URLs are properly disallowed, // even when invoked through a reload. // See https://crbug.com/723796. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, VerifyBlockedErrorPageURL_Reload) { NavigationControllerImpl& controller = web_contents()->GetController(); GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); // Navigate to an URL, which redirects to a data: URL, since it is an // unsafe redirect and will result in a blocked navigation and error page. GURL redirect_to_blank_url( embedded_test_server()->GetURL("/server-redirect?data:text/html,Hello!")); EXPECT_FALSE(NavigateToURL(shell(), redirect_to_blank_url)); EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType()); TestNavigationObserver reload_observer(web_contents()); EXPECT_TRUE(ExecJs(shell(), "location.reload()")); reload_observer.Wait(); // The expectation is that the blocked URL is present in the NavigationEntry, // and shows up in both GetURL and GetVirtualURL. EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); EXPECT_FALSE( controller.GetLastCommittedEntry()->GetURL().SchemeIs(url::kDataScheme)); EXPECT_EQ(redirect_to_blank_url, controller.GetLastCommittedEntry()->GetURL()); EXPECT_EQ(redirect_to_blank_url, controller.GetLastCommittedEntry()->GetVirtualURL()); } // TODO(crbug.com/40924471): Test is flaky on Android, Linux. #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX) #define MAYBE_BackFollowedByReload DISABLED_BackFollowedByReload #else #define MAYBE_BackFollowedByReload BackFollowedByReload #endif IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_BackFollowedByReload) { // First, make two history entries. GURL url1(embedded_test_server()->GetURL("/title1.html")); GURL url2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(NavigateToURL(shell(), url2)); // Then execute a back navigation in Javascript followed by a reload. TestNavigationObserver navigation_observer(web_contents()); EXPECT_TRUE(ExecJs(web_contents(), "history.back(); location.reload();")); navigation_observer.Wait(); // The reload should have cancelled the back navigation, and the last // committed URL should still be the second URL. EXPECT_EQ(url2, web_contents()->GetLastCommittedURL()); } // Test that a navigation response can be entirely fetched, even after the // NavigationURLLoader has been deleted. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, FetchResponseAfterNavigationURLLoaderDeleted) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/main_document"); ASSERT_TRUE(embedded_test_server()->Start()); // Load a new document. GURL url(embedded_test_server()->GetURL("/main_document")); TestNavigationManager navigation_manager(web_contents(), url); shell()->LoadURL(url); // The navigation starts. EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // A NavigationRequest exists at this point. EXPECT_TRUE(main_frame()->navigation_request()); // The response's headers are received. response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "\r\n" "..."); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); // The renderer commits the navigation and the browser deletes its // NavigationRequest. ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); EXPECT_FALSE(main_frame()->navigation_request()); // The NavigationURLLoader has been deleted by now. Check that the renderer // can still receive more bytes. DOMMessageQueue dom_message_queue(web_contents()); response.Send( "<script>window.domAutomationController.send('done');</script>"); std::string done; EXPECT_TRUE(dom_message_queue.WaitForMessage(&done)); EXPECT_EQ("\"done\"", done); } IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, BrowserNavigationNetworkIsolationKey) { GURL url(embedded_test_server()->GetURL("/title1.html")); url::Origin origin = url::Origin::Create(url); URLLoaderMonitor monitor({url}); EXPECT_TRUE(NavigateToURL(shell(), url)); monitor.WaitForUrls(); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); ASSERT_TRUE(request->trusted_params); EXPECT_TRUE(net::IsolationInfo::Create( net::IsolationInfo::RequestType::kMainFrame, origin, origin, net::SiteForCookies::FromOrigin(origin)) .IsEqualForTesting(request->trusted_params->isolation_info)); } IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, RenderNavigationIsolationInfo) { GURL url(embedded_test_server()->GetURL("/title2.html")); url::Origin origin = url::Origin::Create(url); EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); URLLoaderMonitor monitor({url}); EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url)); monitor.WaitForUrls(); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); ASSERT_TRUE(request->trusted_params); EXPECT_TRUE(net::IsolationInfo::Create( net::IsolationInfo::RequestType::kMainFrame, origin, origin, net::SiteForCookies::FromOrigin(origin)) .IsEqualForTesting(request->trusted_params->isolation_info)); } IN_PROC_BROWSER_TEST_F(NetworkIsolationNavigationBrowserTest, SubframeIsolationInfo) { GURL url(embedded_test_server()->GetURL("/page_with_iframe.html")); GURL iframe_document = embedded_test_server()->GetURL("/title1.html"); url::Origin origin = url::Origin::Create(url); url::Origin iframe_origin = url::Origin::Create(iframe_document); URLLoaderMonitor monitor({iframe_document}); EXPECT_TRUE(NavigateToURL(shell(), url)); monitor.WaitForUrls(); std::optional<network::ResourceRequest> main_frame_request = monitor.GetRequestInfo(url); ASSERT_TRUE(main_frame_request.has_value()); ASSERT_TRUE(main_frame_request->trusted_params); EXPECT_TRUE(net::IsolationInfo::Create( net::IsolationInfo::RequestType::kMainFrame, origin, origin, net::SiteForCookies::FromOrigin(origin)) .IsEqualForTesting( main_frame_request->trusted_params->isolation_info)); std::optional<network::ResourceRequest> iframe_request = monitor.GetRequestInfo(iframe_document); ASSERT_TRUE(iframe_request->trusted_params); EXPECT_TRUE( net::IsolationInfo::Create(net::IsolationInfo::RequestType::kSubFrame, origin, iframe_origin, net::SiteForCookies::FromOrigin(origin)) .IsEqualForTesting(iframe_request->trusted_params->isolation_info)); } // Tests that the initiator is not set for a browser initiated top frame // navigation. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BrowserNavigationInitiator) { GURL url(embedded_test_server()->GetURL("/title1.html")); URLLoaderMonitor monitor; // Perform the actual navigation. EXPECT_TRUE(NavigateToURL(shell(), url)); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); ASSERT_TRUE(request.has_value()); ASSERT_FALSE(request->request_initiator.has_value()); } // Test that the initiator is set to the starting page when a renderer initiated // navigation goes from the starting page to another page. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererNavigationInitiator) { GURL starting_page(embedded_test_server()->GetURL("a.com", "/title1.html")); url::Origin starting_page_origin; starting_page_origin = starting_page_origin.Create(starting_page); EXPECT_TRUE(NavigateToURL(shell(), starting_page)); GURL url(embedded_test_server()->GetURL("/title2.html")); URLLoaderMonitor monitor; // Perform the actual navigation. EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url)); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); ASSERT_TRUE(request.has_value()); EXPECT_EQ(starting_page_origin, request->request_initiator); } // Test that the initiator is set to the starting page when a sub frame is // navigated by Javascript from some starting page to another page. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SubFrameJsNavigationInitiator) { GURL starting_page(embedded_test_server()->GetURL("/frame_tree/top.html")); EXPECT_TRUE(NavigateToURL(shell(), starting_page)); // The main_frame() and subframe should each have a live RenderFrame. EXPECT_TRUE(main_frame() ->current_frame_host() ->render_view_host() ->IsRenderViewLive()); EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); EXPECT_TRUE( main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); GURL url(embedded_test_server()->GetURL("a.com", "/title1.html")); URLLoaderMonitor monitor({url}); std::string script = "location.href='" + url.spec() + "'"; // Perform the actual navigation. EXPECT_TRUE(ExecJs(main_frame()->child_at(0)->current_frame_host(), script)); EXPECT_TRUE(main_frame() ->current_frame_host() ->render_view_host() ->IsRenderViewLive()); EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); EXPECT_TRUE( main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); url::Origin starting_page_origin; starting_page_origin = starting_page_origin.Create(starting_page); monitor.WaitForUrls(); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); EXPECT_EQ(starting_page_origin, request->request_initiator); } // Test that the initiator is set to the starting page when a sub frame, // selected by Id, is navigated by Javascript from some starting page to another // page. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SubframeNavigationByTopFrameInitiator) { // Go to a page on a.com with an iframe that is on b.com GURL starting_page(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), starting_page)); // The main_frame and subframe should each have a live RenderFrame. EXPECT_TRUE(main_frame() ->current_frame_host() ->render_view_host() ->IsRenderViewLive()); EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); EXPECT_TRUE( main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); GURL url(embedded_test_server()->GetURL("c.com", "/title1.html")); URLLoaderMonitor monitor; // Perform the actual navigation. NavigateIframeToURL(web_contents(), "child-0", url); EXPECT_TRUE(main_frame() ->current_frame_host() ->render_view_host() ->IsRenderViewLive()); EXPECT_TRUE(main_frame()->current_frame_host()->IsRenderFrameLive()); EXPECT_TRUE( main_frame()->child_at(0)->current_frame_host()->IsRenderFrameLive()); url::Origin starting_page_origin; starting_page_origin = starting_page_origin.Create(starting_page); std::optional<network::ResourceRequest> request = monitor.GetRequestInfo(url); ASSERT_TRUE(request.has_value()); EXPECT_EQ(starting_page_origin, request->request_initiator); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedCrossSiteNewWindowInitator) { GURL url(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); auto initiator_global_token = current_frame_host()->GetGlobalFrameToken(); // Simulate clicking on a cross-site link. { const char kReplacePortNumber[] = "setPortNumber(%d);"; uint16_t port_number = embedded_test_server()->port(); url = embedded_test_server()->GetURL("foo.com", "/title2.html"); EXPECT_TRUE( ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); TestNavigationObserver observer(url); observer.StartWatchingNewWebContents(); EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowLink();")); observer.Wait(); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(initiator_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initiator_global_token.child_id, observer.last_initiator_process_id()); } } // Ensure that renderer initiated navigations which have the opener suppressed // work. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedNewWindowNoOpenerNavigation) { GURL url(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); RenderFrameHost* initial_rfh = current_frame_host(); url::Origin initial_origin = initial_rfh->GetLastCommittedOrigin(); auto initiator_global_token = initial_rfh->GetGlobalFrameToken(); // Simulate clicking on a cross-site link which has rel="noopener". { const char kReplacePortNumber[] = "setPortNumber(%d);"; uint16_t port_number = embedded_test_server()->port(); url = embedded_test_server()->GetURL("foo.com", "/title2.html"); EXPECT_TRUE( ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); TestNavigationObserver observer(url); observer.StartWatchingNewWebContents(); EXPECT_EQ(true, EvalJs(shell(), "clickCrossSiteNewWindowNoOpenerLink();")); observer.Wait(); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_EQ(initial_origin, observer.last_initiator_origin().value()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(initiator_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initiator_global_token.child_id, observer.last_initiator_process_id()); } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedWithSubframeInitator) { GURL url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(a())")); EXPECT_TRUE(NavigateToURL(shell(), url)); GURL subframe_url = embedded_test_server()->GetURL("a.com", "/simple_links.html"); EXPECT_TRUE( NavigateToURLFromRenderer(main_frame()->child_at(0), subframe_url)); RenderFrameHostImpl* subframe_rfh = current_frame_host()->child_at(0)->current_frame_host(); auto initiator_global_token = subframe_rfh->GetGlobalFrameToken(); // Simulate clicking on a cross-site link. { const char kReplacePortNumber[] = "setPortNumber(%d);"; uint16_t port_number = embedded_test_server()->port(); url = embedded_test_server()->GetURL("foo.com", "/title2.html"); EXPECT_TRUE(ExecJs(subframe_rfh, base::StringPrintf(kReplacePortNumber, port_number))); TestNavigationObserver observer(url); observer.StartWatchingNewWebContents(); EXPECT_EQ(true, EvalJs(subframe_rfh, "clickCrossSiteNewWindowLink();")); observer.Wait(); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(initiator_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initiator_global_token.child_id, observer.last_initiator_process_id()); } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, InitiatorFrameStateConsistentAtDidStartNavigation) { GURL form_page_url(embedded_test_server()->GetURL( "a.com", "/form_that_posts_to_echoall.html")); EXPECT_TRUE(NavigateToURL(shell(), form_page_url)); // Give the form an action that will navigate to a slow page. GURL form_action_url(embedded_test_server()->GetURL("b.com", "/slow?100")); EXPECT_TRUE( ExecJs(shell(), JsReplace("document.getElementById('form').action = $1", form_action_url))); // Open a new window that can be targeted by the form submission. WebContents* form_contents = web_contents(); ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), "window.open('about:blank', 'target_frame');")); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); EXPECT_TRUE( ExecJs(form_contents, "document.getElementById('form').target = 'target_frame';")); TestNavigationManager popup_manager(popup_contents, form_action_url); TestNavigationManager form_manager( form_contents, embedded_test_server()->GetURL("a.com", "/title2.html")); // Submit the form and navigate the form's page. EXPECT_TRUE(ExecJs(form_contents, "window.location.href = 'title2.html'")); EXPECT_TRUE( ExecJs(form_contents, "document.getElementById('form').submit();")); // The form page's navigation should start prior to the form navigation. EXPECT_TRUE(form_manager.WaitForRequestStart()); EXPECT_FALSE(popup_manager.GetNavigationHandle()); // When the navigation starts for the popup, ensure that the original page has // not finished navigating. If this was not the case, we could not make any // statements on the validity of initiator state during a navigation. // Navigation handles are only available prior to DidFinishNavigation(). EXPECT_TRUE(popup_manager.WaitForRequestStart()); EXPECT_TRUE(form_manager.GetNavigationHandle()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedMiddleClickInitator) { GURL url(embedded_test_server()->GetURL("/simple_links.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); auto initiator_global_token = current_frame_host()->GetGlobalFrameToken(); // Simulate middle-clicking on a cross-site link. { const char kReplacePortNumber[] = "setPortNumber(%d);"; uint16_t port_number = embedded_test_server()->port(); url = embedded_test_server()->GetURL("foo.com", "/title2.html"); EXPECT_TRUE( ExecJs(shell(), base::StringPrintf(kReplacePortNumber, port_number))); TestNavigationObserver observer(url); observer.StartWatchingNewWebContents(); EXPECT_EQ(true, EvalJs(shell(), R"( target = document.getElementById('cross_site_link'); var evt = new MouseEvent("click", {"button": 1 /* middle_button */}); target.dispatchEvent(evt);)")); observer.Wait(); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_TRUE(observer.last_initiator_frame_token().has_value()); EXPECT_EQ(initiator_global_token.frame_token, observer.last_initiator_frame_token().value()); EXPECT_EQ(initiator_global_token.child_id, observer.last_initiator_process_id()); } } // Data URLs can have a reference fragment like any other URLs. This test makes // sure it is taken into account. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, DataURLWithReferenceFragment) { GURL url("data:text/html,body#foo"); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ("body", EvalJs(shell(), "document.body.textContent;")); EXPECT_EQ("#foo", EvalJs(shell(), "location.hash;")); } // Regression test for https://crbug.com/796561. // 1) Start on a document with history.length == 1. // 2) Create an iframe and call history.pushState at the same time. // 3) history.back() must work. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, IframeAndPushStateSimultaneously) { GURL main_url = embedded_test_server()->GetURL("/simple_page.html"); GURL iframe_url = embedded_test_server()->GetURL("/hello.html"); // 1) Start on a new document such that history.length == 1. { EXPECT_TRUE(NavigateToURL(shell(), main_url)); EXPECT_EQ(1, EvalJs(shell(), "history.length")); } // 2) Create an iframe and call history.pushState at the same time. { TestNavigationManager iframe_navigation(web_contents(), iframe_url); ExecuteScriptAsync(shell(), "let iframe = document.createElement('iframe');" "iframe.src = '/hello.html';" "document.body.appendChild(iframe);"); EXPECT_TRUE(iframe_navigation.WaitForRequestStart()); // The iframe navigation is paused. In the meantime, a pushState navigation // begins and ends. TestNavigationManager push_state_navigation(web_contents(), main_url); ExecuteScriptAsync(shell(), "window.history.pushState({}, null);"); ASSERT_TRUE(push_state_navigation.WaitForNavigationFinished()); // The iframe navigation is resumed. ASSERT_TRUE(iframe_navigation.WaitForNavigationFinished()); } // 3) history.back() must work. { TestNavigationObserver navigation_observer(web_contents()); EXPECT_TRUE(ExecJs(web_contents(), "history.back();")); navigation_observer.Wait(); } } // Regression test for https://crbug.com/260144 // Back/Forward navigation in an iframe must not stop ongoing XHR. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, IframeNavigationsDoNotStopXHR) { // A response for the XHR request. It will be delayed until the end of all the // navigations. net::test_server::ControllableHttpResponse xhr_response( embedded_test_server(), "/xhr"); EXPECT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); DOMMessageQueue dom_message_queue(web_contents()); std::string message; // 1) Send an XHR. ExecuteScriptAsync( shell(), "let xhr = new XMLHttpRequest();" "xhr.open('GET', './xhr', true);" "xhr.onabort = () => window.domAutomationController.send('xhr.onabort');" "xhr.onerror = () => window.domAutomationController.send('xhr.onerror');" "xhr.onload = () => window.domAutomationController.send('xhr.onload');" "xhr.send();"); // 2) Create an iframe and wait for the initial load. { ExecuteScriptAsync( shell(), "var iframe = document.createElement('iframe');" "iframe.src = './title1.html';" "iframe.onload = function() {" " window.domAutomationController.send('iframe.onload');" "};" "document.body.appendChild(iframe);"); EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); EXPECT_EQ("\"iframe.onload\"", message); } // 3) Navigate the iframe elsewhere. { ExecuteScriptAsync(shell(), "var iframe = document.querySelector('iframe');" "iframe.src = './title2.html';"); EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); EXPECT_EQ("\"iframe.onload\"", message); } // 4) history.back() in the iframe. { ExecuteScriptAsync(shell(), "var iframe = document.querySelector('iframe');" "iframe.contentWindow.history.back()"); EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); EXPECT_EQ("\"iframe.onload\"", message); } // 5) history.forward() in the iframe. { ExecuteScriptAsync(shell(), "var iframe = document.querySelector('iframe');" "iframe.contentWindow.history.forward()"); EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); EXPECT_EQ("\"iframe.onload\"", message); } // 6) Wait for the XHR. { xhr_response.WaitForRequest(); xhr_response.Send( "HTTP/1.1 200 OK\r\n" "Connection: close\r\n" "Content-Length: 2\r\n" "Content-Type: text/plain; charset=utf-8\r\n" "\r\n" "OK"); xhr_response.Done(); EXPECT_TRUE(dom_message_queue.WaitForMessage(&message)); EXPECT_EQ("\"xhr.onload\"", message); } EXPECT_FALSE(dom_message_queue.PopMessage(&message)); } // Regression test for https://crbug.com/856396. // Note that original issue for the bug is not applicable anymore, because there // is no provisional document loader which has not committed yet. We keep the // modified version of this test to check removing iframe from the load event // handler. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, ReplacingDocumentLoaderFiresLoadEvent) { net::test_server::ControllableHttpResponse main_document_response( embedded_test_server(), "/main_document"); net::test_server::ControllableHttpResponse iframe_response( embedded_test_server(), "/iframe"); ASSERT_TRUE(embedded_test_server()->Start()); // 1) Load the main document. shell()->LoadURL(embedded_test_server()->GetURL("/main_document")); main_document_response.WaitForRequest(); main_document_response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "\r\n" "<script>" " var detach_iframe = function() {" " var iframe = document.querySelector('iframe');" " iframe.parentNode.removeChild(iframe);" " }" "</script>" "<body onload='detach_iframe()'>" " <iframe src='/iframe'></iframe>" "</body>"); main_document_response.Done(); // 2) The iframe starts to load, but the server only have time to send the // response's headers, not the response's body. This should commit the // iframe's load. iframe_response.WaitForRequest(); iframe_response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "\r\n"); // 3) In the meantime the iframe navigates elsewhere. It causes the previous // DocumentLoader to be replaced by the new one. Removing it may // trigger the 'load' event and delete the iframe. EXPECT_TRUE( ExecJs(shell(), "document.querySelector('iframe').src = '/title1.html'")); // 4) Finish the original request. iframe_response.Done(); // Wait for the iframe to be deleted and check the renderer process is still // alive. int iframe_count = 1; while (iframe_count != 0) { iframe_count = EvalJs( shell(), "var iframe_count = document.getElementsByTagName('iframe').length;" "iframe_count;") .ExtractInt(); } } class NavigationDownloadBrowserTest : public NavigationBaseBrowserTest { protected: void SetUpOnMainThread() override { NavigationBaseBrowserTest::SetUpOnMainThread(); // Set up a test download directory, in order to prevent prompting for // handling downloads. ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir()); ShellDownloadManagerDelegate* delegate = static_cast<ShellDownloadManagerDelegate*>( web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath()); } private: base::ScopedTempDir downloads_directory_; }; // Regression test for https://crbug.com/855033 // 1) A page contains many scripts and DOM elements. It forces the parser to // yield CPU to other tasks. That way the response body's data are not fully // read when URLLoaderClient::OnComplete(..) is received. // 2) A script makes the document navigates elsewhere while it is still loading. // It cancels the parser of the current document. Due to a bug, the document // loader was not marked to be 'loaded' at this step. // 3) The request for the new navigation starts and it turns out it is a // download. The navigation is dropped. // 4) There are no more possibilities for DidStopLoading() to be sent. IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest, StopLoadingAfterDroppedNavigation) { net::test_server::ControllableHttpResponse main_response( embedded_test_server(), "/main"); ASSERT_TRUE(embedded_test_server()->Start()); GURL main_url(embedded_test_server()->GetURL("/main")); GURL download_url(embedded_test_server()->GetURL("/download-test1.lib")); shell()->LoadURL(main_url); main_response.WaitForRequest(); std::string headers = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "\r\n"; // Craft special HTML to make the blink::DocumentParser yield CPU to other // tasks. The goal is to ensure the response body datapipe is not fully read // when URLLoaderClient::OnComplete() is called. // This relies on the HTMLParserScheduler::ShouldYield() heuristics. std::string mix_of_script_and_div = "<script></script><div></div>"; for (size_t i = 0; i < 10; ++i) { mix_of_script_and_div += mix_of_script_and_div; // Exponential growth. } std::string navigate_to_download = "<script>location.href='" + download_url.spec() + "'</script>"; main_response.Send(headers + navigate_to_download + mix_of_script_and_div); main_response.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); } // Renderer initiated back/forward navigation in beforeunload should not prevent // the user to navigate away from a website. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, HistoryBackInBeforeUnload) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url_1)); EXPECT_TRUE(ExecJs(web_contents(), "onbeforeunload = function() {" " history.pushState({}, null, '/');" " history.back();" "};", EXECUTE_SCRIPT_NO_USER_GESTURE)); EXPECT_TRUE(NavigateToURL(shell(), url_2)); } // Same as 'HistoryBackInBeforeUnload', but wraps history.back() inside // window.setTimeout(). Thus it is executed "outside" of its beforeunload // handler and thus avoid basic navigation circumventions. // Regression test for: https://crbug.com/879965. IN_PROC_BROWSER_TEST_F(NavigationGoToEntryAtOffsetBrowserTest, HistoryBackInBeforeUnloadAfterSetTimeout) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url_1)); EXPECT_TRUE(ExecJs(web_contents(), "onbeforeunload = function() {" " history.pushState({}, null, '/');" " setTimeout(()=>history.back());" "};", EXECUTE_SCRIPT_NO_USER_GESTURE)); TestNavigationManager navigation(web_contents(), url_2); base::RunLoop run_loop; SetQuitHandlerForGoToEntryAtOffset(run_loop.QuitClosure()); shell()->LoadURL(url_2); run_loop.Run(); ASSERT_TRUE(navigation.WaitForNavigationFinished()); EXPECT_TRUE(navigation.was_successful()); } // Renderer initiated back/forward navigation can't cancel an ongoing browser // initiated navigation if it is not user initiated. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, HistoryBackCancelPendingNavigationNoUserGesture) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url_1)); // 1) A pending browser initiated navigation (omnibox, ...) starts. TestNavigationManager navigation(web_contents(), url_2); shell()->LoadURL(url_2); EXPECT_TRUE(navigation.WaitForRequestStart()); // 2) history.back() is sent but is not user initiated. EXPECT_TRUE(ExecJs(web_contents(), "history.pushState({}, null, '/');" "history.back();", EXECUTE_SCRIPT_NO_USER_GESTURE)); // 3) The first pending navigation is not canceled and can continue. ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation. EXPECT_TRUE(navigation.was_successful()); } // Renderer initiated back/forward navigation can cancel an ongoing browser // initiated navigation if it is user initiated. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, HistoryBackCancelPendingNavigationUserGesture) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url_1)); // 1) A pending browser initiated navigation (omnibox, ...) starts. TestNavigationManager navigation(web_contents(), url_2); shell()->LoadURL(url_2); EXPECT_TRUE(navigation.WaitForRequestStart()); // 2) history.back() is sent and is user initiated. EXPECT_TRUE(ExecJs(web_contents(), "history.pushState({}, null, '/');" "history.back();")); // 3) Check the first pending navigation has been canceled. ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Resume navigation. EXPECT_FALSE(navigation.was_successful()); } // Ensure the renderer process doesn't send too many IPC to the browser process // when history.pushState() and history.back() are called in a loop. // Failing to do so causes the browser to become unresponsive. // See https://crbug.com/882238 // TODO(crbug.com/379844650): Disabled on Linux sanitizer bots due to flakiness. #if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER) #define MAYBE_IPCFlood_GoToEntryAtOffset DISABLED_IPCFlood_GoToEntryAtOffset #else #define MAYBE_IPCFlood_GoToEntryAtOffset IPCFlood_GoToEntryAtOffset #endif IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, MAYBE_IPCFlood_GoToEntryAtOffset) { GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "Throttling navigation to prevent the browser from hanging. See " "https://crbug.com/1038223. Command line switch " "--disable-ipc-flooding-protection can be used to bypass the " "protection"); EXPECT_TRUE(ExecJs(shell(), R"( for(let i = 0; i<1000; ++i) { history.pushState({},"page 2", "bar.html"); history.back(); } )")); ASSERT_TRUE(console_observer.Wait()); } // Ensure the renderer process doesn't send too many IPC to the browser process // when doing a same-document navigation is requested in a loop. // Failing to do so causes the browser to become unresponsive. // TODO(arthursonzogni): Make the same test, but when the navigation is // requested from a remote frame. // See https://crbug.com/882238 IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, IPCFlood_Navigation) { GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "Throttling navigation to prevent the browser from hanging. See " "https://crbug.com/1038223. Command line switch " "--disable-ipc-flooding-protection can be used to bypass the " "protection"); EXPECT_TRUE(ExecJs(shell(), R"( for(let i = 0; i<1000; ++i) { location.href = "#" + i; ++i; } )")); ASSERT_TRUE(console_observer.Wait()); } // TODO(http://crbug.com/632514): This test currently expects opener downloads // go through, but when the linked bug is resolved the download should be // disallowed. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OpenerNavigation_DownloadPolicy) { base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedTempDir download_dir; ASSERT_TRUE(download_dir.CreateUniqueTempDir()); ShellDownloadManagerDelegate* delegate = static_cast<ShellDownloadManagerDelegate*>( web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); delegate->SetDownloadBehaviorForTesting(download_dir.GetPath()); EXPECT_TRUE( NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); // Open a popup. EXPECT_EQ(true, EvalJs(web_contents(), "!!window.open();")); EXPECT_EQ(2u, Shell::windows().size()); // Using the popup, navigate its opener to a download. base::HistogramTester histograms; WebContents* popup = Shell::windows()[1]->web_contents(); EXPECT_NE(popup, web_contents()); DownloadTestObserverInProgress observer( web_contents()->GetBrowserContext()->GetDownloadManager(), 1 /* wait_count */); EXPECT_TRUE(ExecJs( popup, "window.opener.location ='data:html/text;base64,'+btoa('payload');", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.WaitForFinished(); } // A variation of the OpenerNavigation_DownloadPolicy test above, but uses a // cross-origin URL for the popup window. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, CrossOriginOpenerNavigation_DownloadPolicy) { base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedTempDir download_dir; ASSERT_TRUE(download_dir.CreateUniqueTempDir()); ShellDownloadManagerDelegate* delegate = static_cast<ShellDownloadManagerDelegate*>( web_contents()->GetBrowserContext()->GetDownloadManagerDelegate()); delegate->SetDownloadBehaviorForTesting(download_dir.GetPath()); EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Open a popup. ShellAddedObserver shell_observer; EXPECT_TRUE(EvalJs(web_contents(), JsReplace("!!window.open($1);", embedded_test_server()->GetURL( "bar.com", "/title1.html"))) .ExtractBool()); Shell* new_shell = shell_observer.GetShell(); EXPECT_EQ(2u, Shell::windows().size()); // Wait for the navigation in the popup to complete, so the origin of the // document will be correct. WebContents* popup = new_shell->web_contents(); EXPECT_NE(popup, web_contents()); EXPECT_TRUE(WaitForLoadStop(popup)); // Using the popup, navigate its opener to a download. base::HistogramTester histograms; const GURL data_url("data:html/text;base64,cGF5bG9hZA=="); TestNavigationManager manager(web_contents(), data_url); EXPECT_TRUE(ExecJs(popup, base::StringPrintf("window.opener.location ='%s'", data_url.spec().c_str()))); ASSERT_TRUE(manager.WaitForNavigationFinished()); EXPECT_FALSE(manager.was_successful()); } // Regression test for https://crbug.com/872284. // A NavigationThrottle cancels a download in WillProcessResponse. // The navigation request must be canceled and it must also cancel the network // request. Failing to do so resulted in the network socket being leaked. IN_PROC_BROWSER_TEST_F(NavigationDownloadBrowserTest, CancelDownloadOnResponseStarted) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Block every iframe in WillProcessResponse. content::TestNavigationThrottleInserter throttle_inserter( web_contents(), base::BindLambdaForTesting( [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { auto throttle = std::make_unique<TestNavigationThrottle>(handle); throttle->SetResponse(TestNavigationThrottle::WILL_PROCESS_RESPONSE, TestNavigationThrottle::SYNCHRONOUS, NavigationThrottle::CANCEL_AND_IGNORE); return throttle; })); // Insert enough iframes so that if sockets are not properly released: there // will not be enough of them to complete all navigations. As of today, only 6 // sockets can be used simultaneously. So using 7 iframes is enough. This test // uses 33 as a margin. EXPECT_TRUE(ExecJs(shell(), R"( for(let i = 0; i<33; ++i) { let iframe = document.createElement('iframe'); iframe.src = './download-test1.lib' document.body.appendChild(iframe); } )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); } // Add header on redirect. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, AddRequestHeaderOnRedirect) { net::test_server::ControllableHttpResponse response_1(embedded_test_server(), "", true); net::test_server::ControllableHttpResponse response_2(embedded_test_server(), "", true); ASSERT_TRUE(embedded_test_server()->Start()); content::TestNavigationThrottleInserter throttle_inserter( web_contents(), base::BindLambdaForTesting( [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { auto throttle = std::make_unique<TestNavigationThrottle>(handle); NavigationRequest* request = NavigationRequest::From(handle); throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, base::BindLambdaForTesting([request]() { request->SetRequestHeader("header_name", "header_value"); })); return throttle; })); // 1) There is no "header_name" header in the initial request. shell()->LoadURL(embedded_test_server()->GetURL("/doc")); response_1.WaitForRequest(); EXPECT_FALSE( base::Contains(response_1.http_request()->headers, "header_name")); response_1.Send( "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); response_1.Done(); // 2) The header is added to the second request after the redirect. response_2.WaitForRequest(); EXPECT_EQ("header_value", response_2.http_request()->headers.at("header_name")); // Redirect should not record a ReceivedResponse event. EXPECT_EQ(0u, test_ukm_recorder() .GetEntriesByName("Navigation.ReceivedResponse") .size()); } // Add header on request start, modify it on redirect. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, AddRequestHeaderModifyOnRedirect) { net::test_server::ControllableHttpResponse response_1(embedded_test_server(), "", true); net::test_server::ControllableHttpResponse response_2(embedded_test_server(), "", true); ASSERT_TRUE(embedded_test_server()->Start()); content::TestNavigationThrottleInserter throttle_inserter( web_contents(), base::BindLambdaForTesting( [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { auto throttle = std::make_unique<TestNavigationThrottle>(handle); NavigationRequest* request = NavigationRequest::From(handle); throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST, base::BindLambdaForTesting([request]() { request->SetRequestHeader("header_name", "header_value"); })); throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, base::BindLambdaForTesting([request]() { request->SetRequestHeader("header_name", "other_value"); })); return throttle; })); // 1) The header is added to the initial request. shell()->LoadURL(embedded_test_server()->GetURL("/doc")); response_1.WaitForRequest(); EXPECT_EQ("header_value", response_1.http_request()->headers.at("header_name")); response_1.Send( "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); response_1.Done(); // 2) The header is modified in the second request after the redirect. response_2.WaitForRequest(); EXPECT_EQ("other_value", response_2.http_request()->headers.at("header_name")); } // Add header on request start, remove it on redirect. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, AddRequestHeaderRemoveOnRedirect) { net::test_server::ControllableHttpResponse response_1(embedded_test_server(), "", true); net::test_server::ControllableHttpResponse response_2(embedded_test_server(), "", true); ASSERT_TRUE(embedded_test_server()->Start()); content::TestNavigationThrottleInserter throttle_inserter( web_contents(), base::BindLambdaForTesting( [](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { NavigationRequest* request = NavigationRequest::From(handle); auto throttle = std::make_unique<TestNavigationThrottle>(handle); throttle->SetCallback(TestNavigationThrottle::WILL_START_REQUEST, base::BindLambdaForTesting([request]() { request->SetRequestHeader("header_name", "header_value"); })); throttle->SetCallback(TestNavigationThrottle::WILL_REDIRECT_REQUEST, base::BindLambdaForTesting([request]() { request->RemoveRequestHeader("header_name"); })); return throttle; })); // 1) The header is added to the initial request. shell()->LoadURL(embedded_test_server()->GetURL("/doc")); response_1.WaitForRequest(); EXPECT_EQ("header_value", response_1.http_request()->headers.at("header_name")); response_1.Send( "HTTP/1.1 302 Moved Temporarily\r\nLocation: /new_doc\r\n\r\n"); response_1.Done(); // 2) The header is removed from the second request after the redirect. response_2.WaitForRequest(); EXPECT_FALSE( base::Contains(response_2.http_request()->headers, "header_name")); } // Name of header used by CorsInjectingUrlLoader. const std::string kCorsHeaderName = "test-header"; // URLLoaderThrottle that stores the last value of |kCorsHeaderName|. class CorsInjectingUrlLoader : public blink::URLLoaderThrottle { public: explicit CorsInjectingUrlLoader(std::string* last_cors_header_value) : last_cors_header_value_(last_cors_header_value) {} // blink::URLLoaderThrottle: void WillStartRequest(network::ResourceRequest* request, bool* defer) override { if (std::optional<std::string> header = request->cors_exempt_headers.GetHeader(kCorsHeaderName); header) { last_cors_header_value_->swap(*header); } else { last_cors_header_value_->clear(); } } private: // See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details. raw_ptr<std::string> last_cors_header_value_; }; // ContentBrowserClient responsible for creating CorsInjectingUrlLoader. class CorsContentBrowserClient : public ContentBrowserTestContentBrowserClient { public: explicit CorsContentBrowserClient(std::string* last_cors_header_value) : last_cors_header_value_(last_cors_header_value) {} // ContentBrowserClient overrides: std::vector<std::unique_ptr<blink::URLLoaderThrottle>> CreateURLLoaderThrottles( const network::ResourceRequest& request, BrowserContext* browser_context, const base::RepeatingCallback<WebContents*()>& wc_getter, NavigationUIData* navigation_ui_data, FrameTreeNodeId frame_tree_node_id, std::optional<int64_t> navigation_id) override { std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles; throttles.push_back( std::make_unique<CorsInjectingUrlLoader>(last_cors_header_value_)); return throttles; } private: // See |NavigationCorsExemptBrowserTest::last_cors_header_value_| for details. raw_ptr<std::string> last_cors_header_value_; }; class NavigationCorsExemptBrowserTest : public NavigationBaseBrowserTest { public: NavigationCorsExemptBrowserTest() = default; protected: const std::string& last_cors_header_value() const { return last_cors_header_value_; } void SetUpCommandLine(base::CommandLine* command_line) override { ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser( true); NavigationBaseBrowserTest::SetUpCommandLine(command_line); } void SetUpOnMainThread() override { cors_content_browser_client_ = std::make_unique<CorsContentBrowserClient>(&last_cors_header_value_); host_resolver()->AddRule("*", "127.0.0.1"); } void TearDownOnMainThread() override { cors_content_browser_client_.reset(); ShellContentBrowserClient::set_allow_any_cors_exempt_header_for_browser( false); } private: // Last value of kCorsHeaderName. Set by CorsInjectingUrlLoader. std::string last_cors_header_value_; std::unique_ptr<CorsContentBrowserClient> cors_content_browser_client_; }; // Verifies a header added by way of SetRequestHeader() makes it into // |cors_exempt_headers|. IN_PROC_BROWSER_TEST_F(NavigationCorsExemptBrowserTest, SetCorsExemptRequestHeader) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "", true); ASSERT_TRUE(embedded_test_server()->Start()); const std::string header_value = "value"; content::TestNavigationThrottleInserter throttle_inserter( web_contents(), base::BindLambdaForTesting([header_value](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { NavigationRequest* request = NavigationRequest::From(handle); auto throttle = std::make_unique<TestNavigationThrottle>(handle); throttle->SetCallback( TestNavigationThrottle::WILL_START_REQUEST, base::BindLambdaForTesting([request, header_value]() { request->SetCorsExemptRequestHeader(kCorsHeaderName, header_value); })); return throttle; })); shell()->LoadURL(embedded_test_server()->GetURL("/doc")); response.WaitForRequest(); EXPECT_EQ(header_value, response.http_request()->headers.at(kCorsHeaderName)); EXPECT_EQ(header_value, last_cors_header_value()); } // Test NavigationRequest::CheckAboutSrcDoc() IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocBrowserInitiated) { const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo", "about:srcdoc#foo"}; // 1. Main frame navigations to about:srcdoc and its variations are blocked. for (const char* url : about_srcdoc_urls) { NavigationHandleObserver handle_observer(web_contents(), GURL(url)); EXPECT_FALSE(NavigateToURL(shell(), GURL(url))); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_TRUE(handle_observer.is_error()); EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); } // 2. Subframe navigations to variations of about:srcdoc are not blocked. for (const char* url : about_srcdoc_urls) { GURL main_url = embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationHandleObserver handle_observer(web_contents(), GURL(url)); shell()->LoadURLForFrame(GURL(url), "child-name-0", ui::PAGE_TRANSITION_FORWARD_BACK); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_FALSE(handle_observer.is_error()); EXPECT_EQ(net::OK, handle_observer.net_error_code()); } } // Test NavigationRequest::CheckAboutSrcDoc(). IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, BlockedSrcDocRendererInitiated) { EXPECT_TRUE( NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html"))); const char* about_srcdoc_urls[] = {"about:srcdoc", "about:srcdoc?foo", "about:srcdoc#foo"}; // 1. Main frame navigations to about:srcdoc and its variations are blocked. for (const char* url : about_srcdoc_urls) { DidStartNavigationObserver start_observer(web_contents()); NavigationHandleObserver handle_observer(web_contents(), GURL(url)); EXPECT_TRUE(ExecJs(main_frame(), JsReplace("location.href = $1", url))); start_observer.Wait(); WaitForLoadStop(web_contents()); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_TRUE(handle_observer.is_error()); EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); } // 2. Subframe navigations to variations of about:srcdoc are blocked unless // they are same-document or the initiator is same origin to the srcdoc's // parent. In this test suite, the subframe is self-navigating, but attempting // a cross-origin navigation from a non-about:srcdoc page to about:srcdoc, // which isn't allowed. for (const char* url : about_srcdoc_urls) { GURL main_url = embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"); EXPECT_TRUE(NavigateToURL(shell(), main_url)); DidStartNavigationObserver start_observer(web_contents()); NavigationHandleObserver handle_observer(web_contents(), GURL(url)); FrameTreeNode* subframe = main_frame()->child_at(0); // Executing location.href = "about:srcdoc" fails. // Note: if the subframe had already been navigated to about:srcdoc, then // executing location.href = 'about:srcdoc#foo' would be considered a same- // document navigation, and would be allowed. This behavior is tested in // web platform tests, e.g. // grandparent_location_aboutsrcdoc.sub.window.js, added in the same CL that // added this comment. EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url))); start_observer.Wait(); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_TRUE(handle_observer.is_error()); EXPECT_EQ(net::ERR_INVALID_URL, handle_observer.net_error_code()); } // 3. Navigate the sub-frame to be same-origin to the mainframe, then do the // about:srcdoc navigations with with the main frame as the initiator. The // test should succeed since the initiator and the parent are the same. { DidStartNavigationObserver start_observer(web_contents()); EXPECT_TRUE(ExecJs( main_frame(), "document.querySelector('iframe').src = 'title1.html';")); start_observer.Wait(); EXPECT_TRUE(WaitForLoadStop(web_contents())); } for (const char* url : about_srcdoc_urls) { DidStartNavigationObserver start_observer(web_contents()); NavigationHandleObserver handle_observer(web_contents(), GURL(url)); EXPECT_TRUE( ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url))); start_observer.Wait(); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_FALSE(handle_observer.is_error()); EXPECT_EQ(net::OK, handle_observer.net_error_code()); } // 4. The subframe is now on about:srcdoc ... verify it can reload itself. { FrameTreeNode* subframe = main_frame()->child_at(0); GURL url("about:srcdoc"); { // First, navigate the subframe to about:srcdoc without a fragment, so // that the subsequent reload will have a commit (i.e. it won't be // reloading a same-document navigation). DidStartNavigationObserver start_observer(web_contents()); NavigationHandleObserver handle_observer(web_contents(), GURL(url)); EXPECT_TRUE( ExecJs(main_frame(), JsReplace("frames[0].location.href = $1", url))); start_observer.Wait(); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_FALSE(handle_observer.is_error()); EXPECT_EQ(net::OK, handle_observer.net_error_code()); } DidStartNavigationObserver start_observer(web_contents()); NavigationHandleObserver handle_observer(web_contents(), url); EXPECT_TRUE(ExecJs(subframe, "location.reload()")); start_observer.Wait(); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.has_committed()); EXPECT_FALSE(handle_observer.is_error()); EXPECT_EQ(net::OK, handle_observer.net_error_code()); } } // Ensure that about:srcdoc navigations get their origin and base URL from their // parent frame (since that's where the content comes from) and not from the // initiator of the navigation (like about:blank cases). See also the // NavigateGrandchildToAboutBlank test. See https://crbug.com/1515381. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GrandchildToAboutSrcdoc_BaseUrl_CrossOrigin) { GURL parent_url = embedded_test_server()->GetURL( "a.com", "/frame_tree/page_with_one_frame.html"); EXPECT_TRUE(NavigateToURL(shell(), parent_url)); FrameTreeNode* subframe = main_frame()->child_at(0); // Navigate the subframe to a cross-site URL with a srcdoc subframe. GURL child_url = embedded_test_server()->GetURL( "b.com", "/frame_tree/page_with_srcdoc_frame.html"); TestNavigationObserver observer(web_contents()); EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", child_url))); observer.Wait(); EXPECT_EQ(child_url, subframe->current_frame_host()->GetLastCommittedURL()); FrameTreeNode* grandchild = subframe->child_at(0); EXPECT_EQ("hello", EvalJs(grandchild, "document.body.innerHTML").ExtractString()); // From the main frame, attempt to navigate the grandchild frame to // about:srcdoc. This should fail. TestNavigationObserver srcdoc_observer(web_contents()); EXPECT_TRUE( ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';")); srcdoc_observer.Wait(); EXPECT_EQ( "Could not load the requested resource.<br>Error code: -300 " "(net::ERR_INVALID_URL)", EvalJs(grandchild, "document.body.innerHTML").ExtractString()); // Since the navigation attempt failed, the origin and base URI are inherited // from the error page. EXPECT_EQ(GURL(url::kAboutSrcdocURL), grandchild->current_frame_host()->GetLastCommittedURL()); EXPECT_TRUE( grandchild->current_frame_host()->GetLastCommittedOrigin().opaque()); EXPECT_EQ("chrome-error://chromewebdata/", EvalJs(grandchild, "document.baseURI").ExtractString()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GrandchildToAboutSrcdoc_BaseUrl_SameOrigin) { GURL mainframe_url = embedded_test_server()->GetURL( "a.com", "/frame_tree/page_with_one_frame.html"); EXPECT_TRUE(NavigateToURL(shell(), mainframe_url)); FrameTreeNode* sub_frame = main_frame()->child_at(0); // Navigate `sub_frame` to a same-origin URL with a srcdoc subframe. GURL subframe_url = embedded_test_server()->GetURL( "a.com", "/frame_tree/page_with_srcdoc_frame.html"); TestNavigationObserver observer(web_contents()); EXPECT_TRUE(ExecJs(sub_frame, JsReplace("location.href = $1", subframe_url))); observer.Wait(); EXPECT_EQ(subframe_url, sub_frame->current_frame_host()->GetLastCommittedURL()); FrameTreeNode* srcdoc_frame = sub_frame->child_at(0); EXPECT_EQ("hello", EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString()); EXPECT_EQ(subframe_url, EvalJs(srcdoc_frame, "document.baseURI").ExtractString()); // From the mainframe, attempt to navigate `srcdoc_frame` to about:srcdoc. // This should succeed since the mainframe and `sub_frame` are same origin. TestNavigationObserver srcdoc_observer(web_contents()); EXPECT_TRUE( ExecJs(main_frame(), "frames[0][0].location.href = 'about:srcdoc';")); srcdoc_observer.Wait(); EXPECT_EQ("hello", EvalJs(srcdoc_frame, "document.body.innerHTML").ExtractString()); // The origin and base URI should be inherited from the initiator, since it's // same-origin to the srcdoc's parent frame `sub_frame`. EXPECT_EQ(GURL(url::kAboutSrcdocURL), srcdoc_frame->current_frame_host()->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(subframe_url), srcdoc_frame->current_frame_host()->GetLastCommittedOrigin()); // This picks up the mainframe's url since it's same-origin to `sub_frame`, // which is `srcdoc_frame`'s parent. EXPECT_EQ(mainframe_url, EvalJs(srcdoc_frame, "document.baseURI").ExtractString()); } // Ensure that about:blank navigations get their origin and base URL from the // initiator of the navigation, and not from their parent frame (like // about:srcdoc cases). See also the NavigateGrandchildToAboutSrcdoc test. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, GrandchildToAboutBlank_BaseUrl) { GURL url_a = embedded_test_server()->GetURL( "a.com", "/frame_tree/page_with_one_frame.html"); EXPECT_TRUE(NavigateToURL(shell(), url_a)); FrameTreeNode* subframe = main_frame()->child_at(0); // Navigate the subframe to a cross-site URL with a srcdoc subframe. GURL url_b = embedded_test_server()->GetURL( "b.com", "/frame_tree/page_with_srcdoc_frame.html"); TestNavigationObserver observer(web_contents()); EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1", url_b))); observer.Wait(); EXPECT_EQ(url_b, subframe->current_frame_host()->GetLastCommittedURL()); FrameTreeNode* grandchild = subframe->child_at(0); EXPECT_EQ("hello", EvalJs(grandchild, "document.body.innerHTML").ExtractString()); // From the main frame, navigate the grandchild frame to about:blank. TestNavigationObserver srcdoc_observer(web_contents()); EXPECT_TRUE( ExecJs(main_frame(), "frames[0][0].location.href = 'about:blank';")); srcdoc_observer.Wait(); // There is no content when navigating to about:blank. EXPECT_EQ("", EvalJs(grandchild, "document.body.innerHTML").ExtractString()); // The origin and base URI should be inherited from the initiator of the // navigation and not the parent frame, unlike navigations to about:srcdoc. EXPECT_EQ(GURL(url::kAboutBlankURL), grandchild->current_frame_host()->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), grandchild->current_frame_host()->GetLastCommittedOrigin()); EXPECT_EQ(url_a, EvalJs(grandchild, "document.baseURI").ExtractString()); } // Test renderer initiated navigations to about:srcdoc are routed through the // browser process. It means RenderFrameHostImpl::BeginNavigation() is called. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutSrcDocUsesBeginNavigation) { GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // If DidStartNavigation is called before DidCommitProvisionalLoad, then it // means the navigation was driven by the browser process, otherwise by the // renderer process. This tests it was driven by the browser process: InterceptAndCancelDidCommitProvisionalLoad interceptor(web_contents()); DidStartNavigationObserver observer(web_contents()); EXPECT_TRUE(ExecJs(shell(), R"( let iframe = document.createElement("iframe"); iframe.srcdoc = "foo" document.body.appendChild(iframe); )")); observer.Wait(); // BeginNavigation is called. interceptor.Wait(1); // DidCommitNavigation is called. } // Regression test for https://crbug.com/996044 // 1) Navigate an iframe to srcdoc (about:srcdoc); // 2) Same-document navigation to about:srcdoc#1. // 3) Same-document navigation to about:srcdoc#2. // 4) history.back() to about:srcdoc#1. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SrcDocWithFragmentHistoryNavigation) { GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // 1) Navigate an iframe to srcdoc (about:srcdoc) EXPECT_TRUE(ExecJs(shell(), R"( new Promise(async resolve => { let iframe = document.createElement('iframe'); iframe.srcdoc = "test"; iframe.onload = resolve; document.body.appendChild(iframe); }); )")); // 2) Same-document navigation to about:srcdoc#1. // 3) Same-document navigation to about:srcdoc#2. EXPECT_TRUE(ExecJs(shell(), R"( let subwindow = document.querySelector('iframe').contentWindow; subwindow.location.hash = "1"; subwindow.location.hash = "2"; )")); // Inspect the session history. NavigationControllerImpl& controller = web_contents()->GetController(); ASSERT_EQ(3, controller.GetEntryCount()); ASSERT_EQ(2, controller.GetCurrentEntryIndex()); std::array<FrameNavigationEntry*, 3> entry; for (int i = 0; i < 3; ++i) { entry[i] = controller.GetEntryAtIndex(i) ->root_node() ->children[0] ->frame_entry.get(); } EXPECT_EQ(entry[0]->url(), "about:srcdoc"); EXPECT_EQ(entry[1]->url(), "about:srcdoc#1"); EXPECT_EQ(entry[2]->url(), "about:srcdoc#2"); // 4) history.back() to about:srcdoc#1. EXPECT_TRUE(ExecJs(shell(), "history.back()")); ASSERT_EQ(3, controller.GetEntryCount()); ASSERT_EQ(1, controller.GetCurrentEntryIndex()); } // Regression test for https://crbug.com/996044. // 1) Navigate an iframe to srcdoc (about:srcdoc). // 2) Cross-document navigation to about:srcdoc?1. // 3) Cross-document navigation to about:srcdoc?2. // 4) history.back() to about:srcdoc?1. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SrcDocWithQueryHistoryNavigation) { GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // 1) Navigate an iframe to srcdoc (about:srcdoc). EXPECT_TRUE(ExecJs(shell(), R"( new Promise(async resolve => { let iframe = document.createElement('iframe'); iframe.srcdoc = "test"; iframe.onload = resolve; document.body.appendChild(iframe); }); )")); // 2) Cross-document navigation to about:srcdoc?1. { TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?1")); EXPECT_TRUE(ExecJs(shell(), R"( let subwindow = document.querySelector('iframe').contentWindow; subwindow.location.search = "1"; )")); ASSERT_TRUE(commit_waiter.WaitForNavigationFinished()); } // 3) Cross-document navigation to about:srcdoc?2. { TestNavigationManager commit_waiter(web_contents(), GURL("about:srcdoc?2")); EXPECT_TRUE(ExecJs(shell(), R"( let subwindow = document.querySelector('iframe').contentWindow; subwindow.location.search = "2"; )")); ASSERT_TRUE(commit_waiter.WaitForNavigationFinished()); } // Inspect the session history. NavigationControllerImpl& controller = web_contents()->GetController(); ASSERT_EQ(3, controller.GetEntryCount()); ASSERT_EQ(2, controller.GetCurrentEntryIndex()); std::array<FrameNavigationEntry*, 3> entry; for (int i = 0; i < 3; ++i) { entry[i] = controller.GetEntryAtIndex(i) ->root_node() ->children[0] ->frame_entry.get(); } EXPECT_EQ(entry[0]->url(), "about:srcdoc"); EXPECT_EQ(entry[1]->url(), "about:srcdoc?1"); EXPECT_EQ(entry[2]->url(), "about:srcdoc?2"); // 4) history.back() to about:srcdoc#1. EXPECT_TRUE(ExecJs(shell(), "history.back()")); ASSERT_EQ(3, controller.GetEntryCount()); ASSERT_EQ(1, controller.GetCurrentEntryIndex()); } // Make sure embedders are notified about visible URL changes in this scenario: // 1. Navigate to A. // 2. Navigate to B. // 3. Add a forward entry in the history for later (same-document). // 4. Start navigation to C. // 5. Start history cross-document navigation, cancelling 4. // 6. Start history same-document navigation, cancelling 5. // // Regression test for https://crbug.com/998284. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, BackForwardInOldDocumentCancelPendingNavigation) { // This test expects a new request to be made when navigating back, which is // not happening with back-forward cache enabled. // See BackForwardCacheBrowserTest.RestoreWhilePendingCommit which covers the // same scenario for back-forward cache. web_contents()->GetController().GetBackForwardCache().DisableForTesting( BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING); using Response = net::test_server::ControllableHttpResponse; Response response_A1(embedded_test_server(), "/A"); Response response_A2(embedded_test_server(), "/A"); Response response_B1(embedded_test_server(), "/B"); Response response_C1(embedded_test_server(), "/C"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url_a = embedded_test_server()->GetURL("a.com", "/A"); GURL url_b = embedded_test_server()->GetURL("b.com", "/B"); GURL url_c = embedded_test_server()->GetURL("c.com", "/C"); EmbedderVisibleUrlTracker embedder_url_tracker; web_contents()->SetDelegate(&embedder_url_tracker); // 1. Navigate to A. shell()->LoadURL(url_a); response_A1.WaitForRequest(); response_A1.Send(non_cacheable_html_response); response_A1.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // 2. Navigate to B. shell()->LoadURL(url_b); response_B1.WaitForRequest(); response_B1.Send(non_cacheable_html_response); response_B1.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // 3. Add a forward entry in the history for later (same-document). EXPECT_TRUE(ExecJs(web_contents(), R"( history.pushState({},''); history.back(); )")); // 4. Start navigation to C. { EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } shell()->LoadURL(url_c); // TODO(arthursonzogni): The embedder_url_tracker should update to url_c at // this point, but we currently rely on FrameTreeNode::DidStopLoading for // invalidation and it does not occur when a prior navigation is already in // progress. The browser is still waiting on the same-document // "history.back()" to complete. { EXPECT_EQ(url_c, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } embedder_url_tracker.WaitUntilUrlInvalidated(); { EXPECT_EQ(url_c, web_contents()->GetVisibleURL()); EXPECT_EQ(url_c, embedder_url_tracker.url()); } response_C1.WaitForRequest(); // 5. Start history cross-document navigation, cancelling 4. EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); { EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } response_A2.WaitForRequest(); { EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } // 6. Start history same-document navigation, cancelling 5. EXPECT_TRUE(ExecJs(web_contents(), "history.forward()")); { EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } EXPECT_TRUE(WaitForLoadStop(web_contents())); { EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_EQ(url_b, embedder_url_tracker.url()); } } // Regression test for https://crbug.com/999932. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, CanceledNavigationBug999932) { using Response = net::test_server::ControllableHttpResponse; Response response_A1(embedded_test_server(), "/A"); Response response_A2(embedded_test_server(), "/A"); Response response_B1(embedded_test_server(), "/B"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url_a = embedded_test_server()->GetURL("a.com", "/A"); GURL url_b = embedded_test_server()->GetURL("b.com", "/B"); // 1. Navigate to A. shell()->LoadURL(url_a); response_A1.WaitForRequest(); response_A1.Send(non_cacheable_html_response); response_A1.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // 2. Start pending navigation to B. shell()->LoadURL(url_b); EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); EXPECT_TRUE(web_contents()->GetController().GetPendingEntry()); // 3. Cancel (2) with renderer-initiated reload with a UserGesture. EXPECT_TRUE(ExecJs(web_contents(), "location.reload()")); EXPECT_EQ(url_a, web_contents()->GetVisibleURL()); EXPECT_FALSE(web_contents()->GetController().GetPendingEntry()); // 4. Cancel (3) using document.open(); EXPECT_TRUE(ExecJs(web_contents(), "document.open()")); EXPECT_EQ(url_a, web_contents()->GetVisibleURL()); EXPECT_FALSE(web_contents()->GetController().GetPendingEntry()); } // Regression test for https://crbug.com/1001283 // 1) Load main document with CSP: script-src 'none' // 2) Open an about:srcdoc iframe. It inherits the CSP. // 3) The iframe navigates elsewhere. // 4) The iframe navigates back to about:srcdoc. // Check Javascript is never allowed. IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, SrcDocCSPInheritedAfterSameSiteHistoryNavigation) { using Response = net::test_server::ControllableHttpResponse; Response main_document_response(embedded_test_server(), "/main_document"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document"); GURL url_b = embedded_test_server()->GetURL("a.com", "/title1.html"); { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern("Refused to execute inline script *"); // 1) Load main document with CSP: script-src 'none' // 2) Open an about:srcdoc iframe. It inherits the CSP from its parent. shell()->LoadURL(url_a); main_document_response.WaitForRequest(); main_document_response.Send( "HTTP/1.1 200 OK\n" "content-type: text/html; charset=UTF-8\n" "Content-Security-Policy: script-src 'none'\n" "\n" "<iframe name='theiframe' srcdoc='" " <script>" " console.error(\"CSP failure\");" " </script>" "'>" "</iframe>"); main_document_response.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // Check Javascript was blocked the first time. ASSERT_TRUE(console_observer.Wait()); } // 3) The iframe navigates elsewhere. shell()->LoadURLForFrame(url_b, "theiframe", ui::PAGE_TRANSITION_MANUAL_SUBFRAME); EXPECT_TRUE(WaitForLoadStop(web_contents())); { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern("Refused to execute inline script *"); // 4) The iframe navigates back to about:srcdoc. web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // Check Javascript was blocked the second time. ASSERT_TRUE(console_observer.Wait()); } } IN_PROC_BROWSER_TEST_F(NavigationBaseBrowserTest, SrcDocCSPInheritedAfterCrossSiteHistoryNavigation) { using Response = net::test_server::ControllableHttpResponse; Response main_document_response(embedded_test_server(), "/main_document"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url_a = embedded_test_server()->GetURL("a.com", "/main_document"); GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern("Refused to execute inline script *"); // 1) Load main document with CSP: script-src 'none' // 2) Open an about:srcdoc iframe. It inherits the CSP from its parent. shell()->LoadURL(url_a); main_document_response.WaitForRequest(); main_document_response.Send( "HTTP/1.1 200 OK\n" "content-type: text/html; charset=UTF-8\n" "Content-Security-Policy: script-src 'none'\n" "\n" "<iframe name='theiframe' srcdoc='" " <script>" " console.error(\"CSP failure\");" " </script>" "'>" "</iframe>"); main_document_response.Done(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // Check Javascript was blocked the first time. ASSERT_TRUE(console_observer.Wait()); } // 3) The iframe navigates elsewhere. shell()->LoadURLForFrame(url_b, "theiframe", ui::PAGE_TRANSITION_MANUAL_SUBFRAME); EXPECT_TRUE(WaitForLoadStop(web_contents())); { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern("Refused to execute inline script *"); // 4) The iframe navigates back to about:srcdoc. web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); // Check Javascript was blocked the second time. ASSERT_TRUE(console_observer.Wait()); } } // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual // value of RenderFrameHost::GetPageUkmSourceId() --- unremarkable top-level // navigation case. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NavigationRequest_GetNextPageUkmSourceId_Basic) { const GURL kUrl(embedded_test_server()->GetURL("/title1.html")); TestNavigationManager manager(web_contents(), kUrl); shell()->LoadURL(kUrl); EXPECT_TRUE(manager.WaitForRequestStart()); ASSERT_TRUE(main_frame()->navigation_request()); ukm::SourceId nav_request_id = main_frame()->navigation_request()->GetNextPageUkmSourceId(); EXPECT_TRUE(manager.WaitForResponse()); ASSERT_TRUE(manager.WaitForNavigationFinished()); EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id); } // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual // value of RenderFrameHost::GetPageUkmSourceId() --- child frame case. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NavigationRequest_GetNextPageUkmSourceId_ChildFrame) { const GURL kUrl( embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html")); const GURL kDestUrl(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), kUrl)); FrameTreeNode* subframe = main_frame()->child_at(0); ASSERT_TRUE(subframe); TestNavigationManager manager(web_contents(), kDestUrl); EXPECT_TRUE( ExecJs(subframe, JsReplace("location.href = $1", kDestUrl.spec()))); EXPECT_TRUE(manager.WaitForRequestStart()); ASSERT_TRUE(subframe->navigation_request()); ukm::SourceId nav_request_id = subframe->navigation_request()->GetNextPageUkmSourceId(); EXPECT_TRUE(manager.WaitForResponse()); ASSERT_TRUE(manager.WaitForNavigationFinished()); // Should have the same page UKM ID in navigation as page post commit, and as // the top-level frame. EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), nav_request_id); EXPECT_EQ(subframe->current_frame_host()->GetPageUkmSourceId(), nav_request_id); } // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual // value of RenderFrameHost::GetPageUkmSourceId() --- same document navigation. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NavigationRequest_GetNextPageUkmSourceId_SameDocument) { const GURL kUrl(embedded_test_server()->GetURL("/title1.html")); const GURL kFragment(kUrl.Resolve("#here")); EXPECT_TRUE(NavigateToURL(shell(), kUrl)); NavigationHandleObserver handle_observer(web_contents(), kFragment); EXPECT_TRUE( ExecJs(main_frame(), JsReplace("location.href = $1", kFragment.spec()))); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(handle_observer.is_same_document()); EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), handle_observer.next_page_ukm_source_id()); } // Test that NavigationRequest::GetNextPageUkmSourceId returns the eventual // value of RenderFrameHost::GetPageUkmSourceId() --- back navigation; // this case matters because of back-forward cache. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NavigationRequest_GetNextPageUkmSourceId_Back) { const GURL kUrl1(embedded_test_server()->GetURL("a.com", "/title1.html")); const GURL kUrl2(embedded_test_server()->GetURL("b.com", "/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), kUrl1)); EXPECT_TRUE(NavigateToURL(shell(), kUrl2)); NavigationHandleObserver handle_observer(web_contents(), kUrl1); web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_EQ(current_frame_host()->GetPageUkmSourceId(), handle_observer.next_page_ukm_source_id()); } // Tests for cookies. Provides an HTTPS server. class NavigationCookiesBrowserTest : public NavigationBaseBrowserTest { protected: NavigationCookiesBrowserTest() = default; net::EmbeddedTestServer* https_server() { return &https_server_; } private: void SetUpOnMainThread() override { NavigationBaseBrowserTest::SetUpOnMainThread(); mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); https_server()->AddDefaultHandlers(GetTestDataFilePath()); } void SetUpCommandLine(base::CommandLine* command_line) override { NavigationBaseBrowserTest::SetUpCommandLine(command_line); mock_cert_verifier_.SetUpCommandLine(command_line); } void SetUpInProcessBrowserTestFixture() override { NavigationBaseBrowserTest::SetUpInProcessBrowserTestFixture(); mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); } void TearDownInProcessBrowserTestFixture() override { NavigationBaseBrowserTest::TearDownInProcessBrowserTestFixture(); mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); } content::ContentMockCertVerifier mock_cert_verifier_; net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; }; // Test how cookies are inherited in about:srcdoc iframes. // // Regression test: https://crbug.com/1003167. // Test is flaky on all platforms: https://crbug.com/339033006 IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, DISABLED_CookiesInheritedSrcDoc) { using Response = net::test_server::ControllableHttpResponse; Response response_1(https_server(), "/response_1"); Response response_2(https_server(), "/response_2"); Response response_3(https_server(), "/response_3"); ASSERT_TRUE(https_server()->Start()); GURL url_a(https_server()->GetURL("a.com", "/title1.html")); GURL url_b(https_server()->GetURL("b.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url_a)); EXPECT_TRUE(ExecJs(shell(), R"( let iframe = document.createElement("iframe"); iframe.srcdoc = "foo"; document.body.appendChild(iframe); )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* main_document = current_frame_host(); RenderFrameHostImpl* sub_document_1 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url::kAboutSrcdocURL, sub_document_1->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_1->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_1->GetSiteInstance()); // 0. The default state doesn't contain any cookies. EXPECT_EQ("", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); // 1. Set a cookie in the main document, it affects its child too. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); // 2. Set a cookie in the child, it affects its parent too. EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); // 3. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_1, "fetch('/response_1');"); response_1.WaitForRequest(); EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); // 4. Navigate the iframe elsewhere. EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_2 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 5. Set a cookie in the main document. It doesn't affect its child. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 6. Set a cookie in the child. It doesn't affect its parent. EXPECT_TRUE(ExecJs(sub_document_2, "document.cookie = 'd=0; SameSite=none; Secure';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); // 7. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); response_2.WaitForRequest(); EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); // 8. Navigate the iframe back to about:srcdoc. web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_3 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); EXPECT_EQ(url::kAboutSrcdocURL, sub_document_3->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_3->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_3->GetSiteInstance()); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); // 9. Set cookie in the main document. It should be inherited by the child. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); // 11. Set cookie in the child document. It should be reflected on its parent. EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(sub_document_3, "document.cookie")); // 12. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); response_3.WaitForRequest(); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", response_3.http_request()->headers.at("Cookie")); } // Test how cookies are inherited in about:blank iframes. IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, CookiesInheritedAboutBlank) { // This test expects several cross-site navigation to happen. if (!AreAllSitesIsolatedForTesting()) return; using Response = net::test_server::ControllableHttpResponse; Response response_1(https_server(), "/response_1"); Response response_2(https_server(), "/response_2"); Response response_3(https_server(), "/response_3"); ASSERT_TRUE(https_server()->Start()); GURL url_a(https_server()->GetURL("a.com", "/title1.html")); GURL url_b(https_server()->GetURL("b.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url_a)); EXPECT_TRUE( ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');" "iframe.src = $1;" "document.body.appendChild(iframe);", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(ExecJs(shell(), R"( document.querySelector('iframe').src = "about:blank" )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* main_document = current_frame_host(); RenderFrameHostImpl* sub_document_1 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_1->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_1->GetSiteInstance()); // 0. The default state doesn't contain any cookies. EXPECT_EQ("", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); // 1. Set a cookie in the main document, it affects its child too. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); // 2. Set a cookie in the child, it affects its parent too. EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); // 3. Checks cookies are sent while requesting resources. GURL url_response_1 = https_server()->GetURL("a.com", "/response_1"); ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1)); response_1.WaitForRequest(); EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); // 4. Navigate the iframe elsewhere. EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_2 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 5. Set a cookie in the main document. It doesn't affect its child. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 6. Set a cookie in the child. It doesn't affect its parent. EXPECT_TRUE(ExecJs(sub_document_2, "document.cookie = 'd=0; SameSite=none; Secure';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); // 7. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); response_2.WaitForRequest(); EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); // 8. Navigate the iframe back to about:blank. web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_3 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_3->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_3->GetSiteInstance()); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); // 9. Set cookie in the main document. It affects the iframe. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); // 10. Set cookie in the iframe. It affects the main frame. EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(sub_document_3, "document.cookie")); // 11. Even if document.cookie is empty, cookies are sent. ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); response_3.WaitForRequest(); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", response_3.http_request()->headers.at("Cookie")); } // Test how cookies are inherited in about:blank iframes. // // This is a variation of // NavigationCookiesBrowserTest.CookiesInheritedAboutBlank. Instead of // requesting an history navigation, a new navigation is requested from the main // frame. The navigation is cross-site instead of being same-site. IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, CookiesInheritedAboutBlank2) { // This test expects several cross-site navigation to happen. if (!AreAllSitesIsolatedForTesting()) return; using Response = net::test_server::ControllableHttpResponse; Response response_1(https_server(), "/response_1"); Response response_2(https_server(), "/response_2"); Response response_3(https_server(), "/response_3"); ASSERT_TRUE(https_server()->Start()); GURL url_a(https_server()->GetURL("a.com", "/title1.html")); GURL url_b(https_server()->GetURL("b.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url_a)); EXPECT_TRUE( ExecJs(shell(), JsReplace("let iframe = document.createElement('iframe');" "iframe.src = $1;" "document.body.appendChild(iframe);", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(ExecJs(shell(), R"( document.querySelector('iframe').src = "about:blank" )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* main_document = current_frame_host(); RenderFrameHostImpl* sub_document_1 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url::kAboutBlankURL, sub_document_1->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_1->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_1->GetSiteInstance()); // 0. The default state doesn't contain any cookies. EXPECT_EQ("", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_1, "document.cookie")); // 1. Set a cookie in the main document, it affects its child too. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0';")); EXPECT_EQ("a=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0", EvalJs(sub_document_1, "document.cookie")); // 2. Set a cookie in the child, it affects its parent too. EXPECT_TRUE(ExecJs(sub_document_1, "document.cookie = 'b=0';")); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0", EvalJs(sub_document_1, "document.cookie")); // 3. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_1, "fetch('/response_1');"); response_1.WaitForRequest(); EXPECT_EQ("a=0; b=0", response_1.http_request()->headers.at("Cookie")); // 4. Navigate the iframe elsewhere. EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_2 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ("a=0; b=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 5. Set a cookie in the main document. It doesn't affect its child. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'c=0';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("", EvalJs(sub_document_2, "document.cookie")); // 6. Set a cookie in the child. It doesn't affect its parent. EXPECT_TRUE(ExecJs(sub_document_2, "document.cookie = 'd=0; SameSite=none; Secure';")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("d=0", EvalJs(sub_document_2, "document.cookie")); // 7. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_2, "fetch('/response_2');"); response_2.WaitForRequest(); EXPECT_EQ("d=0", response_2.http_request()->headers.at("Cookie")); // 8. Ask the top-level, a.com frame to navigate the subframe to about:blank. EXPECT_TRUE(ExecJs(shell(), R"( document.querySelector('iframe').src = "about:blank"; )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_3 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url::kAboutBlankURL, sub_document_3->GetLastCommittedURL()); EXPECT_EQ(url::Origin::Create(url_a), sub_document_3->GetLastCommittedOrigin()); EXPECT_EQ(main_document->GetSiteInstance(), sub_document_3->GetSiteInstance()); EXPECT_EQ("a=0; b=0; c=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0", EvalJs(sub_document_3, "document.cookie")); // 9. Set cookie in the main document. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'e=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0", EvalJs(sub_document_3, "document.cookie")); // 10. Set cookie in the child document. EXPECT_TRUE(ExecJs(sub_document_3, "document.cookie = 'f=0';")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(main_document, "document.cookie")); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", EvalJs(sub_document_3, "document.cookie")); // 11. Checks cookies are sent while requesting resources. ExecuteScriptAsync(sub_document_3, "fetch('/response_3');"); response_3.WaitForRequest(); EXPECT_EQ("a=0; b=0; c=0; e=0; f=0", response_3.http_request()->headers.at("Cookie")); } // Test how cookies are inherited in data-URL iframes. IN_PROC_BROWSER_TEST_F(NavigationCookiesBrowserTest, CookiesInheritedDataUrl) { using Response = net::test_server::ControllableHttpResponse; Response response_1(https_server(), "/response_1"); Response response_2(https_server(), "/response_2"); Response response_3(https_server(), "/response_3"); ASSERT_TRUE(https_server()->Start()); GURL url_a(https_server()->GetURL("a.com", "/title1.html")); GURL url_b(https_server()->GetURL("b.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url_a)); EXPECT_TRUE(ExecJs(shell(), R"( let iframe = document.createElement("iframe"); iframe.src = "data:text/html,"; document.body.appendChild(iframe); )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* main_document = current_frame_host(); RenderFrameHostImpl* sub_document_1 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ("data:text/html,", sub_document_1->GetLastCommittedURL()); EXPECT_TRUE(sub_document_1->GetLastCommittedOrigin().opaque()); if (ShouldCreateSiteInstanceForDataUrls()) { EXPECT_NE(main_document->GetSiteInstance(), sub_document_1->GetSiteInstance()); EXPECT_EQ(main_document->GetSiteInstance()->group(), sub_document_1->GetSiteInstance()->group()); } else { EXPECT_EQ(main_document->GetSiteInstance(), sub_document_1->GetSiteInstance()); } // 1. Writing a cookie inside a data-URL document is forbidden. { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "*Failed to set the 'cookie' property on 'Document': Cookies are " "disabled inside 'data:' URLs.*"); ExecuteScriptAsync(sub_document_1, "document.cookie = 'a=0';"); ASSERT_TRUE(console_observer.Wait()); } // 2. Reading a cookie inside a data-URL document is forbidden. { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "*Failed to read the 'cookie' property from 'Document': Cookies are " "disabled inside 'data:' URLs.*"); ExecuteScriptAsync(sub_document_1, "document.cookie"); ASSERT_TRUE(console_observer.Wait()); } // 3. Set cookie in the main document. No cookies are sent when requested from // the data-URL. EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'a=0;SameSite=Lax'")); EXPECT_TRUE(ExecJs(main_document, "document.cookie = 'b=0;SameSite=Strict'")); GURL url_response_1 = https_server()->GetURL("a.com", "/response_1"); ExecuteScriptAsync(sub_document_1, JsReplace("fetch($1)", url_response_1)); response_1.WaitForRequest(); EXPECT_EQ(0u, response_1.http_request()->headers.count("Cookie")); // 4. Navigate the iframe elsewhere and back using history navigation. EXPECT_TRUE(ExecJs(sub_document_1, JsReplace("location.href = $1", url_b))); EXPECT_TRUE(WaitForLoadStop(web_contents())); web_contents()->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* sub_document_2 = main_document->child_at(0)->current_frame_host(); EXPECT_EQ(url_a, main_document->GetLastCommittedURL()); EXPECT_EQ("data:text/html,", sub_document_2->GetLastCommittedURL()); EXPECT_TRUE(sub_document_2->GetLastCommittedOrigin().opaque()); if (ShouldCreateSiteInstanceForDataUrls()) { EXPECT_NE(main_document->GetSiteInstance(), sub_document_2->GetSiteInstance()); EXPECT_EQ(main_document->GetSiteInstance()->GetSiteInstanceGroupId(), sub_document_2->GetSiteInstance()->GetSiteInstanceGroupId()); } else { EXPECT_EQ(main_document->GetSiteInstance(), sub_document_2->GetSiteInstance()); } // 5. Writing a cookie inside a data-URL document is still forbidden. { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "*Failed to set the 'cookie' property on 'Document': Cookies are " "disabled inside 'data:' URLs.*"); ExecuteScriptAsync(sub_document_2, "document.cookie = 'c=0';"); ASSERT_TRUE(console_observer.Wait()); } // 6. Reading a cookie inside a data-URL document is still forbidden. { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "*Failed to read the 'cookie' property from 'Document': Cookies are " "disabled inside 'data:' URLs.*"); ExecuteScriptAsync(sub_document_2, "document.cookie"); ASSERT_TRUE(console_observer.Wait()); } // 7. No cookies are sent when requested from the data-URL. GURL url_response_2 = https_server()->GetURL("a.com", "/response_2"); ExecuteScriptAsync(sub_document_2, JsReplace("fetch($1)", url_response_2)); response_2.WaitForRequest(); EXPECT_EQ(0u, response_2.http_request()->headers.count("Cookie")); } // Tests for validating URL rewriting behavior like chrome://newtab to // chrome-native://newtab. class NavigationUrlRewriteBrowserTest : public NavigationBaseBrowserTest { protected: static constexpr const char* kRewriteURL = "http://a.com/rewrite"; static constexpr const char* kNoAccessScheme = "no-access"; static constexpr const char* kNoAccessURL = "no-access://testing/"; class BrowserClient : public ContentBrowserTestContentBrowserClient { public: void BrowserURLHandlerCreated(BrowserURLHandler* handler) override { handler->AddHandlerPair(RewriteUrl, BrowserURLHandlerImpl::null_handler()); fake_url_loader_factory_ = std::make_unique<FakeNetworkURLLoaderFactory>( "HTTP/1.1 200 OK\nContent-Type: text/html\n\n", "This is a test", /* network_accessed */ true, net::OK); } mojo::PendingRemote<network::mojom::URLLoaderFactory> CreateNonNetworkNavigationURLLoaderFactory( const std::string& scheme, FrameTreeNodeId frame_tree_node_id) override { if (scheme == kNoAccessScheme) { mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote; fake_url_loader_factory_->Clone( pending_remote.InitWithNewPipeAndPassReceiver()); return pending_remote; } return {}; } static bool RewriteUrl(GURL* url, BrowserContext* browser_context) { if (*url == GURL(kRewriteURL)) { *url = GURL(kNoAccessURL); return true; } return false; } private: std::unique_ptr<FakeNetworkURLLoaderFactory> fake_url_loader_factory_; }; NavigationUrlRewriteBrowserTest() { url::AddStandardScheme(kNoAccessScheme, url::SCHEME_WITH_HOST); url::AddNoAccessScheme(kNoAccessScheme); // This test needs to use an unassigned SiteInstance for kNoAccessScheme, // which requires adding it as an empty document scheme. url::AddEmptyDocumentScheme(kNoAccessScheme); } void SetUpOnMainThread() override { NavigationBaseBrowserTest::SetUpOnMainThread(); ASSERT_TRUE(embedded_test_server()->Start()); browser_client_ = std::make_unique<BrowserClient>(); } void TearDownOnMainThread() override { browser_client_.reset(); NavigationBaseBrowserTest::TearDownOnMainThread(); } GURL GetRewriteToNoAccessURL() const { return GURL(kRewriteURL); } private: std::unique_ptr<BrowserClient> browser_client_; url::ScopedSchemeRegistryForTests scoped_registry_; }; // Tests navigating to a URL that gets rewritten to a "no access" URL. This // mimics the behavior of navigating to special URLs like chrome://newtab and // chrome://history which get rewritten to "no access" chrome-native:// URLs. IN_PROC_BROWSER_TEST_F(NavigationUrlRewriteBrowserTest, RewriteToNoAccess) { // Perform an initial navigation. { TestNavigationObserver observer(web_contents()); GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_FALSE(observer.last_initiator_origin().has_value()); } // Navigate to the URL that will get rewritten to a "no access" URL. { TestNavigationObserver observer(web_contents()); // Note: We are using LoadURLParams here because we need to have the // initiator_origin set and NavigateToURL() does not do that. NavigationController::LoadURLParams params(GetRewriteToNoAccessURL()); params.initiator_origin = current_frame_host()->GetLastCommittedOrigin(); web_contents()->GetController().LoadURLWithParams(params); web_contents()->Focus(); observer.Wait(); EXPECT_EQ(GURL(kNoAccessURL), observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_TRUE(observer.last_initiator_origin().has_value()); } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigation) { WebContents* wc = shell()->web_contents(); GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#frag1"); GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html#frag2"); NavigationHandleCommitObserver navigation_0(wc, url1); NavigationHandleCommitObserver navigation_1(wc, url2); EXPECT_TRUE(NavigateToURL(shell(), url1)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); EXPECT_TRUE(NavigateToURL(shell(), url2)); // The NavigationEntry changes on a same-document navigation. EXPECT_NE(web_contents()->GetController().GetLastCommittedEntry(), entry); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); EXPECT_TRUE(navigation_1.was_same_document()); } // Some navigations are not allowed, such as when they fail the content security // policy, or for trying to load about:srcdoc in the main frame. These result in // us redirecting the navigation to an error page via // RenderFrameHostImpl::FailedNavigation(). // Repeating the request with a different URL fragment results in attempting a // same-document navigation, but error pages do not support such navigations. In // this case treat each failed navigation request as a separate load, with the // resulting navigation being performed as a cross-document navigation. This is // regression test for https://crbug.com/1018385. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigationOnBlockedPage) { GURL url1("about:srcdoc#0"); GURL url2("about:srcdoc#1"); NavigationHandleCommitObserver navigation_0(web_contents(), url1); NavigationHandleCommitObserver navigation_1(web_contents(), url2); // Big warning: about:srcdoc is not supposed to be valid browser-initiated // main-frame navigation, it is currently blocked by the NavigationRequest. // It is used here to reproduce bug https://crbug.com/1018385. Please avoid // copying this kind of navigation in your own tests. EXPECT_FALSE(NavigateToURL(shell(), url1)); EXPECT_FALSE(NavigateToURL(shell(), url2)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); EXPECT_FALSE(navigation_1.was_same_document()); } // This navigation is allowed by the browser, but the network will not be able // to connect to the site, so the NavigationRequest fails on the browser side // and is redirected to an error page. Performing another navigation should // make the full attempt again, in case the network request succeeds this time. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigationOnBadServerErrorPage) { GURL url1("http://badserver.com:9/"); GURL url2("http://badserver.com:9/#1"); NavigationHandleCommitObserver navigation_0(web_contents(), url1); NavigationHandleCommitObserver navigation_1(web_contents(), url2); // The navigation is okay from the browser's perspective, so NavigateToURL() // will return true. But the network request ultimately fails, so the request // is redirected to an error page. EXPECT_FALSE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); // The 2nd request shares a URL but it should be another cross-document // navigation, rather than trying to navigate inside the error page. EXPECT_FALSE(NavigateToURL(shell(), url2)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_FALSE(navigation_1.was_same_document()); } // This navigation is allowed by the browser, and the request to the server is // successful, but it returns 404 error headers, and (optionally) an error page. // When another request is made for the same page but with a different fragment, // the browser will attempt to perform a same-document navigation but that // navigation is intended for the actual document not the error page that has // been loaded instead. A same-document navigation in the renderer-loaded error // page should be performed as a cross-document navigation in order to attempt // to reload the page. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigationOn404ErrorPage) { // This case is a non-empty 404 page. It makes different choices about where // to load the page on a same-document navigation. { GURL url1 = embedded_test_server()->GetURL("a.com", "/page404.html"); GURL url2 = embedded_test_server()->GetURL("a.com", "/page404.html#1"); NavigationHandleCommitObserver navigation_0(web_contents(), url1); NavigationHandleCommitObserver navigation_1(web_contents(), url2); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); // This is another navigation to the non-existent URL, but with a different // fragment. We have successfully loaded content from a.com. The fact that // it is 404 response does not mean it is an error page, since the term // "error page" is used for cases where the browser encounters an error // loading a document from the origin. HTTP responses with >400 status codes // are just like regular documents from the origin and we render their // response body just like we would a 200 response. This is why it can make // sense for a same document navigation to be performed from a 404 page. EXPECT_TRUE(NavigateToURL(shell(), url2)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_TRUE(navigation_1.was_same_document()); } // This case is an empty 404 page. It makes different choices about where // to load the page on a same-document navigation. Since the server has only // replied with an error, the browser will display its own error page and // therefore it is not one coming from the server's origin. { GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html"); GURL url2 = embedded_test_server()->GetURL("a.com", "/empty404.html#1"); NavigationHandleCommitObserver navigation_0(web_contents(), url1); NavigationHandleCommitObserver navigation_1(web_contents(), url2); EXPECT_FALSE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); // This is another navigation to the non-existent URL, but with a different // fragment. Since we did not load a document from the server (we got // `false` from `NavigateToURL()`) there is no server-provided document to // navigate within. The result should be a cross-document navigation in // order to attempt to load the document at the given path from the server // again. EXPECT_FALSE(NavigateToURL(shell(), url2)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_FALSE(navigation_1.was_same_document()); } // This case is also an empty 404 page, but we do replaceState and pushState // afterwards, creating successful same-document navigations. { // Navigate to empty 404, committing an error page. GURL url1 = embedded_test_server()->GetURL("a.com", "/empty404.html"); NavigationHandleCommitObserver navigation(web_contents(), url1); EXPECT_FALSE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation.has_committed()); EXPECT_FALSE(navigation.was_same_document()); // replaceState on an error page, without changing the URL. { FrameNavigateParamsCapturer capturer(main_frame()); capturer.set_wait_for_load(false); EXPECT_TRUE(ExecJs(shell(), "history.replaceState('foo', '')")); capturer.Wait(); EXPECT_TRUE(capturer.is_same_document()); } // pushState on an error page, without changing the URL. { FrameNavigateParamsCapturer capturer(main_frame()); capturer.set_wait_for_load(false); EXPECT_TRUE(ExecJs(shell(), "history.pushState('foo', '')")); capturer.Wait(); EXPECT_TRUE(capturer.is_same_document()); } } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigationFromCrossDocumentRedirect) { WebContents* wc = shell()->web_contents(); GURL url0 = embedded_test_server()->GetURL("/title1.html#frag1"); GURL url1 = embedded_test_server()->GetURL("/server-redirect?title1.html#frag2"); GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2"); NavigationHandleCommitObserver navigation_0(wc, url0); NavigationHandleCommitObserver navigation_1(wc, url1); NavigationHandleCommitObserver navigation_2(wc, url2); EXPECT_TRUE(NavigateToURL(shell(), url0)); // Since the redirect does not land at the URL we passed in, we get a false // return here. EXPECT_FALSE(NavigateToURL(shell(), url1)); // The navigation to |url1| is redirected and so |url1| does not commit. Then // the resulting navigation to |url2| lands at the same document URL as |url0| // which would be a same-document navigation if there wasn't a redirect // involved. But since it started as a cross-document navigation it results in // loading a new document instead of doing a same-document navigation. EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_1.has_committed()); EXPECT_TRUE(navigation_2.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); EXPECT_FALSE(navigation_1.was_same_document()); EXPECT_FALSE(navigation_2.was_same_document()); EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2); // Redirect should not record a ReceivedResponse event. EXPECT_EQ(1u, test_ukm_recorder() .GetEntriesByName("Navigation.ReceivedResponse") .size()); } // 1. The browser navigates to a.html. // 2. The renderer uses history.pushState() to change the URL of the current // document from a.html to b.html. // 3. The browser tries to perform a same-document navigation to a.html#foo, // since it did not hear about the document's URL changing yet. When it gets // to the renderer, we discover a race has happened. // 4. Meanwhile, the browser hears about the URL change to b.html and applies // it. // Now - how do we resolve the race? // 5. We will reorder the a.html#foo navigation to start over in the browser // after the b.html navigation. // Technically, this is still a same-document navigation! The URL changed but // the document did not. Currently, however, the browser only considers the URL // when performing a non-history navigation to decide if it's a same-document // navigation, so.. // 6. The browser will perform a cross-document navigation to a.html#foo. // // TODO(crbug.com/40799231): Test is flaky on various platforms. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, DISABLED_SameDocumentNavigationRacesPushStateURLChange) { WebContents* wc = shell()->web_contents(); GURL url0 = embedded_test_server()->GetURL("/title1.html"); GURL url1 = embedded_test_server()->GetURL("/title2.html"); GURL url2 = embedded_test_server()->GetURL("/title1.html#frag2"); NavigationHandleCommitObserver navigation_0(wc, url0); NavigationHandleCommitObserver navigation_1(wc, url1); NavigationHandleCommitObserver navigation_2(wc, url2); // Start at `url0`. EXPECT_TRUE(NavigateToURL(shell(), url0)); // Have the renderer `history.pushState()` to `url1`, which leaves it on the // `url0` document, but with a different URL now. ExecuteScriptAsync(shell(), JsReplace("history.pushState('', '', $1);" "window.location.href == $1;", url1)); // The browser didn't hear about the change yet. EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url0); { // We will wait for 2 navigations: one will be the pushState() and the other // will be the navigation to `url2` started below. TestNavigationObserver nav_observer(wc, 2); // Start a same-document navigation to url2 that is racing with the // renderer's history.pushState(). shell()->LoadURL(url2); nav_observer.Wait(); } // The last navigation to resolve is the one to `url2` as it's reordered to // come after the race with the already-completed history.pushState(). EXPECT_EQ(wc->GetPrimaryMainFrame()->GetLastCommittedURL(), url2); // Navigation 0 was a cross-document navigation, to initially load the // document. EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); // Navigation 1 was a same-document navigation, from the renderer's // history.pushState() call. EXPECT_TRUE(navigation_1.has_committed()); EXPECT_TRUE(navigation_1.was_same_document()); // Navigation 2 was restarted and came after. When it restarted, it saw the // URL did not match and did a cross-document navigation. Technically the same // document was still loaded from `url0`, but the browser makes its choice // on the document's current URL. EXPECT_TRUE(navigation_2.has_committed()); EXPECT_FALSE(navigation_2.was_same_document()); } class GetEffectiveUrlClient : public ContentBrowserTestContentBrowserClient { public: GURL GetEffectiveURL(content::BrowserContext* browser_context, const GURL& url) override { if (effective_url_) return *effective_url_; return url; } bool IsSuitableHost(RenderProcessHost* process_host, const GURL& site_url) override { if (!disallowed_process_id_) return true; return process_host->GetDeprecatedID() != disallowed_process_id_; } void set_effective_url(const GURL& url) { effective_url_ = url; } void set_disallowed_process(int id) { disallowed_process_id_ = id; } private: std::optional<GURL> effective_url_; int disallowed_process_id_ = 0; }; // While a document is open, state in the browser may change such that loading // the document would choose a different SiteInstance. A cross-document // navigation would pick up this different SiteInstance, but a same-document // navigation should not. It should just navigate inside the currently loaded // document instead of reloading the document. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentNavigationWhenSiteInstanceWouldChange) { auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1"); GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2"); GetEffectiveUrlClient new_client; NavigationHandleCommitObserver navigation_0(wc, url0); EXPECT_TRUE(NavigateToURL(shell(), url0)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame(); RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess(); // When we both change the effective URL and also disallow the current // renderer process, a new load of the current document would get a different // SiteInstance. GURL modified_url0 = embedded_test_server()->GetURL("c.com", "/title1.html#ref1"); new_client.set_effective_url(modified_url0); new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID()); NavigationHandleCommitObserver navigation_1(wc, url1); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_TRUE(navigation_1.was_same_document()); // The RenderFrameHost should not have changed, we should perform the // navigation in the currently loaded document. EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame()); EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess()); } // This tests the same ideas as the above test except in this case the same- // document navigation is done through a history navigation, which exercises // different codepaths in the NavigationControllerImpl. IN_PROC_BROWSER_TEST_F( NavigationBrowserTest, SameDocumentHistoryNavigationWhenSiteInstanceWouldChange) { auto* wc = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL url0 = embedded_test_server()->GetURL("a.com", "/title1.html#ref1"); GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html#ref2"); NavigationHandleCommitObserver navigation_0(wc, url0); NavigationHandleCommitObserver navigation_1(wc, url1); GetEffectiveUrlClient new_client; EXPECT_TRUE(NavigateToURL(shell(), url0)); EXPECT_TRUE(navigation_0.has_committed()); EXPECT_FALSE(navigation_0.was_same_document()); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_TRUE(navigation_1.was_same_document()); RenderFrameHost* main_frame_host = wc->GetPrimaryMainFrame(); RenderProcessHost* main_frame_process_host = main_frame_host->GetProcess(); // When we both change the effective URL and also disallow the current // renderer process, a new load of the current document would get a different // SiteInstance. GURL modified_url0 = embedded_test_server()->GetURL("c.com", "/title1.html#ref1"); new_client.set_effective_url(modified_url0); new_client.set_disallowed_process(main_frame_process_host->GetDeprecatedID()); // Navigates to the same-document. Since the SiteInstance changed, we would // normally try isolate this navigation by using a different RenderProcessHost // and RenderFrameHost. But since it is same-document, we want to avoid that // and perform the navigation inside the loaded |url0| document. wc->GetController().GoBack(); EXPECT_TRUE(WaitForLoadStop(wc)); // The RenderFrameHost should not have changed, we should perform the // navigation in the currently loaded document. EXPECT_EQ(main_frame_host, wc->GetPrimaryMainFrame()); EXPECT_EQ(main_frame_process_host, wc->GetPrimaryMainFrame()->GetProcess()); } // Verify that actual renderer-initiated navigations to about:blank#blocked // are respected, even though both the browser and renderer rewrite some illegal // navigations to that URL as well. See https://crbug.com/40066983. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentHashNavigationToBlockedFragmentAllowed) { const GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); TestNavigationObserver blank_observer(web_contents()); EXPECT_TRUE(ExecJs(shell(), "location.href = 'about:blank';")); blank_observer.Wait(); GURL blocked_url = GURL(kBlockedURL); TestNavigationObserver blocked_observer(web_contents()); EXPECT_TRUE(ExecJs(shell(), JsReplace("location.href = $1", blocked_url))); blocked_observer.Wait(); // If the browser process receives a request to same-document navigate to // about:blank#blocked, the URL should be used and not ignored (as in a // blocked case like the SameDocumentLongURLHashNavigation test). EXPECT_EQ(blocked_url, web_contents()->GetLastCommittedURL()); } // Verify that same-document navigations from about:blank to an excessively long // fragment do not crash the browser. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentAboutBlankLongURLHashNavigation) { const GURL blank_url(url::kAboutBlankURL); EXPECT_TRUE(NavigateToURL(shell(), blank_url)); std::string long_url = "#"; long_url.append(2 * url::kMaxURLChars, 'a'); EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url))); // If the renderer attempts to navigate same-document from about:blank to a // too long hash (>2 MB), the URL (and base URL) will be blocked and rewritten // in the renderer to avoid the Mojo serialization limit. Ensure that the // browser process does not crash due to an empty base URL, and that the // blocked URL is used, unlike in the non-about:blank case in the // SameDocumentLongURLHashNavigation test. // TODO(crbug.com/40067230): Ideally this would be blocked earlier in the // renderer process, failing the navigation. EXPECT_EQ(GURL(kBlockedURL), web_contents()->GetLastCommittedURL()); // The renderer process considers the same-document navigation to the long URL // to have successfully completed. EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURLHashNavigation) { const GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); std::string long_url = "#"; long_url.append(2 * url::kMaxURLChars, 'a'); EXPECT_TRUE(ExecJs(shell(), JsReplace("location = $1", long_url))); // If the browser process receives a request to same-document navigate to a // too long URL (>2 MB), it simply pretends that the renderer performed a // same-document navigation to the currently committed URL (previously, it was // mapped to about:blank#blocked, which could be confusing). // TODO(crbug.com/40067230): Ideally this would be blocked in the renderer // instead of having special browser-side handling. EXPECT_EQ(url, web_contents()->GetLastCommittedURL()); // The renderer process enforces no such limit and should consider the // same-document navigation to have successfully completed. EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURLPushState) { const GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); std::string long_url = "#"; long_url.append(2 * url::kMaxURLChars, 'a'); EXPECT_TRUE(ExecJs( shell(), JsReplace("history.pushState('state', '', $1)", long_url))); // If the browser process receives a request to same-document navigate to a // too long URL (>2 MB), it simply pretends that the renderer performed a // same-document navigation to the currently committed URL (previously, it was // mapped to about:blank#blocked, which could be confusing). // TODO(crbug.com/40067230): Ideally this would be blocked in the renderer // instead of having special browser-side handling. EXPECT_EQ(url, web_contents()->GetLastCommittedURL()); // The renderer process enforces no such limit and should consider the // same-document navigation to have successfully completed. EXPECT_EQ(long_url, EvalJs(web_contents(), "location.hash")); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURL204PopupHashNavigation) { const GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Open a popup window with a navigation that will result in a 204. This will // result in a WebContents where the last committed URL is the empty URL. const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent")); ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url))); Shell* opened_shell = new_shell_observer.GetShell(); EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); std::string long_url = "#"; long_url.append(2 * url::kMaxURLChars, 'a'); EXPECT_TRUE(ExecJs(opened_shell, JsReplace("location = $1", long_url))); // Hash navigations in a popup in this state (incorrectly) perform a // cross-document navigation. This is because the check for whether or not to // perform a same-document navigation uses the initial empty Document's actual // URL (which is, surprisingly enough, the empty URL) rather than URL the web // platform generally sees (which is about:blank). As a result, the check ends // up comparing the empty URL against the completed URL of about:blank#..., // which means the URLs are not equal ignoring fragments, and Blink performs a // cross-document navigation instead. // // TODO(crbug.com/40922971): This probably should be fixed to be treated as a // same-document navigation. EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); EXPECT_EQ(GURL(kBlockedURL), opened_shell->web_contents()->GetLastCommittedURL()); EXPECT_EQ(kBlockedURL, EvalJs(opened_shell->web_contents(), "location.href")); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentLongURL204PopupPushState) { const GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Open a popup window with a navigation that will result in a 204. This will // result in a WebContents where the last committed URL is the empty URL. const GURL nocontent_url(embedded_test_server()->GetURL("/nocontent")); ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", nocontent_url))); Shell* opened_shell = new_shell_observer.GetShell(); EXPECT_TRUE(WaitForLoadStop(opened_shell->web_contents())); std::string long_url = "#"; long_url.append(2 * url::kMaxURLChars, 'a'); // Blink incorrectly disallows pushState() because the security check is // broken, since the security check uses the initial empty Document's actual // URL (which is, surprisingly enough, the empty URL) rather than the URL the // web platform generally sees (which is about:blank). // // TODO(crbug.com/40922971): This pushState() should probably be allowed. EXPECT_EQ( "SecurityError", EvalJs( opened_shell, JsReplace( "try { history.pushState('state', '', $1) } catch (e) { e.name }", long_url))); } // Ensure that no crash occurs when doing a same-document navigation within a // site-less SiteInstance, such as for a browser-initiated about:blank. // See https://crbug.com/359807735. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameDocumentSitelessNavigation) { WebContents* web_contents = shell()->web_contents(); GURL url1 = GURL("about:blank#1"); GURL url2 = GURL("about:blank#2"); NavigationHandleCommitObserver navigation_1(web_contents, url1); NavigationHandleCommitObserver navigation_2(web_contents, url2); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(NavigateToURL(shell(), url2)); EXPECT_TRUE(navigation_1.has_committed()); EXPECT_TRUE(navigation_2.has_committed()); EXPECT_FALSE(navigation_1.was_same_document()); EXPECT_TRUE(navigation_2.was_same_document()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NonDeterministicUrlRewritesUseLastUrl) { // Lambda expressions cannot be assigned to function pointers if they use // captures, so track how many times the handler is called using a non-const // static variable. static int rewrite_count; rewrite_count = 0; BrowserURLHandler::URLHandler handler_method = [](GURL* url, BrowserContext* browser_context) { GURL::Replacements replace_path; if (rewrite_count > 0) { replace_path.SetPathStr("title2.html"); } else { replace_path.SetPathStr("title1.html"); } *url = url->ReplaceComponents(replace_path); rewrite_count++; return true; }; BrowserURLHandler::GetInstance()->AddHandlerPair( handler_method, BrowserURLHandler::null_handler()); TestNavigationObserver observer(web_contents()); shell()->LoadURL(embedded_test_server()->GetURL("/virtual-url.html")); observer.Wait(); EXPECT_EQ("/title2.html", observer.last_navigation_url().path()); EXPECT_EQ(2, rewrite_count); } // Create two windows. When the second is deleted, it initiates a navigation in // the first. This is a situation where the navigation has an initiator frame // token, but no corresponding RenderFrameHost. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, RendererInitiatedCrossWindowNavigationInPagehide) { GURL url(embedded_test_server()->GetURL("/empty.html")); GURL always_referrer_url(embedded_test_server()->GetURL( "/set-header?Referrer-Policy: unsafe-url")); // Setup the opener window. EXPECT_TRUE(NavigateToURL(shell(), url)); // Setup the openee window; ShellAddedObserver new_shell_observer; EXPECT_TRUE( ExecJs(shell(), JsReplace("window.open($1);", always_referrer_url))); Shell* openee_shell = new_shell_observer.GetShell(); EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); // When deleted, the openee will initiate a navigation in its opener. EXPECT_TRUE(ExecJs(openee_shell, R"( window.addEventListener("pagehide", () => { opener.location.href = "about:blank"; }) )")); RenderFrameHost* openee_rfh = static_cast<WebContentsImpl*>(openee_shell->web_contents()) ->GetPrimaryMainFrame(); // Issue a KeepAlive for the navigation state so that the PolicyContainerHost // will still exist after the initiator RenderFrameHost is gone. mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle> keep_alive; static_cast<RenderFrameHostImpl*>(openee_rfh) ->IssueKeepAliveHandle(keep_alive.InitWithNewPipeAndPassReceiver()); auto initiator_global_token = openee_rfh->GetGlobalFrameToken(); base::RunLoop loop; DidStartNavigationCallback callback( web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { auto* request = NavigationRequest::From(handle); const std::optional<blink::LocalFrameToken>& frame_token = request->GetInitiatorFrameToken(); EXPECT_TRUE(frame_token.has_value()); EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); EXPECT_EQ(initiator_global_token.child_id, request->GetInitiatorProcessId()); auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken( request->GetInitiatorProcessId(), *frame_token); ASSERT_FALSE(initiator_rfh); // Even if the initiator RenderFrameHost is gone, its policy container // should still be around since the LocalFrame has not been destroyed // yet. PolicyContainerHost* initiator_policy_container = RenderFrameHostImpl::GetPolicyContainerHost( base::OptionalToPtr(frame_token), request->GetInitiatorProcessId(), web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); ASSERT_TRUE(initiator_policy_container); ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policy_container->referrer_policy()); // Even if the initiator RenderFrameHost is gone, the navigation request // (to "about:blank") should have inherited its policy container. auto* initiator_policies = request->GetInitiatorPolicyContainerPolicies(); ASSERT_TRUE(initiator_policies); ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policies->referrer_policy); loop.Quit(); })); // Delete the openee, which trigger the navigation in the opener. openee_shell->Close(); loop.Run(); } // A document initiates a form submission in another frame, then deletes itself. // Check the initiator frame token. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FormSubmissionThenDeleteFrame) { GURL url(embedded_test_server()->GetURL("/empty.html")); GURL always_referrer_url(embedded_test_server()->GetURL( "/set-header?Referrer-Policy: unsafe-url")); // Setup the opener window. EXPECT_TRUE(NavigateToURL(shell(), url)); // Setup the openee window; ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", url))); Shell* openee_shell = new_shell_observer.GetShell(); // Create a 'named' iframe in the first window. This will be the target of the // form submission. EXPECT_TRUE(ExecJs(shell(), R"( new Promise(resolve => { let iframe = document.createElement("iframe"); iframe.onload = resolve; iframe.name = 'form-submission-target'; iframe.src = location.href; document.body.appendChild(iframe); }); )")); // Create an iframe in the second window. It will be initiating a form // submission and removing itself before the scheduled form navigation occurs. // This iframe will have referrer policy "unsafe-url". EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); EXPECT_TRUE(ExecJs(openee_shell, JsReplace(R"( new Promise(resolve => { let iframe = document.createElement('iframe'); iframe.onload = resolve; iframe.src = $1; document.body.appendChild(iframe); }); )", always_referrer_url))); EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); RenderFrameHost* initiator_rfh = static_cast<WebContentsImpl*>(openee_shell->web_contents()) ->GetPrimaryMainFrame() ->child_at(0) ->current_frame_host(); auto initiator_global_token = initiator_rfh->GetGlobalFrameToken(); base::RunLoop loop; DidStartNavigationCallback callback( web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { auto* request = NavigationRequest::From(handle); ASSERT_TRUE(request->IsPost()); const std::optional<blink::LocalFrameToken>& frame_token = request->GetInitiatorFrameToken(); EXPECT_TRUE(frame_token.has_value()); EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); EXPECT_EQ(initiator_global_token.child_id, request->GetInitiatorProcessId()); auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken( request->GetInitiatorProcessId(), frame_token.value()); ASSERT_FALSE(deleted_initiator_rfh); // Even if the initiator RenderFrameHost is gone, its policy container // should still be around since the LocalFrame has not been destroyed // yet. PolicyContainerHost* initiator_policy_container = RenderFrameHostImpl::GetPolicyContainerHost( base::OptionalToPtr(frame_token), request->GetInitiatorProcessId(), web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); ASSERT_TRUE(initiator_policy_container); EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policy_container->referrer_policy()); auto* initiator_policies = request->GetInitiatorPolicyContainerPolicies(); ASSERT_TRUE(initiator_policies); ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policies->referrer_policy); loop.Quit(); })); // Initiate a form submission into the first window and delete the initiator. EXPECT_TRUE(WaitForLoadStop(openee_shell->web_contents())); ExecuteScriptAsync(initiator_rfh, R"( let input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", "my_token"); input.setAttribute("value", "my_value"); // Schedule a form submission navigation (which will occur in a separate // task). let form = document.createElement('form'); form.appendChild(input); form.setAttribute("method", "POST"); form.setAttribute("action", "about:blank"); form.setAttribute("target", "form-submission-target"); document.body.appendChild(form); form.submit(); // Delete this frame before the scheduled navigation occurs in the target // frame. parent.document.querySelector("iframe").remove(); )"); loop.Run(); } // Same as the previous test, but for a remote frame navigation: // A document initiates a form submission in a cross-origin frame, then deletes // itself. Check the initiator frame token. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FormSubmissionInRemoteFrameThenDeleteFrame) { GURL url(embedded_test_server()->GetURL("/empty.html")); GURL cross_origin_always_referrer_url(embedded_test_server()->GetURL( "foo.com", "/set-header?Referrer-Policy: unsafe-url")); // Setup the main page. EXPECT_TRUE(NavigateToURL(shell(), url)); // Create a cross origin child iframe. This iframe will embed another iframe, // which will initiate the navigation. The only purpose of this iframe is to // allow its child to delete itself by issuing // parent.document.querySelector("iframe").remove(); // (The main frame cannot do it because it is cross-origin.) EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); EXPECT_TRUE(ExecJs(shell(), JsReplace(R"( let iframe = document.createElement('iframe'); iframe.src = $1; document.body.appendChild(iframe); )", cross_origin_always_referrer_url))); EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); RenderFrameHostImpl* middle_rfh = current_frame_host()->child_at(0)->current_frame_host(); // Now create a grandchild iframe, which is same-origin with the parent (but // cross-origin with the grandparent). The grandchild will initiate a form // submission in the top frame and remove itself before the scheduled form // navigation occurs. This iframe will have referrer policy "unsafe-url". EXPECT_TRUE(ExecJs(middle_rfh, JsReplace(R"( let iframe = document.createElement('iframe'); iframe.src = $1; document.body.appendChild(iframe); )", cross_origin_always_referrer_url))); EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); RenderFrameHost* initiator_rfh = middle_rfh->child_at(0)->current_frame_host(); auto initiator_global_token = initiator_rfh->GetGlobalFrameToken(); base::RunLoop loop; DidStartNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { auto* request = NavigationRequest::From(handle); ASSERT_TRUE(request->IsPost()); const std::optional<blink::LocalFrameToken>& frame_token = request->GetInitiatorFrameToken(); EXPECT_TRUE(frame_token.has_value()); EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); EXPECT_EQ(initiator_global_token.child_id, request->GetInitiatorProcessId()); auto* deleted_initiator_rfh = RenderFrameHostImpl::FromFrameToken( request->GetInitiatorProcessId(), frame_token.value()); ASSERT_FALSE(deleted_initiator_rfh); // Even if the initiator RenderFrameHost is gone, its policy container // should still be around since the LocalFrame has not been destroyed // yet. PolicyContainerHost* initiator_policy_container = RenderFrameHostImpl::GetPolicyContainerHost( base::OptionalToPtr(frame_token), request->GetInitiatorProcessId(), web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); ASSERT_TRUE(initiator_policy_container); EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policy_container->referrer_policy()); EXPECT_EQ( network::mojom::ReferrerPolicy::kAlways, request->GetInitiatorPolicyContainerPolicies()->referrer_policy); loop.Quit(); })); // Initiate a form submission into the main frame and delete the initiator. ExecuteScriptAsync(initiator_rfh, R"( let input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", "my_token"); input.setAttribute("value", "my_value"); // Schedule a form submission navigation (which will occur in a separate // task). let form = document.createElement('form'); form.appendChild(input); form.setAttribute("method", "POST"); form.setAttribute("action", "about:blank"); form.setAttribute("target", "_top"); document.body.appendChild(form); form.submit(); // Delete this frame before the scheduled navigation occurs in the main // frame. parent.document.querySelector("iframe").remove(); )"); loop.Run(); } // A class to intercept RemoteFrameHost IPCs, specifically OpenURL. When an // OpenURL IPC is received, this interceptor closes the initiator's Shell, // `shell_to_close`, and ensures the corresponding process exits before // proceeding with the OpenURL call. class InitiatorClosingOpenURLInterceptor : public blink::mojom::RemoteFrameHostInterceptorForTesting { public: // `this` takes ownership of `shell_to_close` and eventually deletes it. InitiatorClosingOpenURLInterceptor(content::RenderFrameProxyHost* proxy_host, std::unique_ptr<Shell> shell_to_close, RenderProcessHost* renderer_to_exit) : shell_to_close_(std::move(shell_to_close)), renderer_to_exit_(renderer_to_exit), swapped_impl_(std::make_unique<mojo::test::ScopedSwapImplForTesting< blink::mojom::RemoteFrameHost>>( proxy_host->frame_host_receiver_for_testing(), this)) {} ~InitiatorClosingOpenURLInterceptor() override = default; blink::mojom::RemoteFrameHost* GetForwardingInterface() override { return swapped_impl_->old_impl(); } // This closes `shell_to_close_` and causes `renderer_to_exit_` to exit // before forwarding the call to the RenderFrameProxyHost. This mimics the // case where the frame that sent the OpenURL gets closed before the IPC // reaches its destination. Once the OpenURL IPC is sent, the proxy should // receive it, even if the sender is gone. void OpenURL(blink::mojom::OpenURLParamsPtr params) override { // `Close()` internally deletes the pointer, so it must be released so // `shell_to_close_` doesn't point to a deleted value. shell_to_close_.release()->Close(); renderer_to_exit_->Shutdown(content::RESULT_CODE_KILLED); GetForwardingInterface()->OpenURL(std::move(params)); // Delete the swapped impl while the real RenderFrameProxyHost still exists, // since we only need to intercept a single OpenURL call. The next task may // delete the real impl. swapped_impl_.reset(); // Clear the other raw_ptrs to avoid dangling pointers. renderer_to_exit_ = nullptr; } private: std::unique_ptr<Shell> shell_to_close_; raw_ptr<RenderProcessHost> renderer_to_exit_; // The `swapped_impl_` is a unique_ptr, so the member can be deleted before // `this` gets destroyed. The original implementation would normally be // swapped back in when `this` is destroyed. However, in this test, the // `RenderFrameProxyHost` is deleted shortly after the OpenURL IPC is handled, // and relying on normal scoper cleanup would cause a use-after-free. To avoid // this, we early delete the `swapped_impl_` to swap back the original // implementation as soon as `OpenURL()` has been processed. std::unique_ptr< mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost>> swapped_impl_; }; // Test the case that once an OpenURL IPC is sent, it is received and the // navigation occurs even if the sender is deleted while the IPC is in flight. // This test opens a main frame, which opens a cross-site popup. The test then // does a form submission to the popup and closes the main frame. // Unlike FormSubmissionInRemoteFrameThenDeleteFrame, the initiator is the last // (and only) frame of that SiteInstance. Deleting it usually causes proxies in // the same SiteInstanceGroup to be deleted, meaning the OpenURL IPC may never // be received. // // Fails on linux-bfcache-rel and android-bfcache-rel. See crbug.com/336671248. #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_ANDROID) #define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \ DISABLED_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL #else #define MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL \ FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL #endif IN_PROC_BROWSER_TEST_F( NavigationBrowserTest, MAYBE_FormSubmissionInRemoteFrameSenderDeletedBeforeReceivingOpenURL) { // We crash a renderer in the OpenURL interceptor. content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; content::IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); // Get a unique_ptr to the shell, which will be used to transfer ownership // later on. std::unique_ptr<Shell> shell_a = base::WrapUnique(CreateBrowser()); // Setup the main page, a.com. The referrer policy is needed to test the // keep alive part of PolicyContainerHost. GURL always_referrer_url_a(embedded_test_server()->GetURL( "a.com", "/set-header?Referrer-Policy: unsafe-url")); EXPECT_TRUE(NavigateToURL(shell_a.get(), always_referrer_url_a)); EXPECT_TRUE(WaitForLoadStop(shell_a->web_contents())); // The a.com's RenderFrameHost will be the initiator of the form submission. RenderFrameHostImpl* rfh_a = static_cast<RenderFrameHostImpl*>( shell_a->web_contents()->GetPrimaryMainFrame()); // Create a cross origin popup that will be the target of the form submission. // This is cross-site so we can test the case where the last RenderFrameHost // for the initiator's site is gone when the initiator deletes itself, causing // all proxies in its SiteInstanceGroup to potentially delete themselves // before the OpenURL call can be received. // However, we also need to be able to navigate the target frame, so this is // opened as a popup. GURL url_b(embedded_test_server()->GetURL("b.com", "/empty.html")); WebContentsImpl* web_contents_b = nullptr; { WebContentsAddedObserver observer_b; ASSERT_TRUE(ExecJs(rfh_a, JsReplace("window.open($1, '_bpopup')", url_b))); web_contents_b = static_cast<WebContentsImpl*>(observer_b.GetWebContents()); } WaitForLoadStop(web_contents_b); base::RunLoop loop; auto initiator_global_token = rfh_a->GetGlobalFrameToken(); // Register a callback to make sure the script below triggers a // DidStartNavigation event to fire. This indicates that the popup main // frame's proxy in A's SiteInstanceGroup received and ran the OpenURL call. DidStartNavigationCallback callback( web_contents_b, base::BindLambdaForTesting([&](NavigationHandle* handle) { auto* request = NavigationRequest::From(handle); ASSERT_TRUE(request->IsPost()); const std::optional<blink::LocalFrameToken>& frame_token = request->GetInitiatorFrameToken(); EXPECT_TRUE(frame_token.has_value()); EXPECT_EQ(initiator_global_token.frame_token, frame_token.value()); EXPECT_EQ(initiator_global_token.child_id, request->GetInitiatorProcessId()); // This is the RenderFrameHost in the WebContents that was forced to // `Close()` in the interceptor, so it should be deleted. auto* initiator_rfh = RenderFrameHostImpl::FromFrameToken( request->GetInitiatorProcessId(), frame_token.value()); EXPECT_FALSE(initiator_rfh); // Even if the initiator RenderFrameHost is gone, its // PolicyContainerHost should still be around since the LocalFrame has // not been destroyed yet. PolicyContainerHost* initiator_policy_container = RenderFrameHostImpl::GetPolicyContainerHost( base::OptionalToPtr(frame_token), request->GetInitiatorProcessId(), web_contents()->GetPrimaryMainFrame()->GetStoragePartition()); ASSERT_TRUE(initiator_policy_container); EXPECT_EQ(network::mojom::ReferrerPolicy::kAlways, initiator_policy_container->referrer_policy()); EXPECT_EQ( network::mojom::ReferrerPolicy::kAlways, request->GetInitiatorPolicyContainerPolicies()->referrer_policy); loop.Quit(); })); // Intercept the OpenURL call to the proxy for the popup in A's // SiteInstanceGroup, which will happen in a separate navigation task posted // from the form submission task in the script below. // Ownership of `shell_a` is being transferred to the interceptor. The // interceptor will delete `shell_a` so it should not be used after this. SiteInstanceGroup* a_sig = rfh_a->GetSiteInstance()->group(); auto proxy_host_interceptor = std::make_unique<InitiatorClosingOpenURLInterceptor>( web_contents_b->GetPrimaryMainFrame() ->browsing_context_state() ->GetRenderFrameProxyHost(a_sig), std::move(shell_a), a_sig->process()); // Initiate a form submission into the b.com popup that will navigate the // popup to about:blank. // We want the initiator to be closed between the time the about:blank OpenURL // IPC is sent and received. This is done in the interceptor by closing the // shell the initiator belongs to. The timing means window.close() is not a // viable option: it would post a task after the OpenURL navigation task, so // we can't ensure the window is closed before OpenURL runs. ExecuteScriptAsync(rfh_a, R"( let input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", "my_token"); input.setAttribute("value", "my_value"); // Schedule a form submission navigation (which will occur in a separate // task). let form = document.createElement('form'); form.appendChild(input); form.setAttribute("method", "POST"); form.setAttribute("action", "about:blank"); form.setAttribute("target", "_bpopup"); document.body.appendChild(form); form.submit(); )"); loop.Run(); // Make sure the about:blank navigation finishes successfully. WaitForLoadStop(web_contents_b); EXPECT_EQ(GURL("about:blank"), web_contents_b->GetLastCommittedURL()); } // Check that when RenderProcessHostImpl::DisableRefCounts is called while a // NavigationStateKeepAlive exists, the navigation still succeeds. This is a // regression test for crbug.com/348150830. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, DisableRefCountsWhileKeepAliveExists) { GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); // This test needs the browser process to call DisableRefCounts after the form // submission's NavigationStateKeepAlive is created and before the task that // sends the BeginNavigation IPC. To do this, use EvalJS to return a string to // the test framework between those two renderer-side tasks, allowing the // browser process to reset the counts before the BeginNavigation IPC is // received and the NavigationStateKeepAlive is destroyed. std::string expected_str("Placeholder value"); std::string js_str = base::StringPrintf( "f = document.createElement('form');" "f.action = 'about:blank';" "document.body.appendChild(f);" "f.submit();" "'%s';", expected_str.c_str()); TestNavigationObserver observer(shell()->web_contents()); EXPECT_EQ(expected_str, EvalJs(shell(), js_str).ExtractString()); // Expect at this point that a NavigationStateKeepAlive has been created for // the form submission. NavigationStateKeepAlive* keep_alive = current_frame_host()->GetStoragePartition()->GetNavigationStateKeepAlive( current_frame_host()->GetFrameToken()); ASSERT_TRUE(keep_alive); // Disable ref counts on the process, which resets all ref counts to 0. This // seems to happen in practice in https://crbug.com/348150830 when a // BrowserContext is closed before all of its frames are properly cleaned up, // but the exact repro steps for this aren't known, so simulate this behavior // with an explicit DisableRefCounts() call. current_frame_host()->GetProcess()->DisableRefCounts(); // Wait for the navigation to complete. At that point, the // NavigationStateKeepAlive goes away, which can possibly decrement the // associated ref count. Since DisableRefCounts() was called, the ref count // should not be further decremented, and the navigation should complete // successfully. observer.Wait(); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_TRUE(current_frame_host()->GetLastCommittedURL().IsAboutBlank()); } using MediaNavigationBrowserTest = NavigationBaseBrowserTest; // Media navigations synchronously complete the time of the `CommitNavigation` // IPC call. Ensure that the renderer does not crash if the media navigation // results in an HTTP error with no body, since the renderer will reentrantly // commit an error page while handling the `CommitNavigation` IPC. IN_PROC_BROWSER_TEST_F(MediaNavigationBrowserTest, FailedNavigation) { embedded_test_server()->RegisterRequestHandler(base::BindRepeating( [](const net::test_server::HttpRequest& request) -> std::unique_ptr<net::test_server::HttpResponse> { auto response = std::make_unique<net::test_server::BasicHttpResponse>(); response->set_code(net::HTTP_NOT_FOUND); response->set_content_type("video/mp4"); return response; })); ASSERT_TRUE(embedded_test_server()->Start()); const GURL error_url(embedded_test_server()->GetURL("/moo.mp4")); EXPECT_FALSE(NavigateToURL(shell(), error_url)); EXPECT_EQ(error_url, current_frame_host()->GetLastCommittedURL()); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType()); } using DocumentPolicyBrowserTest = NavigationBaseBrowserTest; // Test that scroll restoration can be disabled with // Document-Policy: force-load-at-top IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, ScrollRestorationDisabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html")); TestNavigationManager navigation_manager(web_contents(), url); // This test expects the document is freshly loaded on the back navigation // so that the document policy to force-load-at-top will run. This will not // happen if the document is back-forward cached, so we need to disable it. DisableBackForwardCacheForTesting(web_contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING); // Load the document with document policy force-load-at-top shell()->LoadURL(url); EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: force-load-at-top\r\n" "\r\n" "<p style='position: absolute; top: 10000px;'>Some text</p>"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); { RenderFrameSubmissionObserver frame_observer(web_contents()); // Scroll down the page a bit EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)")); frame_observer.WaitForScrollOffsetAtTop(false); } // Navigate away EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Navigate back EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(RenderWidgetHostImpl::From( web_contents()->GetRenderViewHost()->GetWidget())); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } // Test that scroll restoration works as expected with // Document-Policy: force-load-at-top=?0 IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, ScrollRestorationEnabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html")); RenderFrameSubmissionObserver frame_observer(web_contents()); TestNavigationManager navigation_manager(web_contents(), url); // Load the document with document policy force-load-at-top set to false. shell()->LoadURL(url); EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: force-load-at-top=?0\r\n" "\r\n" "<p style='position: absolute; top: 10000px;'>Some text</p>"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Scroll down the page a bit EXPECT_TRUE(ExecJs(web_contents(), "window.scrollTo(0, 1000)")); frame_observer.WaitForScrollOffsetAtTop(false); // Navigate away EXPECT_TRUE(ExecJs(web_contents(), "window.location = 'about:blank'")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Navigate back EXPECT_TRUE(ExecJs(web_contents(), "history.back()")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Ensure scroll restoration activated frame_observer.WaitForScrollOffsetAtTop(false); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); EXPECT_FALSE(last_metadata.is_scroll_offset_at_top); } // Test that element fragment anchor scrolling can be disabled with // Document-Policy: force-load-at-top IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, FragmentAnchorDisabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html#text")); // Load the target document TestNavigationManager navigation_manager(web_contents(), url); shell()->LoadURL(url); // Start navigation EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // Send Document-Policy header response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: force-load-at-top\r\n" "\r\n" "<p id='text' style='position: absolute; top: 10000px;'>Some text</p>"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(RenderWidgetHostImpl::From( web_contents()->GetRenderViewHost()->GetWidget())); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } // Test that element fragment anchor scrolling works as expected with // Document-Policy: force-load-at-top=?0 IN_PROC_BROWSER_TEST_F(DocumentPolicyBrowserTest, FragmentAnchorEnabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html#text")); RenderFrameSubmissionObserver frame_observer(web_contents()); // Load the target document TestNavigationManager navigation_manager(web_contents(), url); shell()->LoadURL(url); // Start navigation EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // Send Document-Policy header response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: force-load-at-top=?0\r\n" "\r\n" "<p id='text' style='position: absolute; top: 10000px;'>Some text</p>"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_TRUE(WaitForRenderFrameReady(current_frame_host())); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(web_contents()).LastRenderFrameMetadata(); EXPECT_FALSE(last_metadata.is_scroll_offset_at_top); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitBasic) { GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); auto origin_expected = url::Origin::Create(url); TestNavigationManager manager(web_contents(), url); shell()->LoadURL(url); EXPECT_TRUE(manager.WaitForResponse()); NavigationRequest* navigation = main_frame()->navigation_request(); std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit(); ASSERT_TRUE(origin_to_commit.has_value()); ASSERT_TRUE(manager.WaitForNavigationFinished()); url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin(); EXPECT_FALSE(origin_to_commit->opaque()); EXPECT_FALSE(origin_committed.opaque()); EXPECT_EQ(origin_expected, *origin_to_commit); EXPECT_EQ(origin_expected, origin_committed); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommit204) { GURL url = embedded_test_server()->GetURL("a.com", "/nocontent"); TestNavigationManager manager(web_contents(), url); shell()->LoadURL(url); EXPECT_TRUE(manager.WaitForResponse()); NavigationRequest* navigation = main_frame()->navigation_request(); std::optional<url::Origin> origin_to_commit = navigation->GetOriginToCommit(); EXPECT_FALSE(origin_to_commit.has_value()); ASSERT_TRUE(manager.WaitForNavigationFinished()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitSandboxFromResponse) { GURL url = embedded_test_server()->GetURL( "a.com", "/set-header?Content-Security-Policy: sandbox"); TestNavigationManager manager(web_contents(), url); shell()->LoadURL(url); EXPECT_TRUE(manager.WaitForResponse()); NavigationRequest* navigation = main_frame()->navigation_request(); url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); ASSERT_TRUE(manager.WaitForNavigationFinished()); url::Origin origin_committed = current_frame_host()->GetLastCommittedOrigin(); EXPECT_TRUE(origin_to_commit.opaque()); EXPECT_TRUE(origin_committed.opaque()); // TODO(crbug.com/40092527). The nonce must match. EXPECT_NE(origin_to_commit, origin_committed); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitSandboxFromParentDocument) { GURL url_top = embedded_test_server()->GetURL( "a.com", "/set-header?Content-Security-Policy: sandbox allow-scripts"); EXPECT_TRUE(NavigateToURL(shell(), url_top)); GURL url_iframe = embedded_test_server()->GetURL("a.com", "/empty.html"); TestNavigationManager manager(web_contents(), url_iframe); ExecuteScriptAsync(current_frame_host(), R"( let iframe = document.createElement("iframe"); iframe.src = "./empty.html"; document.body.appendChild(iframe); )"); EXPECT_TRUE(manager.WaitForResponse()); FrameTreeNode* iframe = current_frame_host()->child_at(0); NavigationRequest* navigation = iframe->navigation_request(); url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); ASSERT_TRUE(manager.WaitForNavigationFinished()); url::Origin origin_committed = iframe->current_frame_host()->GetLastCommittedOrigin(); EXPECT_TRUE(origin_to_commit.opaque()); EXPECT_TRUE(origin_committed.opaque()); // TODO(crbug.com/40092527). The nonce must match. EXPECT_NE(origin_to_commit, origin_committed); // Both document have the same URL. Only the first sets CSP:sandbox, but both // are sandboxed. They get an opaque origin different from each others. EXPECT_NE(current_frame_host()->GetLastCommittedOrigin(), origin_committed); } // Regression test for https://crbug.com/1158306. // Navigate to a response, which set Content-Security-Policy: sandbox AND block // the response. The error page shouldn't set sandbox flags. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromCspSandboxResponse) { // Block every navigation in WillProcessResponse. std::unique_ptr<content::TestNavigationThrottleInserter> blocker = BlockNavigationWillProcessResponse(web_contents()); // Navigate toward a document witch sets CSP:sandbox. GURL url = embedded_test_server()->GetURL( "a.com", "/set-header?Content-Security-Policy: sandbox"); TestNavigationManager manager(web_contents(), url); shell()->LoadURL(url); ASSERT_TRUE(manager.WaitForNavigationFinished()); // An error page committed. It doesn't have any sandbox flags, despite the // original response headers. EXPECT_TRUE(current_frame_host()->IsErrorDocument()); EXPECT_EQ(network::mojom::WebSandboxFlags::kNone, current_frame_host()->active_sandbox_flags()); EXPECT_EQ(url, current_frame_host()->GetLastCommittedURL()); EXPECT_TRUE(current_frame_host()->GetLastCommittedOrigin().opaque()); EXPECT_TRUE( current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(url)); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ProcessShutdownDuringDeferredNavigationThrottle) { GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); class ShutdownThrottle : public TaskRunnerDeferringThrottle, WebContentsObserver { public: explicit ShutdownThrottle(WebContents* web_contents, NavigationHandle* handle) : TaskRunnerDeferringThrottle( base::SingleThreadTaskRunner::GetCurrentDefault(), /*defer_start=*/false, /*defer_redirect=*/false, /*defer_response=*/true, handle), web_contents_(web_contents) { WebContentsObserver::Observe(web_contents_); } void AsyncResume() override { // Shutdown the renderer and delay Resume() until then. web_contents_->GetPrimaryMainFrame()->GetProcess()->Shutdown(1); } void RenderFrameDeleted(RenderFrameHost* frame_host) override { TaskRunnerDeferringThrottle::AsyncResume(); } private: raw_ptr<WebContents> web_contents_; }; auto inserter = std::make_unique<TestNavigationThrottleInserter>( shell()->web_contents(), base::BindLambdaForTesting( [&](NavigationHandle* handle) -> std::unique_ptr<NavigationThrottle> { return std::make_unique<ShutdownThrottle>(shell()->web_contents(), handle); })); class DoesNotReadyToCommitObserver : public WebContentsObserver { public: explicit DoesNotReadyToCommitObserver(WebContents* contents) : WebContentsObserver(contents) {} // WebContentsObserver overrides. void ReadyToCommitNavigation(NavigationHandle* handle) override { // This method should not happen. Since the process is destroyed before // we become ready to commit, we can not ever reach // ReadyToCommitNavigation. Doing so would fail because the renderer is // gone. ADD_FAILURE() << "ReadyToCommitNavigation but renderer has crashed. " "IsRenderFrameLive: " << handle->GetRenderFrameHost()->IsRenderFrameLive(); navigation_was_ready_to_commit_ = true; } void DidFinishNavigation(NavigationHandle* handle) override { navigation_finished_ = true; navigation_committed_ = handle->HasCommitted(); } bool navigation_was_ready_to_commit() { return navigation_was_ready_to_commit_; } bool navigation_finished() { return navigation_finished_; } bool navigation_committed() { return navigation_committed_; } private: bool navigation_was_ready_to_commit_ = false; bool navigation_finished_ = false; bool navigation_committed_ = false; }; // Watch that ReadyToCommitNavigation() will not happen when the renderer is // gone. DoesNotReadyToCommitObserver no_commit_obs(shell()->web_contents()); // We will shutdown the renderer during this navigation. ScopedAllowRendererCrashes scoped_allow_renderer_crashes; // Important: This is a browser-initiated navigation, so the NavigationRequest // does not have an open connection (NavigationClient) to the renderer that it // is listening to for termination while running NavigationThrottles. // // Expect this navigation to be aborted, so we stop waiting after the // uncommitted navigation is done. GURL url2 = embedded_test_server()->GetURL("a.com", "/title1.html"); NavigateToURLBlockUntilNavigationsComplete( shell(), url2, /*number_of_navigations=*/1, /*ignore_uncommitted_navigations=*/false); // The renderer was shutdown mid-navigation. EXPECT_FALSE( shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive()); // The navigation was aborted, which means it finished but did not commit, and // _importantly_ it never reported "ReadyToCommitNavigation" without a live // renderer. EXPECT_TRUE(no_commit_obs.navigation_finished()); EXPECT_FALSE(no_commit_obs.navigation_was_ready_to_commit()); EXPECT_FALSE(no_commit_obs.navigation_committed()); } // Sandbox flags defined by the parent must not apply to Chrome's error page. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ErrorPageFromInSandboxedIframe) { GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); // Block every navigation in WillProcessResponse. std::unique_ptr<content::TestNavigationThrottleInserter> blocker = BlockNavigationWillProcessResponse(web_contents()); TestNavigationManager manager(web_contents(), url); ExecuteScriptAsync(current_frame_host(), R"( let iframe = document.createElement("iframe"); iframe.src = location.href; iframe.sandbox = "allow-orientation-lock"; document.body.appendChild(iframe); )"); ASSERT_TRUE(manager.WaitForNavigationFinished()); RenderFrameHostImpl* child_rfh = current_frame_host()->child_at(0)->current_frame_host(); EXPECT_TRUE(child_rfh->IsErrorDocument()); EXPECT_EQ(network::mojom::WebSandboxFlags::kNone, child_rfh->active_sandbox_flags()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, OriginToCommitSandboxFromFrame) { GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); TestNavigationManager manager(web_contents(), url); ExecuteScriptAsync(current_frame_host(), R"( let iframe = document.createElement("iframe"); iframe.src = location.href; iframe.sandbox = ""; document.body.appendChild(iframe); )"); EXPECT_TRUE(manager.WaitForResponse()); FrameTreeNode* iframe = current_frame_host()->child_at(0); NavigationRequest* navigation = iframe->navigation_request(); url::Origin origin_to_commit = navigation->GetOriginToCommit().value(); ASSERT_TRUE(manager.WaitForNavigationFinished()); url::Origin origin_committed = iframe->current_frame_host()->GetLastCommittedOrigin(); EXPECT_TRUE(origin_to_commit.opaque()); EXPECT_TRUE(origin_committed.opaque()); // TODO(crbug.com/40092527). Make the nonce to match. EXPECT_NE(origin_to_commit, origin_committed); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, NavigateToAboutBlankWhileFirstNavigationPending) { GURL url_a = embedded_test_server()->GetURL("a.com", "/empty.html"); GURL url_b = embedded_test_server()->GetURL("b.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url_a)); ShellAddedObserver new_shell_observer; ExecuteScriptAsync( current_frame_host(), JsReplace("window.open($1, '_blank').location = 'about:blank'", url_b)); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); TestNavigationManager manager_1(popup_contents, url_b); TestNavigationManager manager_2(popup_contents, GURL("about:blank")); ASSERT_TRUE(manager_1.WaitForNavigationFinished()); ASSERT_TRUE(manager_2.WaitForNavigationFinished()); EXPECT_EQ(popup_contents->GetLastCommittedURL(), "about:blank"); } class NetworkIsolationSplitCacheAppendIframeOrigin : public NavigationBaseBrowserTest { public: NetworkIsolationSplitCacheAppendIframeOrigin() { feature_list_.InitAndEnableFeature( net::features::kSplitCacheByNetworkIsolationKey); } private: base::test::ScopedFeatureList feature_list_; }; // Make a main document, have it request a cacheable subresources. Then make a // same-site document in an iframe that serves the CSP:Sandbox header. Stop the // test server, have the sandboxed document requests the same subresource. The // request should fail. To make sure the request is actually in the cache, the // main document should be able to request it again. IN_PROC_BROWSER_TEST_F(NetworkIsolationSplitCacheAppendIframeOrigin, SandboxedUsesDifferentCache) { auto server = std::make_unique<net::EmbeddedTestServer>(); server->AddDefaultHandlers(GetTestDataFilePath()); EXPECT_TRUE(server->Start()); GURL url_main_document = server->GetURL("a.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url_main_document)); EXPECT_TRUE(ExecJs(current_frame_host(), R"( new Promise(resolve => { let iframe = document.createElement("iframe"); iframe.onload = resolve; iframe.src = "/set-header?Content-Security-Policy: sandbox allow-scripts"; document.body.appendChild(iframe); }) )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); RenderFrameHostImpl* main_rfh = current_frame_host(); RenderFrameHostImpl* sub_rfh = main_rfh->child_at(0)->current_frame_host(); EXPECT_FALSE(main_rfh->GetLastCommittedOrigin().opaque()); EXPECT_TRUE(sub_rfh->GetLastCommittedOrigin().opaque()); const char* fetch_cacheable = R"( fetch("cacheable.svg") .then(() => "success") .catch(() => "error") )"; EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable)); server.reset(); EXPECT_EQ("error", EvalJs(sub_rfh, fetch_cacheable)); EXPECT_EQ("success", EvalJs(main_rfh, fetch_cacheable)); } // The Content Security Policy directive 'treat-as-public-address' is parsed // into the parsed headers by services/network and applied there. That directive // is ignored in report-only policies. Here we check that Blink reports a // console message if 'treat-as-public-address' is delivered in a report-only // policy. This serves also as a regression test for https://crbug.com/1150314 IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, TreatAsPublicAddressInReportOnly) { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "The Content Security Policy directive 'treat-as-public-address' is " "ignored when delivered in a report-only policy."); GURL url = embedded_test_server()->GetURL( "/set-header?" "Content-Security-Policy-Report-Only: treat-as-public-address"); EXPECT_TRUE(NavigateToURL(shell(), url)); ASSERT_TRUE(console_observer.Wait()); } // The Content Security Policy directive 'plugin-types' has been removed. Here // we check that Blink reports a console message if 'plugin-type' is delivered // in a policy. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, ContentSecurityPolicyErrorPluginTypes) { WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern( "The Content-Security-Policy directive 'plugin-types' has been removed " "from the specification. " "If you want to block plugins, consider specifying \"object-src 'none'\" " "instead."); GURL url = embedded_test_server()->GetURL( "/set-header?" "Content-Security-Policy: plugin-types application/pdf"); EXPECT_TRUE(NavigateToURL(shell(), url)); ASSERT_TRUE(console_observer.Wait()); } class SubresourceLoadingTest : public NavigationBrowserTest { public: SubresourceLoadingTest() = default; SubresourceLoadingTest(const SubresourceLoadingTest&) = delete; SubresourceLoadingTest& operator=(const SubresourceLoadingTest&) = delete; void DontTestNetworkServiceCrashes() { test_network_service_crashes_ = false; } void VerifyResultsOfAboutBlankNavigation(RenderFrameHost* target_frame, RenderFrameHost* initiator_frame) { // Verify that `target_frame` has been navigated to "about:blank". EXPECT_EQ(GURL(url::kAboutBlankURL), target_frame->GetLastCommittedURL()); // Verify that "about:blank" committed with the expected origin, and in the // expected SiteInstance. EXPECT_EQ(target_frame->GetLastCommittedOrigin(), initiator_frame->GetLastCommittedOrigin()); EXPECT_EQ(target_frame->GetSiteInstance(), initiator_frame->GetSiteInstance()); // Ask for cookies in the `target_frame`. One implicit verification here // is whether this step will hit any `cookie_url`-related NOTREACHED or DwoC // in RestrictedCookieManager::ValidateAccessToCookiesAt. This verification // is non-racey, because `document.cookie` must have heard back from the // RestrictedCookieManager before returning the value of cookies (this // ignores possible Blink-side caching, but this is the first time the // renderer needs the cookies and so this is okay for this test). EXPECT_EQ("", EvalJs(target_frame, "document.cookie")); // Verify that the "about:blank" frame is able to load an image. VerifyImageSubresourceLoads(target_frame); } void VerifyImageSubresourceLoads( const ToRenderFrameHost& target, const std::string& target_document = "document") { RenderFrameHostImpl* target_frame = static_cast<RenderFrameHostImpl*>(target.render_frame_host()); VerifySingleImageSubresourceLoad(target_frame, target_document); // Verify detecting and recovering from a NetworkService crash (e.g. via the // `network_service_disconnect_handler_holder_mojo` field and the // UpdateSubresourceLoaderFactories method of RenderFrameHostImpl). if (!IsInProcessNetworkService() && test_network_service_crashes_) { SimulateNetworkServiceCrash(); // In addition to waiting (inside SimulateNetworkServiceCrash above) for // getting notified about being disconnected from // network::mojom::NetworkServiceTest, we also want to make sure that the // relevant RenderFrameHost realizes that the NetworkService has crashed. // Which RenderFrameHost is relevant varies from test to test, so we // flush multiple frames and use kDoNothingIfNoNetworkServiceConnection. FlushNetworkInterfacesInOpenerChain(target_frame); // Rerun the test after the NetworkService crash. VerifySingleImageSubresourceLoad(target_frame, target_document); } } private: void FlushNetworkInterfacesInOpenerChain(RenderFrameHostImpl* current_frame) { std::set<WebContents*> visited_contents; while (true) { // Check if we've already visited the current frame tree. DCHECK(current_frame); WebContents* current_contents = WebContents::FromRenderFrameHost(current_frame); DCHECK(current_contents); if (base::Contains(visited_contents, current_contents)) break; visited_contents.insert(current_contents); // Flush all the frames in the `current_contents's active page. current_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost( [](RenderFrameHost* frame_to_flush) { constexpr bool kDoNothingIfNoNetworkServiceConnection = true; frame_to_flush->FlushNetworkAndNavigationInterfacesForTesting( kDoNothingIfNoNetworkServiceConnection); }); // Traverse the `current_frame`'s opener chain. if (FrameTreeNode* opener_node = current_frame->frame_tree_node()->opener()) { current_frame = opener_node->current_frame_host(); } else { break; // Break out of the loop if there is no opener. } } } void VerifySingleImageSubresourceLoad(RenderFrameHost* target, std::string_view target_document) { // Use a random, GUID-based hostname, to avoid hitting the network cache. GURL image_url = embedded_test_server()->GetURL( base::Uuid::GenerateRandomV4().AsLowercaseString() + ".com", "/blank.jpg"); const std::string script = base::StrCat({ R"(new Promise(resolve => { let img = document.createElement('img'); )", JsReplace("img.src = $1;", image_url), R"( img.addEventListener('load', () => { resolve('allowed'); }); img.addEventListener('error', err => { resolve(`error: ${err}`); }); )", target_document, R"(.body.appendChild(img); }); )"}); EXPECT_EQ("allowed", EvalJs(target, script)); } bool test_network_service_crashes_ = true; }; // The test below verifies that an "about:blank" navigation commits with the // right origin, even when the initiator of the navigation is not the parent or // opener of the frame targeted by the navigation. In the // GrandchildToAboutBlank... testcases, the navigation is initiated by the // grandparent of the target frame. // // In this test case there are no process swaps and the parent of the navigated // frame is a local frame (even in presence of site-per-process). See also // GrandchildToAboutBlank_ABA_CrossSite and // GrandchildToAboutBlank_ABB_CrossSite. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, GrandchildToAboutBlank_ABA_SameSite) { GURL url(embedded_test_server()->GetURL( "a.example.com", "/cross_site_iframe_factory.html?" "a.example.com(b.example.com(a.example.com))")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Verify the desired properties of the test setup. RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderFrameHostImpl* child_frame = main_frame->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild_frame = child_frame->child_at(0)->current_frame_host(); EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance()); EXPECT_EQ(main_frame->GetLastCommittedOrigin(), grandchild_frame->GetLastCommittedOrigin()); EXPECT_NE(main_frame->GetLastCommittedOrigin(), child_frame->GetLastCommittedOrigin()); // Navigate the grandchild frame to about:blank ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); TestNavigationObserver nav_observer(shell()->web_contents(), 1); ASSERT_TRUE( ExecJs(main_frame, "grandchild_window = window.open('about:blank', 'grandchild')")); nav_observer.Wait(); // Verify that the grandchild has the same origin as the main frame (*not* the // origin of the parent frame). main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); child_frame = main_frame->child_at(0)->current_frame_host(); grandchild_frame = child_frame->child_at(0)->current_frame_host(); VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); } // The test below verifies that an "about:blank" navigation commits with the // right origin, even when the initiator of the navigation is not the parent or // opener of the frame targeted by the navigation. In the // GrandchildToAboutBlank... testcases, the navigation is initiated by the // grandparent of the target frame. // // In this test case there are no process swaps and the parent of the navigated // frame is a remote frame (in presence of site-per-process). See also // GrandchildToAboutBlank_ABA_SameSite and GrandchildToAboutBlank_ABB_CrossSite. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, GrandchildToAboutBlank_ABA_CrossSite) { GURL url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b(a))")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Verify the desired properties of the test setup. RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderFrameHostImpl* child_frame = main_frame->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild_frame = child_frame->child_at(0)->current_frame_host(); if (AreAllSitesIsolatedForTesting()) { EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } else { EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } EXPECT_EQ(main_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance()); EXPECT_EQ(main_frame->GetLastCommittedOrigin(), grandchild_frame->GetLastCommittedOrigin()); EXPECT_NE(main_frame->GetLastCommittedOrigin(), child_frame->GetLastCommittedOrigin()); // Navigate the grandchild frame to about:blank ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); TestNavigationObserver nav_observer(shell()->web_contents(), 1); ASSERT_TRUE( ExecJs(main_frame, "grandchild_window = window.open('about:blank', 'grandchild')")); nav_observer.Wait(); // Verify that the grandchild has the same origin as the main frame (*not* the // origin of the parent frame). main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); child_frame = main_frame->child_at(0)->current_frame_host(); grandchild_frame = child_frame->child_at(0)->current_frame_host(); VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); } // The test below verifies that an "about:blank" navigation commits with the // right origin, even when the initiator of the navigation is not the parent or // opener of the frame targeted by the navigation. In the // GrandchildToAboutBlank... testcases, the navigation is initiated by the // grandparent of the target frame. // // In this test case the navigation forces a process swap of the target frame. // See also GrandchildToAboutBlank_ABA_SameSite and // GrandchildToAboutBlank_ABA_CrossSite. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, GrandchildToAboutBlank_ABB_CrossSite) { GURL url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b(b))")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Verify the desired properties of the test setup. RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderFrameHostImpl* child_frame = main_frame->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild_frame = child_frame->child_at(0)->current_frame_host(); if (AreAllSitesIsolatedForTesting()) { EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } else { EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } EXPECT_EQ(child_frame->GetSiteInstance(), grandchild_frame->GetSiteInstance()); EXPECT_EQ(child_frame->GetLastCommittedOrigin(), grandchild_frame->GetLastCommittedOrigin()); EXPECT_NE(main_frame->GetLastCommittedOrigin(), grandchild_frame->GetLastCommittedOrigin()); // Navigate the grandchild frame to about:blank ASSERT_TRUE(ExecJs(grandchild_frame, "window.name = 'grandchild'")); TestNavigationObserver nav_observer(shell()->web_contents(), 1); ASSERT_TRUE( ExecJs(main_frame, "grandchild_window = window.open('about:blank', 'grandchild')")); nav_observer.Wait(); // Verify that the grandchild has the same origin as the main frame (*not* the // origin of the parent frame). main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); child_frame = main_frame->child_at(0)->current_frame_host(); grandchild_frame = child_frame->child_at(0)->current_frame_host(); VerifyResultsOfAboutBlankNavigation(grandchild_frame, main_frame); } // The test below verifies that an "about:blank" navigation commits with the // right origin, even when the initiator of the navigation is not the parent or // opener of the frame targeted by the navigation. In the // TopToAboutBlank_CrossSite testcase, the top-level navigation is initiated by // a cross-site subframe. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, TopToAboutBlank_CrossSite) { GURL url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Verify the desired properties of the test setup. RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderFrameHostImpl* child_frame = main_frame->child_at(0)->current_frame_host(); if (AreAllSitesIsolatedForTesting()) { EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } else { EXPECT_EQ(main_frame->GetSiteInstance(), child_frame->GetSiteInstance()); } url::Origin a_origin = url::Origin::Create(embedded_test_server()->GetURL("a.com", "/")); url::Origin b_origin = url::Origin::Create(embedded_test_server()->GetURL("b.com", "/")); EXPECT_EQ(a_origin, main_frame->GetLastCommittedOrigin()); EXPECT_EQ(b_origin, child_frame->GetLastCommittedOrigin()); // Have the subframe initiate navigation of the main frame to about:blank. // // (Note that this scenario is a bit artificial/silly, because the final // about:blank frame won't have any same-origin friends that could populate // it. OTOH, it is still important to maintain all the invariants in this // scenario. And it is still possible that a same-origin frame (e.g. in // another window in the same BrowsingInstance) exists and can populate the // about:blank frame. TestNavigationObserver nav_observer(shell()->web_contents(), 1); ASSERT_TRUE(ExecJs(child_frame, "window.top.location = 'about:blank'")); nav_observer.Wait(); // Verify that the main frame is the only remaining frame and that it has the // same origin as the navigation initiator. main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); EXPECT_EQ(0u, main_frame->child_count()); EXPECT_EQ(b_origin, main_frame->GetLastCommittedOrigin()); EXPECT_EQ(GURL(url::kAboutBlankURL), main_frame->GetLastCommittedURL()); } // The test below verifies that an "about:blank" navigation commits with the // right origin, even when the initiator of the navigation is not the parent or // opener of the frame targeted by the navigation. In the // SameSiteSiblingToAboutBlank_CrossSiteTop testcase, the navigation is // initiated by a same-origin sibling (notably, not by one of target frame's // ancestors) and both siblings are subframes of a cross-site main frame. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, SameSiteSiblingToAboutBlank_CrossSiteTop) { GURL url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b,b)")); EXPECT_TRUE(NavigateToURL(shell(), url)); // Name the 2nd child. RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderFrameHostImpl* child_frame1 = main_frame->child_at(0)->current_frame_host(); RenderFrameHostImpl* child_frame2 = main_frame->child_at(1)->current_frame_host(); ASSERT_TRUE(ExecJs(child_frame2, "window.name = 'child2'")); // Grab `child2` window from the 1st child... ASSERT_TRUE(ExecJs(child_frame1, "child2 = window.open('', 'child2')")); // ...but make sure that child2's opener doesn't point to child1. ASSERT_TRUE(ExecJs(main_frame, "child2 = window.open('', 'child2')")); EXPECT_EQ(true, EvalJs(child_frame2, "window.opener == window.top")); // From child1 initiate navigation of child2 to about:blank. TestNavigationObserver nav_observer(shell()->web_contents(), 1); ASSERT_TRUE(ExecJs(child_frame1, "child2.location = 'about:blank'")); nav_observer.Wait(); // Verify that child2 has the origin of the initiator of the navigation. main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); child_frame1 = main_frame->child_at(0)->current_frame_host(); child_frame2 = main_frame->child_at(1)->current_frame_host(); VerifyResultsOfAboutBlankNavigation(child_frame2, child_frame1); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. Note that some aspects of the current behavior (e.g. the // synchronous re-navigation) are not spec-compliant - see // https://crbug.com/778318 and https://github.com/whatwg/html/issues/3267. // Note that the same behavior is expected in the ...NewFrameWithoutSrc and // in the ...NewFrameWithAboutBlank testcases. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc) { GURL opener_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), opener_url)); // This inserts an `iframe` element without an `src` attribute. According to // some specs "the browsing context will remain at the initial about:blank // page", although other specs suggest that there is an explicit, separate // navigation. See: // https://html.spec.whatwg.org/dev/iframe-embed-object.html#the-iframe-element // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#shared-attribute-processing-steps-for-iframe-and-frame-elements ASSERT_TRUE(ExecJs(shell(), R"( let ifr = document.createElement('iframe'); document.body.appendChild(ifr); )")); WaitForLoadStop(shell()->web_contents()); RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); VerifyResultsOfAboutBlankNavigation(subframe, main_frame); } // See the doc comment for the // URLLoaderFactoryInInitialEmptyDoc_NewFrameWithoutSrc test case. IN_PROC_BROWSER_TEST_F( SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_NewFrameWithAboutBlank) { GURL opener_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), opener_url)); ASSERT_TRUE(ExecJs(shell(), R"( ifr = document.createElement('iframe'); ifr.src = 'about:blank'; document.body.appendChild(ifr); )")); WaitForLoadStop(shell()->web_contents()); RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); VerifyResultsOfAboutBlankNavigation(subframe, main_frame); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginFlagOfSameOriginAboutBlankNavigation) { GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); GURL iframe_url(embedded_test_server()->GetURL("a.com", "/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), parent_url)); EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( let iframe = document.createElement('iframe'); iframe.src = $1; document.body.appendChild(iframe); )", iframe_url))); WaitForLoadStop(shell()->web_contents()); base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_TRUE(handle->IsSameOrigin()); loop.Quit(); })); // Changing the src to trigger DidFinishNavigationCallback EXPECT_TRUE(ExecJs(current_frame_host(), R"( document.querySelector("iframe").src = 'about:blank'; )")); loop.Run(); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginFlagOfCrossOriginAboutBlankNavigation) { GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), parent_url)); EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( let iframe = document.createElement('iframe'); iframe.src = $1; document.body.appendChild(iframe); )", iframe_url))); WaitForLoadStop(shell()->web_contents()); base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_FALSE(handle->IsSameOrigin()); loop.Quit(); })); // Changing the src to trigger DidFinishNavigationCallback EXPECT_TRUE(ExecJs(current_frame_host(), R"( document.querySelector("iframe").src = 'about:blank'; )")); loop.Run(); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginFlagOfSrcdocNavigation) { GURL url = embedded_test_server()->GetURL("a.com", "/empty.html"); GURL cross_origin = embedded_test_server()->GetURL("b.com", "/empty.html"); EXPECT_TRUE(NavigateToURL(shell(), url)); // Navigating to about:srcdoc from the initial empty document is always a // same-origin navigation: // - about:srcdoc is same-origin with the parent. // - the initial empty document is same-origin with the parent. { base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_TRUE(handle->IsSameOrigin()); loop.Quit(); })); EXPECT_TRUE(ExecJs(current_frame_host(), R"( let iframe = document.createElement('iframe'); iframe.srcdoc = "dummy content"; document.body.appendChild(iframe); )")); loop.Run(); } // Now, navigate cross-origin, and back to about:srcdoc with a brand new // iframe. The navigation is now considered cross-origin. // - the previous document is cross-origin with the parent. // - about:srcdoc is same-origin with the parent. { EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( let iframe2 = document.createElement('iframe'); iframe2.src = $1; iframe2.id = 'iframe2'; document.body.appendChild(iframe2); )", cross_origin))); WaitForLoadStop(shell()->web_contents()); base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_FALSE(handle->IsSameOrigin()); loop.Quit(); })); EXPECT_TRUE(ExecJs(current_frame_host(), R"( document.getElementById("iframe2").srcdoc = "dummy content"; )")); loop.Run(); } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginFlagOfAboutBlankToAboutBlankNavigation) { GURL parent_url(embedded_test_server()->GetURL("a.com", "/empty.html")); GURL iframe_url(embedded_test_server()->GetURL("b.com", "/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), parent_url)); EXPECT_TRUE(ExecJs(main_frame(), JsReplace(R"( let iframe = document.createElement('iframe'); iframe.src = $1; document.body.appendChild(iframe); )", iframe_url))); WaitForLoadStop(shell()->web_contents()); // Test a same-origin about:blank navigation { base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_TRUE(handle->IsSameOrigin()); loop.Quit(); })); RenderFrameHostImpl* child_document = current_frame_host()->child_at(0)->current_frame_host(); EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)")); loop.Run(); } // Test another same-origin about:blank navigation { base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_TRUE(handle->IsSameOrigin()); loop.Quit(); })); RenderFrameHostImpl* child_document = current_frame_host()->child_at(0)->current_frame_host(); EXPECT_TRUE(ExecJs(child_document, R"(location.href = "about:blank";)")); loop.Run(); } // Test a cross-origin about:blank navigation { base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); EXPECT_FALSE(handle->IsSameOrigin()); loop.Quit(); })); EXPECT_TRUE(ExecJs(current_frame_host(), R"( document.querySelector('iframe').src = "about:blank"; )")); loop.Run(); } } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, SameOriginOfSandboxedIframe) { EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); base::RunLoop loop; DidFinishNavigationCallback callback( shell()->web_contents(), base::BindLambdaForTesting([&](NavigationHandle* handle) { ASSERT_TRUE(handle->HasCommitted()); // TODO(crbug.com/40092527) Take sandbox into account. Same Origin // should be true EXPECT_FALSE(handle->IsSameOrigin()); loop.Quit(); })); EXPECT_TRUE(ExecJs(current_frame_host(), R"( let iframe = document.createElement('iframe'); iframe.sandbox = "allow-scripts"; iframe.src = "/empty.html"; document.body.appendChild(iframe); )")); loop.Run(); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl) { GURL opener_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), opener_url)); WebContentsImpl* popup = nullptr; { WebContentsAddedObserver popup_observer; ASSERT_TRUE(ExecJs(shell(), "window.open('', '_blank')")); popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents()); } WaitForLoadStop(popup); // Verify that we are at the initial empty document. EXPECT_EQ(1, popup->GetController().GetEntryCount()); EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry()); EXPECT_TRUE( popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document()); // Verify that the `popup` is at "about:blank", with expected origin, with // working `document.cookie`, and with working subresource loads. VerifyResultsOfAboutBlankNavigation( popup->GetPrimaryMainFrame(), shell()->web_contents()->GetPrimaryMainFrame()); } // See the doc comment for the // URLLoaderFactoryInInitialEmptyDoc_NewPopupToEmptyUrl test case. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_NewPopupToAboutBlank) { GURL opener_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), opener_url)); WebContentsImpl* popup = nullptr; { WebContentsAddedObserver popup_observer; ASSERT_TRUE(ExecJs(shell(), "window.open('about:blank', '_blank')")); popup = static_cast<WebContentsImpl*>(popup_observer.GetWebContents()); } WaitForLoadStop(popup); // Verify that we are at the synchronously committed about:blank document. EXPECT_EQ(1, popup->GetController().GetEntryCount()); EXPECT_TRUE(popup->GetController().GetLastCommittedEntry()->IsInitialEntry()); EXPECT_TRUE( popup->GetPrimaryFrameTree().root()->is_on_initial_empty_document()); // Verify other about:blank things. VerifyResultsOfAboutBlankNavigation( popup->GetPrimaryMainFrame(), shell()->web_contents()->GetPrimaryMainFrame()); } // The test below verifies that error pages have a functional URLLoaderFactory. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInErrorPage) { GURL error_url(embedded_test_server()->GetURL("/close-socket")); EXPECT_FALSE(NavigateToURL(shell(), error_url)); VerifyImageSubresourceLoads(shell()->web_contents()->GetPrimaryMainFrame()); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. IN_PROC_BROWSER_TEST_F( SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_HungNavigationInSubframe) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Add a subframe that will never commit a navigation (i.e. that will be stuck // on the initial empty document). const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); ASSERT_TRUE( ExecJs(shell(), JsReplace(R"(ifr = document.createElement('iframe'); ifr.src = $1; document.body.appendChild(ifr); )", hung_url))); // No process swaps are expected before ReadyToCommit (which will never happen // for a navigation to "/hung"). This test assertion double-checks that the // test will cover inheriting URLLoaderFactory from the creator/opener/parent // frame. RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); EXPECT_EQ(main_frame->GetProcess()->GetDeprecatedID(), subframe->GetProcess()->GetDeprecatedID()); // Ask the parent to script the same-origin subframe and trigger some HTTP // subresource loads within the subframe. // // This tests the functionality of the URLLoaderFactory that gets used by the // initial empty document. In this test, the `request_initiator` will be a // non-opaque origin - it requires that the URLLoaderFactory will have a // matching `request_initiator_origin_lock` (e.g. inherited from the parent). VerifyImageSubresourceLoads(shell(), "ifr.contentDocument"); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. IN_PROC_BROWSER_TEST_F( SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopup) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Open a popup window that will never commit a navigation (i.e. that will be // stuck on the initial empty document). const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); WebContents* popup = nullptr; { WebContentsAddedObserver popup_observer; ASSERT_TRUE( ExecJs(shell(), JsReplace("popup = window.open($1)", hung_url))); popup = popup_observer.GetWebContents(); } // No process swaps are expected before ReadyToCommit (which will never happen // for a navigation to "/hung"). This test assertion double-checks that the // test will cover inheriting URLLoaderFactory from the creator/opener/parent // frame. RenderFrameHost* opener_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(), popup_frame->GetProcess()->GetDeprecatedID()); // Ask the opener to script the (same-origin) popup window and trigger some // HTTP subresource loads within the popup. // // This tests the functionality of the URLLoaderFactory that gets used by the // initial empty document. In this test, the `request_initiator` will be a // non-opaque origin - it requires that the URLLoaderFactory will have a // matching `request_initiator_origin_lock` (e.g. inherited from the opener). VerifyImageSubresourceLoads(shell(), "popup.document"); // TODO(crbug.com/40758605): Crash recovery doesn't work when there is // no opener. DontTestNetworkServiceCrashes(); // Test again after closing the opener.. shell()->Close(); VerifyImageSubresourceLoads(popup); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. The ...WithClearedOpener testcase is a regression test for // https://crbug.com/1191203. IN_PROC_BROWSER_TEST_F( SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_HungNavigationInPopupWithClearedOpener) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Open a new window that will never commit a navigation (i.e. that will be // stuck on the initial empty document). Clearing of `popup.opener` tests if // inheriting of URLLoaderFactory from the opener will work when the opener // has been cleared in DOM/Javascript. const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); const char kScriptTemplate[] = R"( popup = window.open($1); popup.opener = null; )"; content::WebContents* popup = nullptr; { WebContentsAddedObserver popup_observer; ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, hung_url))); popup = popup_observer.GetWebContents(); } // No process swaps are expected before ReadyToCommit (which will never happen // for a navigation to "/hung"). This test assertion double-checks that the // test will cover inheriting URLLoaderFactory from the creator/opener/parent // frame. This differentiates the test from the "noopener" case covered in // another testcase. RenderFrameHost* opener_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); EXPECT_EQ(opener_frame->GetProcess()->GetDeprecatedID(), popup_frame->GetProcess()->GetDeprecatedID()); // Double-check that the popup didn't commit any navigation and that it has // an the same origin as the initial opener. EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL()); EXPECT_NE("null", EvalJs(popup, "window.origin")); EXPECT_EQ(shell() ->web_contents() ->GetPrimaryMainFrame() ->GetLastCommittedOrigin() .Serialize(), EvalJs(popup, "window.origin")); // Use the parent frame's `popup` reference to script the same-origin popup // window and trigger some HTTP subresource loads within the popup. // // This tests the functionality of the URLLoaderFactory that gets used by the // initial empty document. In this test, the `request_initiator` will be a // non-opaque origin - it requires that the URLLoaderFactory will have a // matching `request_initiator_origin_lock` (e.g. inherited from the opener). VerifyImageSubresourceLoads(popup); // TODO(crbug.com/40758605): Crash recovery doesn't work when there is // no opener. DontTestNetworkServiceCrashes(); // Test again after closing the opener.. shell()->Close(); VerifyImageSubresourceLoads(popup); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. IN_PROC_BROWSER_TEST_F(SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_204NoOpenerPopup) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Open a new window by following a no-opener link to /nocontent (204). const GURL no_content_url = embedded_test_server()->GetURL("a.com", "/nocontent"); const char kScriptTemplate[] = R"( let anchor = document.createElement('a'); anchor.href = $1; anchor.rel = 'noopener'; anchor.target = '_blank'; anchor.innerText = 'test link'; document.body.appendChild(anchor); anchor.click(); )"; content::WebContents* popup = nullptr; { WebContentsAddedObserver popup_observer; ASSERT_TRUE(ExecJs(shell(), JsReplace(kScriptTemplate, no_content_url))); popup = popup_observer.GetWebContents(); } WaitForLoadStop(popup); // Double-check that the `popup` didn't commit any navigation and that it has // an opaque origin. EXPECT_EQ(GURL(), popup->GetPrimaryMainFrame()->GetLastCommittedURL()); EXPECT_EQ("null", EvalJs(popup, "window.origin")); // Process swap is expected because of 'noopener'. This test assertion // double-checks that in the test it is not possible to inheriting // URLLoaderFactory from the creator/opener/parent frame (because the popup is // in another process). RenderFrameHost* opener_frame = shell()->web_contents()->GetPrimaryMainFrame(); RenderFrameHost* popup_frame = popup->GetPrimaryMainFrame(); EXPECT_NE(opener_frame->GetProcess()->GetDeprecatedID(), popup_frame->GetProcess()->GetDeprecatedID()); // Inject Javascript that triggers some subresource loads over HTTP. // // To some extent, this simulates an ability of 1) Android WebView (see // https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript // into an initial empty document (even when no web/renderer content has // access to the document). // // This tests the functionality of the URLLoaderFactory that gets used by the // initial empty document. In this test, the `request_initiator` will be an // opaque, unique origin (since nothing has committed yet) and will be // compatible with `request_initiator_origin_lock` of the URLLoaderFactory. VerifyImageSubresourceLoads(popup); } // The test below verifies that an initial empty document has a functional // URLLoaderFactory. IN_PROC_BROWSER_TEST_F( SubresourceLoadingTest, URLLoaderFactoryInInitialEmptyDoc_HungNavigationInNewWindow) { // Open a new shell, starting at the "/hung" URL. const GURL hung_url = embedded_test_server()->GetURL("a.com", "/hung"); Shell* new_shell = Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(), hung_url, nullptr, gfx::Size()); // Wait until the renderer process launches (this will flush the CreateView // IPC and make sure that ExecJs and EvalJs are able to work). RenderFrameHost* main_frame = new_shell->web_contents()->GetPrimaryMainFrame(); { RenderProcessHostWatcher process_watcher( main_frame->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY); process_watcher.Wait(); } // Double-check that the new shell didn't commit any navigation and that it // has an opaque origin. EXPECT_EQ(1, new_shell->web_contents()->GetController().GetEntryCount()); EXPECT_TRUE(new_shell->web_contents() ->GetController() .GetLastCommittedEntry() ->IsInitialEntry()); EXPECT_EQ(GURL(), main_frame->GetLastCommittedURL()); EXPECT_EQ("null", EvalJs(main_frame, "window.origin")); // Inject Javascript that triggers some subresource loads over HTTP. // // To some extent, this simulates an ability of 1) Android WebView (see // https://crbug.com/1189838) and 2) Chrome Extensions, to inject Javascript // into an initial empty document (even when no web/renderer content has // access to the document). // // This tests the functionality of the URLLoaderFactory that gets used by the // initial empty document. In this test, the `request_initiator` will be an // opaque, unique origin (since nothing has committed yet) and will be // compatible with `request_initiator_origin_lock` of the URLLoaderFactory. VerifyImageSubresourceLoads(main_frame); } namespace { struct Result { GURL url; std::optional<url::Origin> origin; bool committed; }; class NavigationLogger : public WebContentsObserver { public: explicit NavigationLogger(WebContents* contents) : WebContentsObserver(contents) {} // WebContentsObserver overrides: void DidFinishNavigation(NavigationHandle* handle) override { if (handle->HasCommitted()) { EXPECT_EQ(handle->GetRenderFrameHost()->GetLastCommittedURL(), handle->GetURL()); RenderFrameHost* rfh = handle->GetRenderFrameHost(); results_.push_back({.url = rfh->GetLastCommittedURL(), .origin = rfh->GetLastCommittedOrigin(), .committed = true}); } else { results_.push_back({.url = handle->GetURL(), .committed = false}); } } const std::vector<Result>& results() const { return results_; } private: std::vector<Result> results_; }; } // namespace class UndoCommitNavigationBrowserTest : public NavigationBrowserTest { public: UndoCommitNavigationBrowserTest() { std::map<std::string, std::string> parameters = { {"queueing_level", "none"}, }; // Note that RenderDocument needs to be disabled so that it won't enable // navigation queueing automatically. feature_list_.InitWithFeaturesAndParameters( /*enabled_features=*/{{features::kQueueNavigationsWhileWaitingForCommit, parameters}}, /*disabled_features=*/{features::kRenderDocument}); } void SetUpOnMainThread() override { // These navigation tests require full site isolation since they test races // with committing a navigation in a speculative RenderFrameHost.. if (!AreAllSitesIsolatedForTesting()) { GTEST_SKIP() << "Site isolation is not enabled!"; } NavigationBrowserTest::SetUpOnMainThread(); } void SetUpCommandLine(base::CommandLine* command_line) override { NavigationBrowserTest::SetUpCommandLine(command_line); // PerformanceManager maintains its own parallel frame tree and has // sometimes been confused by things like `UndoCommitNavigation()`. // Force-enable it for test coverage; otherwise, by default, // PerformanceManager uses the dummy implementation. // // TODO(crbug.com/40187286): Enable this by default in content_shell. command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, "PerformanceManagerInstrumentation"); } private: base::test::ScopedFeatureList feature_list_; }; // A helper that invokes `functor` on the next `DidStartNavigation()`. template <typename F> void OnNextDidStartNavigation(WebContents* web_contents, F&& functor) { class Observer : public WebContentsObserver { public: using Callback = base::OnceCallback<void(NavigationHandle*)>; explicit Observer(WebContents* web_contents, Callback callback) : WebContentsObserver(web_contents), callback_(std::move(callback)) {} // WebContentsObserver overrides: void DidStartNavigation(NavigationHandle* handle) override { std::move(callback_).Run(handle); delete this; } private: Callback callback_; }; new Observer(web_contents, base::BindLambdaForTesting(std::forward<F>(functor))); } IN_PROC_BROWSER_TEST_F(UndoCommitNavigationBrowserTest, PerformanceManagerFrameTreeConsistency) { // PerformanceManager reports when a remote frame is attached to a local // parent, and it was previously getting confused by the fact that a // `blink::RemoteFrame` with matching RemoteFrameTokens was being reported as // attached twice: once by the initial page loaded in the next statement, and // the next when the browser needs to send a `UndoCommitNavigation()` to the // a.com renderer. ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* first_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(0); RenderProcessHost* const a_com_render_process_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->current_frame_host() ->GetProcess(); NavigationLogger logger(web_contents); // Start a navigation that will create a speculative RFH in the existing // render process for a.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("a.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(web_contents, infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process. RenderFrameHostImpl* speculative_render_frame_host = first_subframe_node->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(a_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and ignore) the next `DidCommitProvisionalLoad()` for a.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); // Update the id attribute to exercise a PerformanceManager-specific code // path: when the renderer swaps in a `blink::RemoteFrame` to undo the // `CommitNavigation()`, it will report the iframe attribution data again. // PerformanceManager should not complain that V8ContextTracker already has // the iframe attribution data, nor should it update the iframe attribution // data, to preserve existing behavior (unfortunately, the latter part is not // really tested in this browser test). EXPECT_TRUE(ExecJs(web_contents, "document.querySelector('iframe').id = 'new-name';")); // Now begin a new navigation to c.com while the previous a.com navigation // above is paused in the pending commit state. const GURL final_url = embedded_test_server()->GetURL("c.com", "/title1.html"); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, first_subframe_node->render_manager() ->current_frame_host() ->GetLastCommittedURL()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); // This test always uses UndoCommitNavigation, so navigation corresponding to // the paused commit should never commit. EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); EXPECT_EQ(infinitely_loading_url, results[0].url); EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); EXPECT_EQ(final_url, results[1].url); } class ResumeCommitClosureSetWaiter { public: explicit ResumeCommitClosureSetWaiter(NavigationHandle* handle) { // `Helper` "observes" the set via its own destruction, since the navigation // code setting a resume commit closure will replace the testing one // installed here. NavigationRequest::From(handle)->set_resume_commit_closure( base::DoNothingWithBoundArgs(std::make_unique<Helper>(loop_))); } void Wait() { loop_.Run(); } private: class Helper { public: explicit Helper(base::RunLoop& loop) : loop_(loop) {} ~Helper() { loop_->Quit(); } private: raw_ref<base::RunLoop> loop_; }; base::RunLoop loop_; }; class NavigationQueueingBrowserTest : public NavigationBrowserTest { public: NavigationQueueingBrowserTest() { feature_list_.InitAndEnableFeatureWithParameters( features::kQueueNavigationsWhileWaitingForCommit, {{"queueing_level", "full"}}); } void SetUpOnMainThread() override { // These navigation tests require full site isolation since they test races // with committing a navigation in a speculative RenderFrameHost. if (!AreAllSitesIsolatedForTesting()) { GTEST_SKIP(); } NavigationBrowserTest::SetUpOnMainThread(); } const base::HistogramTester& histogram_tester() const { return histogram_tester_; } private: base::test::ScopedFeatureList feature_list_; base::HistogramTester histogram_tester_; }; IN_PROC_BROWSER_TEST_F(NavigationQueueingBrowserTest, Regular) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); NavigationLogger logger(shell()->web_contents()); // Start a navigation that will create a speculative RFH. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); rfh_observer.Wait(); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); RenderFrameHostImpl* speculative_render_frame_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); // Pause the next `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. { const GURL next_url = embedded_test_server()->GetURL("c.com", "/title1.html"); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), next_url)); resume_commit_closure_set_waiter->Wait(); } // Cancel the c.com navigation that was previously queued and (nearly) ready // for commit, to make sure one pending commit navigation that causes multiple // subsequent navigations to queue is correctly counted in the metrics. const GURL final_url = embedded_test_server()->GetURL("d.com", "/title1.html"); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), final_url)); resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); // Both the b.com commit and the d.com commit should record the PendingCommit // time. histogram_tester().ExpectTotalCount( "Navigation.PendingCommit.Duration.Regular", 2); // The d.com navigation should not have blocked any navigation requests from // making progress. histogram_tester().ExpectBucketCount( "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular", false, 1); // But the b.com navigation blocked c.com and d.com. histogram_tester().ExpectBucketCount( "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.Regular", true, 1); if (base::FeatureList::IsEnabled(features::kDeferSpeculativeRFHCreation)) { // For 2 blocked navigations, 2 total blocks are expected when trying to // pick a final RenderFrameHost to commit the navigation. The attempt to // create a RenderFrameHost when starting the navigation will be skipped. histogram_tester().ExpectBucketCount( "Navigation.PendingCommit.BlockedCount.Regular", 2, 1); } else { // For 2 blocked navigations, 4 total blocks are expected: 2 when trying to // assign a RenderFrameHost when starting a navigation, and 2 when trying to // pick a final RenderFrameHost to commit the navigation. histogram_tester().ExpectBucketCount( "Navigation.PendingCommit.BlockedCount.Regular", 4, 1); } histogram_tester().ExpectBucketCount( "Navigation.PendingCommit.BlockedCommitCount.Regular", 2, 1); } class CommitNavigationRaceBrowserTest : public NavigationBrowserTest, public ::testing::WithParamInterface<bool> { public: CommitNavigationRaceBrowserTest() { std::map<std::string, std::string> parameters = { {"queueing_level", GetParam() ? "full" : "none"}, }; feature_list_.InitAndEnableFeatureWithParameters( features::kQueueNavigationsWhileWaitingForCommit, parameters); } void SetUpOnMainThread() override { // These navigation tests require full site isolation since they test races // with committing a navigation in a speculative RenderFrameHost.. if (!AreAllSitesIsolatedForTesting()) { GTEST_SKIP(); } NavigationBrowserTest::SetUpOnMainThread(); } static std::string DescribeParams( const testing::TestParamInfo<ParamType>& info) { return info.param ? "NavigationQueueing" : "UndoCommitNavigation"; } private: base::test::ScopedFeatureList feature_list_; }; // Test for https://crbug.com/40187807 and https://crbug.com/332746903. // // Ensure that racing a navigation commit in a speculative/provisional child // frame in render process B with a detach IPC from render process A (i.e. the // child frame's parent is in render process A and has removed the frame owner // element—e.g. <iframe>—from the DOM) does not result in the detach IPC being // discarded and never received by render process B. IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, DetachAfterCommitNavigationInSubFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b,a)"))); WebContentsImpl* const web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* const first_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(0); FrameTreeNode* const second_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(1); RenderProcessHost* const b_com_render_process_host = first_subframe_node->render_manager()->current_frame_host()->GetProcess(); // Start a navigation in the second child frame that will create a speculative // RFH in the existing render process for b.com. The first child frame is // already hosted in the render process for b.com: this is to ensure the // render process remains live even after the second child frame is detached // later in this test. GURL b_url = embedded_test_server()->GetURL("b.com", "/title1.html"); SpeculativeRenderFrameHostObserver observer(shell()->web_contents(), b_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(second_subframe_node, b_url)); // Ensure the speculative RFH is in the expected process. observer.Wait(); RenderFrameHostImpl* speculative_render_frame_host = second_subframe_node->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and ignore) the next `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); // At this point, the b.com renderer has already committed the RenderFrame, // but on the browser side, the RenderFrameHost is still speculative. // Intentionally do not wait for script completion here. This runs an event // loop that pumps incoming messages, but IPCs from b.com would be processed // out of order, since the `DidCommitProvisionalLoad()` attempt was previously // paused above. ExecuteScriptAsync( web_contents, JsReplace("document.querySelectorAll('iframe')[1].remove()")); // However, since it's not possible to wait for `remove()` to take effect, // the test must cheat a little and directly call the Mojo IPC that the JS // above would eventually trigger. second_subframe_node->render_manager()->current_frame_host()->Detach(); EXPECT_TRUE(WaitForLoadStop(web_contents)); // Validate that render process for b.com has handled the detach message for // the provisional frame that was committing. Before the fix: // - without navigation queueing, the render process for b.com still had the // proxy for the second child frame, because the browser process's request // to delete it was sent via a broken message pipe. Thus, the frame tree in // the render process for b.com incorrectly thought there were still two // child frames. // - with navigation queueing, the render process for b.com has already // committed the navigation in the second child frame, so the renderer-side // proxy has already been destroyed and replaced. However, the browser // process has not heard about the commit yet and sends a detach to the // proxy, which the renderer ignores since it no longer exists. EXPECT_EQ(1, EvalJs(first_subframe_node, "top.length")); } IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, BeginNewNavigationDuringCommitNavigationInMainFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Prior to implementing the UndoCommitNavigation() workaround, the race // condition being tested would result in a crash in the b.com renderer. Open // another b.com window in the same browsing instance to verify that the b.com // renderer does not unexpectedly crash even if the b.com speculative // RenderFrameHost is discarded. ASSERT_TRUE(ExecJs( shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( "b.com", "/title1.html")))); ASSERT_EQ(2u, Shell::windows().size()); WebContentsImpl* new_web_contents = static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); WaitForLoadStop(new_web_contents); RenderProcessHost* const b_com_render_process_host = new_web_contents->GetPrimaryMainFrame()->GetProcess(); NavigationLogger logger(shell()->web_contents()); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process (i.e. the b.com // process that was created for the navigation in the new window earlier). WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); RenderFrameHostImpl* speculative_render_frame_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. const GURL final_url = embedded_test_server()->GetURL("c.com", "/title1.html"); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); EXPECT_EQ(infinitely_loading_url, results[0].url); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); EXPECT_EQ(final_url, results[1].url); } IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, BeginNewNavigationDuringCommitNavigationInSubFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "b.com", "/cross_site_iframe_factory.html?b(a)"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* first_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(0); RenderProcessHost* const b_com_render_process_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->current_frame_host() ->GetProcess(); NavigationLogger logger(web_contents); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process. RenderFrameHostImpl* speculative_render_frame_host = first_subframe_node->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. const GURL final_url = embedded_test_server()->GetURL("c.com", "/title1.html"); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, first_subframe_node->render_manager() ->current_frame_host() ->GetLastCommittedURL()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_EQ(infinitely_loading_url, results[0].url); EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("c.com"), results[1].origin); EXPECT_EQ(final_url, results[1].url); } // Test the behavior of a navigation that gets suspended in the pending commit // state followed by another navigation that results in a failed navigation. A // failed navigation is not a navigation that results in an HTTP error page; it // is a situation where the network request itself fails, e.g. DNS resolution // failed, and Chrome commits an error page instead. IN_PROC_BROWSER_TEST_P( CommitNavigationRaceBrowserTest, BeginNewNavigationDuringCommitFailedNavigationInMainFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Prior to implementing the UndoCommitNavigation() workaround, the race // condition being tested would result in a crash in the b.com renderer. Open // another b.com window in the same browsing instance to verify that the b.com // renderer does not unexpectedly crash even if the b.com speculative // RenderFrameHost is discarded. ASSERT_TRUE(ExecJs( shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( "b.com", "/title1.html")))); ASSERT_EQ(2u, Shell::windows().size()); WebContentsImpl* new_web_contents = static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); WaitForLoadStop(new_web_contents); RenderProcessHost* const b_com_render_process_host = new_web_contents->GetPrimaryMainFrame()->GetProcess(); NavigationLogger logger(shell()->web_contents()); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process (i.e. the b.com // process that was created for the navigation in the new window earlier). WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); RenderFrameHostImpl* speculative_render_frame_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. This navigation will fail and // commit an error page. const GURL final_url = embedded_test_server()->GetURL("c.com", "/title1.html"); std::unique_ptr<URLLoaderInterceptor> url_interceptor = URLLoaderInterceptor::SetupRequestFailForURL(final_url, net::ERR_DNS_TIMED_OUT); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } // The top-level page completes loading but is an error page, so // `WaitForLoadStop()` should return false, since the navigation entry will // have `PAGE_TYPE_ERROR`. EXPECT_FALSE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); EXPECT_TRUE(web_contents->GetPrimaryMainFrame()->IsErrorDocument()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); EXPECT_EQ(infinitely_loading_url, results[0].url); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_TRUE(results[1].committed); EXPECT_TRUE(results[1].origin->opaque()); EXPECT_EQ(embedded_test_server() ->GetOrigin("c.com") .GetTupleOrPrecursorTupleIfOpaque(), results[1].origin->GetTupleOrPrecursorTupleIfOpaque()); EXPECT_EQ(final_url, results[1].url); } IN_PROC_BROWSER_TEST_P( CommitNavigationRaceBrowserTest, BeginNewNavigationDuringCommitFailedNavigationInSubFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "b.com", "/cross_site_iframe_factory.html?b(a)"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* first_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(0); RenderProcessHost* const b_com_render_process_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->current_frame_host() ->GetProcess(); NavigationLogger logger(web_contents); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(web_contents, infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process. RenderFrameHostImpl* speculative_render_frame_host = first_subframe_node->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. This navigation will fail and // commit an error page. const GURL final_url = embedded_test_server()->GetURL("c.com", "/title1.html"); std::unique_ptr<URLLoaderInterceptor> url_interceptor = URLLoaderInterceptor::SetupRequestFailForURL(final_url, net::ERR_DNS_TIMED_OUT); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } // The top-level page completes loading. Unlike the main frame variant of this // test, `WaitForLoadStop()` should return true, since the navigation entry // will have `PAGE_TYPE_NORMAL` as only a subframe failed to load. EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, first_subframe_node->render_manager() ->current_frame_host() ->GetLastCommittedURL()); EXPECT_TRUE(first_subframe_node->render_manager() ->current_frame_host() ->IsErrorDocument()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_EQ(infinitely_loading_url, results[0].url); EXPECT_TRUE(results[1].committed); EXPECT_TRUE(results[1].origin->opaque()); EXPECT_EQ(embedded_test_server() ->GetOrigin("c.com") .GetTupleOrPrecursorTupleIfOpaque(), results[1].origin->GetTupleOrPrecursorTupleIfOpaque()); EXPECT_EQ(final_url, results[1].url); } // about:blank navigations do not require a URL loader and go through a // different path to commit the navigation in the renderer. IN_PROC_BROWSER_TEST_P( CommitNavigationRaceBrowserTest, BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInMainFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Prior to implementing the UndoCommitNavigation() workaround, the race // condition being tested would result in a crash in the b.com renderer. Open // another b.com window in the same browsing instance to verify that the b.com // renderer does not unexpectedly crash even if the b.com speculative // RenderFrameHost is discarded. ASSERT_TRUE(ExecJs( shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( "b.com", "/title1.html")))); ASSERT_EQ(2u, Shell::windows().size()); WebContentsImpl* new_web_contents = static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); EXPECT_TRUE(WaitForLoadStop(new_web_contents)); RenderProcessHost* const b_com_render_process_host = new_web_contents->GetPrimaryMainFrame()->GetProcess(); NavigationLogger logger(shell()->web_contents()); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process (i.e. the b.com // process that was created for the navigation in the new window earlier). WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); RenderFrameHostImpl* speculative_render_frame_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Note that this navigation is initiated by the a.com renderer, as the a.com // renderer is still the current frame host for the main frame. const GURL final_url("about:blank"); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); EXPECT_EQ(infinitely_loading_url, results[0].url); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); EXPECT_EQ(final_url, results[1].url); } IN_PROC_BROWSER_TEST_P( CommitNavigationRaceBrowserTest, BeginNewNavigationWithNoUrlLoaderDuringCommitNavigationInSubFrame) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "b.com", "/cross_site_iframe_factory.html?b(a)"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* first_subframe_node = web_contents->GetPrimaryMainFrame()->child_at(0); RenderProcessHost* const b_com_render_process_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->current_frame_host() ->GetProcess(); NavigationLogger logger(web_contents); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL infinitely_loading_url = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), infinitely_loading_url); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, infinitely_loading_url)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process. RenderFrameHostImpl* speculative_render_frame_host = first_subframe_node->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); std::optional<ResumeCommitClosureSetWaiter> resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the next navigation, since the resume commit closure may be synchronously // set while handling the `BeginNavigation()` IPC in the browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { resume_commit_closure_set_waiter.emplace(handle); }); } // Note that this navigation is initiated by the a.com renderer, as the a.com // renderer is still the current frame host for the main frame. const GURL final_url("about:blank"); ASSERT_TRUE(BeginNavigateToURLFromRenderer(first_subframe_node, final_url)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } EXPECT_TRUE(WaitForLoadStop(web_contents)); EXPECT_EQ(final_url, first_subframe_node->render_manager() ->current_frame_host() ->GetLastCommittedURL()); auto results = logger.results(); ASSERT_EQ(2u, results.size()); // If navigation queueing is enabled, the first navigation will complete the // commit as the new navigation gets queued until the first navigation's // commit finished. If navigation queueing is disabled, the pending commit // navigation will be cancelled. if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); } EXPECT_EQ(infinitely_loading_url, results[0].url); EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); EXPECT_EQ(final_url, results[1].url); } // Tests when a navigation is pending commit, two new navigations start one // after another in the same frame. IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, BeginTwoNavigationsDuringCommitNavigation) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); // Prior to implementing the UndoCommitNavigation() workaround, the race // condition being tested would result in a crash in the b.com renderer. Open // another b.com window in the same browsing instance to verify that the b.com // renderer does not unexpectedly crash even if the b.com speculative // RenderFrameHost is discarded. ASSERT_TRUE(ExecJs( shell(), JsReplace("window.open($1)", embedded_test_server()->GetURL( "b.com", "/title1.html")))); ASSERT_EQ(2u, Shell::windows().size()); WebContentsImpl* new_web_contents = static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); WaitForLoadStop(new_web_contents); RenderProcessHost* const b_com_render_process_host = new_web_contents->GetPrimaryMainFrame()->GetProcess(); NavigationLogger logger(shell()->web_contents()); // Start a navigation that will create a speculative RFH in the existing // render process for b.com. const GURL url_b = embedded_test_server()->GetURL("b.com", "/infinitely_loading_image.html"); SpeculativeRenderFrameHostObserver rfh_observer(shell()->web_contents(), url_b); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b)); rfh_observer.Wait(); // Ensure the speculative RFH is in the expected process (i.e. the b.com // process that was created for the navigation in the new window earlier). WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); RenderFrameHostImpl* speculative_render_frame_host = root->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); commit_pauser.WaitForCommitAndPause(); // Now begin a new navigation to c.com while the previous b.com navigation // above is paused in the pending commit state. std::optional<ResumeCommitClosureSetWaiter> url_c_resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the `url_c` navigation, since the resume commit closure may be // synchronously set while handling the `BeginNavigation()` IPC in the // browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { url_c_resume_commit_closure_set_waiter.emplace(handle); }); } const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); TestNavigationManager url_c_nav(web_contents, url_c); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c)); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { ASSERT_TRUE(url_c_nav.WaitForRequestStart()); } else { url_c_nav.WaitForSpeculativeRenderFrameHostCreation(); } EXPECT_EQ(url_c, root->navigation_request()->GetURL()); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // The navigation to c.com should be queued. url_c_nav.ResumeNavigation(); url_c_resume_commit_closure_set_waiter->Wait(); } // Now begin another navigation to d.com, which will cancel the navigation to // c.com. std::optional<ResumeCommitClosureSetWaiter> url_d_resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // Install a commit closure watched for the `url_d` navigation too. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { url_d_resume_commit_closure_set_waiter.emplace(handle); }); } const GURL url_d = embedded_test_server()->GetURL("d.com", "/title1.html"); TestNavigationManager url_d_nav(web_contents, url_d); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_d)); ASSERT_TRUE(url_d_nav.WaitForRequestStart()); EXPECT_EQ(url_d, root->navigation_request()->GetURL()); // The navigation to c.com didn't commit as it was replaced by the d.com // navigation. EXPECT_TRUE(url_c_nav.WaitForNavigationFinished()); EXPECT_FALSE(url_c_nav.was_committed()); // Continue the d.com navigation. url_d_nav.ResumeNavigation(); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // Wait for the `url_d` navigation to be queued, and finish the pending // commit b.com navigation. url_d_resume_commit_closure_set_waiter->Wait(); commit_pauser.ResumePausedCommit(); } // After all the navigations finished, we will end up in d.com. EXPECT_TRUE(url_d_nav.WaitForNavigationFinished()); EXPECT_EQ(url_d, web_contents->GetLastCommittedURL()); // Check the order of navigations finishing. auto results = logger.results(); ASSERT_EQ(3u, results.size()); EXPECT_FALSE(results[0].committed); EXPECT_EQ(std::nullopt, results[0].origin); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // When navigation queueing is enabled, the pending commit navigation to // b.com won't get canceled when the c.com navigation starts. Then when the // d.com navigation starts, the c.com navigation will get canceled and // finishes first without commmitting (while the b.com navigation stays as // it is pending commit). EXPECT_EQ(url_c, results[0].url); EXPECT_TRUE(results[1].committed); // After continuing b.com's commit, it finishes and commits succesfully. EXPECT_EQ(url_b, results[1].url); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[1].origin); } else { // When navigation queueing is disabled, the pending commit navigation to // b.com gets canceled when the c.com navigation starts. Then when the // d.com navigation starts, the c.com navigation will get canceled too. EXPECT_EQ(url_b, results[0].url); EXPECT_FALSE(results[1].committed); EXPECT_EQ(url_c, results[1].url); EXPECT_EQ(std::nullopt, results[1].origin); } // Finally, the d.com navigation finishes and commits last. EXPECT_TRUE(results[2].committed); EXPECT_EQ(url_d, results[2].url); EXPECT_EQ(embedded_test_server()->GetOrigin("d.com"), results[2].origin); } // Verify that a speculative RFH in the pending commit state is still cleaned up // if the renderer crashes. IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, CrashedInPendingCommit) { GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html"); GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), url_a)); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); SpeculativeRenderFrameHostObserver rfh_observer(web_contents, url_b); ASSERT_TRUE(BeginNavigateToURLFromRenderer(shell(), url_b)); rfh_observer.Wait(); base::WeakPtr<RenderFrameHostImpl> speculative_render_frame_host = web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host() ->GetWeakPtr(); ASSERT_TRUE(speculative_render_frame_host); // Wait for the next `DidCommitProvisionalLoad()` and ignore it. CommitNavigationPauser commit_pauser(speculative_render_frame_host.get()); commit_pauser.WaitForCommitAndPause(); ASSERT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit, speculative_render_frame_host->lifecycle_state()); // Terminate the renderer process while `speculative_render_frame_host` is in // `kPendingCommit`. RenderProcessHostWatcher watcher( speculative_render_frame_host->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); speculative_render_frame_host->GetProcess()->ShutdownForBadMessage( RenderProcessHost::CrashReportMode::NO_CRASH_DUMP); watcher.Wait(); // The speculative RFH should be gone: ASSERT_FALSE(speculative_render_frame_host); EXPECT_FALSE(web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host()); // And a new navigation should not hit any DCHECKs in // `GetFrameHostForNavigation()`. EXPECT_TRUE(NavigateToURLFromRenderer( shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); } // Tests when a back navigation is pending commit, then another back navigation // starts. IN_PROC_BROWSER_TEST_P(CommitNavigationRaceBrowserTest, MultipleBackNavigation) { // This test expects the document is freshly loaded on the back navigation. DisableBackForwardCacheForTesting(web_contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING); // Navigate to a.com, then b.com. const GURL url_a = embedded_test_server()->GetURL("a.com", "/title1.html"); const GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), url_a)); ASSERT_TRUE(NavigateToURL(shell(), url_b)); // Prior to implementing the UndoCommitNavigation() workaround, the race // condition being tested would result in a crash in the b.com renderer. Open // another b.com window in the same browsing instance to verify that the b.com // renderer does not unexpectedly crash even if the b.com speculative // RenderFrameHost is discarded. ASSERT_TRUE(ExecJs(shell(), JsReplace("window.open($1)", url_b))); ASSERT_EQ(2u, Shell::windows().size()); WebContentsImpl* new_web_contents = static_cast<WebContentsImpl*>(Shell::windows()[1]->web_contents()); EXPECT_TRUE(WaitForLoadStop(new_web_contents)); RenderProcessHost* const b_com_render_process_host = new_web_contents->GetPrimaryMainFrame()->GetProcess(); // Navigate to c.com. const GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), url_c)); NavigationLogger logger(shell()->web_contents()); // Start a back navigation that will create a speculative RFH in the existing // render process for b.com. WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); TestNavigationManager first_back_nav(web_contents, url_b); ASSERT_TRUE(ExecJs(shell(), "history.back()")); ASSERT_TRUE(first_back_nav.WaitForResponse()); EXPECT_EQ(url_b, root->navigation_request()->GetURL()); // Ensure the speculative RFH is in the expected process (i.e. the b.com // process that was created for the navigation in the new window earlier). RenderFrameHostImpl* speculative_render_frame_host = root->render_manager()->speculative_frame_host(); ASSERT_TRUE(speculative_render_frame_host); EXPECT_EQ(b_com_render_process_host, speculative_render_frame_host->GetProcess()); // Pause (and potentially ignore, if navigation queueing is disabled) the next // `DidCommitProvisionalLoad()` for b.com. CommitNavigationPauser commit_pauser(speculative_render_frame_host); first_back_nav.ResumeNavigation(); commit_pauser.WaitForCommitAndPause(); // Now begin a new back navigation while the previous back navigation above is // paused in the pending commit state. std::optional<ResumeCommitClosureSetWaiter> second_back_nav_resume_commit_closure_set_waiter; if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // If navigation queueing is enabled, the test should verify that a resume // commit closure is actually set. Install a watcher now, before beginning // the second back navigation, since the resume commit closure may be // synchronously set while handling the `BeginNavigation()` IPC in the // browser. OnNextDidStartNavigation(web_contents, [&](NavigationHandle* handle) { second_back_nav_resume_commit_closure_set_waiter.emplace(handle); }); } TestNavigationManager second_back_nav(web_contents, url_a); NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>( shell()->web_contents()->GetController()); controller.GoBack(); ASSERT_TRUE(second_back_nav.WaitForRequestStart()); EXPECT_EQ(url_a, root->navigation_request()->GetURL()); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // The second back navigation should be queued. second_back_nav.ResumeNavigation(); second_back_nav_resume_commit_closure_set_waiter->Wait(); // Continue the first navigation's commit. commit_pauser.ResumePausedCommit(); } // After all the navigations finished, we will end up in a.com. EXPECT_TRUE(second_back_nav.WaitForNavigationFinished()); EXPECT_EQ(url_a, web_contents->GetLastCommittedURL()); // Check the order of navigations finishing. auto results = logger.results(); ASSERT_EQ(2u, results.size()); if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) { // When navigation queueing is enabled, the pending commit back navigation // to b.com won't get canceled when the second back navigation starts. After // continuing the second back navigation, it finishes and commits // successfully to a.com. EXPECT_EQ(url_b, results[0].url); EXPECT_TRUE(results[0].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("b.com"), results[0].origin); } else { // When navigation queueing is disabled, the pending commit back navigation // to b.com gets canceled when the second back navigation starts. Then the // second back navigation will successfully commit to a.com. EXPECT_EQ(url_b, results[0].url); EXPECT_FALSE(results[0].committed); } EXPECT_EQ(url_a, results[1].url); EXPECT_TRUE(results[1].committed); EXPECT_EQ(embedded_test_server()->GetOrigin("a.com"), results[1].origin); } INSTANTIATE_TEST_SUITE_P(, CommitNavigationRaceBrowserTest, ::testing::Bool(), &CommitNavigationRaceBrowserTest::DescribeParams); // Validate browser-side state when a pending commit RFH sends a bad // CommitNavigation() IPC. Immediately after the bad message is reported, the // speculative RFH should remain in the kPendingCommit state, but with no // pending commit for a cross-document navigation. This somewhat odd state comes // about because processing the commit navigation ack consumes the // NavigationRequest early on, before the bad message is reported. Reporting the // bad message cancels any further processing of the commit navigation ack, but // does not directly clear any other navigation-related state. // // Instead, the pending commit speculative RFH will be asynchronously torn down // later, when the browser process observes the renderer process going away, // which then implicitly ends the navigation. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, CommitBadNavigationInPendingCommitRFHCleanup) { if (!AreAllSitesIsolatedForTesting()) { GTEST_SKIP(); } // Populate the main window with something so a subsequent navigation will // create a speculative RFH. EXPECT_TRUE(NavigateToURL( web_contents(), embedded_test_server()->GetURL("b.com", "/title1.html"))); class CommitBadOriginInterceptor : public DidCommitNavigationInterceptor { public: using DidCommitNavigationInterceptor::DidCommitNavigationInterceptor; WebContentsImpl* web_contents() { return static_cast<WebContentsImpl*>( DidCommitNavigationInterceptor::web_contents()); } bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr* params, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { // Mismatch from the expected origin for the renderer process, which // should trigger a bad message kill. (*params)->origin = url::Origin::Create(GURL("https://example.com/")); frame_watcher_.emplace(render_frame_host); process_watcher_.emplace( render_frame_host->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); base::SequencedTaskRunner::GetCurrentDefault()->PostTask( FROM_HERE, base::BindLambdaForTesting([this]() { // This task should run after the browser has reported a bad // message, but before the browser process has observed render // process termination, so the pending commit speculative RFH should // still be present... auto* speculative_rfh = web_contents() ->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); ASSERT_TRUE(speculative_rfh); EXPECT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit, speculative_rfh->lifecycle_state()); // But it should not have any pending cross-document navigation // commits, since the NavigationRequest is consumed from // `RenderFrameHostImpl::navigation_requests_` as one of the first // parts of handling the commit navigation ack from the renderer. EXPECT_FALSE( speculative_rfh->HasPendingCommitForCrossDocumentNavigation()); validated_speculative_rfh_state_ = true; })); return true; } void CheckPendingCommitRenderFrameHostIsGone() const { // Make sure the validations in the posted callback actually ran. EXPECT_TRUE(validated_speculative_rfh_state_); EXPECT_TRUE(frame_watcher_->IsDestroyed()); } void WaitForRenderProcessExit() { process_watcher_->Wait(); } private: bool validated_speculative_rfh_state_ = false; std::optional<RenderFrameHostWrapper> frame_watcher_; std::optional<RenderProcessHostWatcher> process_watcher_; }; CommitBadOriginInterceptor interceptor(web_contents()); content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; // The infinitely loading page is load-bearing here: `NavigateToURL()` waits // for `DidStopLoading()`, which can be triggered by: // 1. The renderer completing the load and sending `DidStopLoading()` (this is // typical). // 2. The renderer process going away (e.g. crashing) and the browser manually // triggering `DidStopLoading()` (this is unusual). // // However, this test is specifically testing case #2. To avoid the potential // of #1 and #2 racing (and causing the test to flakily fail if #1 wins the // race), set up the test so that the renderer will never call // `DidStopLoading()`. EXPECT_FALSE(NavigateToURL(web_contents(), embedded_test_server()->GetURL( "a.com", "/infinitely_loading_image.html"))); // NavigateToURL() should fail; at this point, make sure the speculative RFH // was in the expected state after the browser reported a bad message. // // Furthermore, after the navigation completes, the speculative RFH should // also be destroyed (implicitly, by the renderer process being terminated for // a bad message). interceptor.CheckPendingCommitRenderFrameHostIsGone(); // This should actually be signalled inside `NavigateToURL()`, since it waits // for the navigation to finish (whether successful or not) before returning. // Make sure the render process host actually exited: if it didn't, then the // test will timeout here. interceptor.WaitForRenderProcessExit(); } // The following test checks what happens if a WebContentsDelegate navigates // away in response to the NavigationStateChanged event. Previously // (https://crbug.com/1210234), this was triggering a crash when creating the // new NavigationRequest, because it was trying to access the current // RenderFrameHost's PolicyContainerHost, which had not been set up yet by // RenderFrameHostImpl::DidNavigate. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, Bug1210234) { class NavigationWebContentsDelegate : public WebContentsDelegate { public: NavigationWebContentsDelegate(const GURL& url_to_intercept, const GURL& url_to_navigate_to) : url_to_intercept_(url_to_intercept), url_to_navigate_to_(url_to_navigate_to) {} void NavigationStateChanged(WebContents* source, InvalidateTypes changed_flags) override { if (!navigated_ && source->GetLastCommittedURL() == url_to_intercept_) { navigated_ = true; source->GetController().LoadURL(url_to_navigate_to_, Referrer(), ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()); } } private: bool navigated_ = false; GURL url_to_intercept_; GURL url_to_navigate_to_; }; GURL warmup_url = embedded_test_server()->GetURL("a.com", "/title1.html"); GURL initial_url = embedded_test_server()->GetURL("b.com", "/title1.html"); GURL redirection_url = embedded_test_server()->GetURL("c.com", "/title1.html"); NavigationWebContentsDelegate delegate(initial_url, redirection_url); web_contents()->SetDelegate(&delegate); ASSERT_TRUE(NavigateToURL(shell(), warmup_url)); // Note that since we committed a navigation, the next cross-origin navigation // will create a speculative RenderFrameHost (when site isolation is enabled). // Start the navigation to `initial_url` and wait until the web contents // navigates to `redirection_url`. We cannot use helper functions like // `NavigateToURLBlockUntilNavigationsComplete` because they wait for // DidStopLoading and check the LastCommittedURL when they receive it. // However, without SiteIsolation, an earlier DidStopLoading might be received // when the WebContents has not yet committed the `redirection_url`. // Prepare for the navigation. WaitForLoadStop(web_contents()); TestNavigationObserver navigation_observer(redirection_url); navigation_observer.WatchExistingWebContents(); shell()->LoadURL(initial_url); navigation_observer.Wait(); EXPECT_TRUE(IsLastCommittedEntryOfPageType(web_contents(), PAGE_TYPE_NORMAL)); EXPECT_EQ(redirection_url, web_contents()->GetLastCommittedURL()); } class NavigationBrowserTestCredentiallessIframe : public NavigationBrowserTest { public: void SetUpCommandLine(base::CommandLine* command_line) override { NavigationBrowserTest::SetUpCommandLine(command_line); command_line->AppendSwitch(switches::kEnableBlinkTestFeatures); } }; IN_PROC_BROWSER_TEST_F(NavigationBrowserTestCredentiallessIframe, CredentiallessAttributeIsHonoredByNavigation) { GURL main_url = embedded_test_server()->GetURL("/page_with_iframe.html"); GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html"); GURL iframe_url_2 = embedded_test_server()->GetURL("/title2.html"); EXPECT_TRUE(NavigateToURL(shell(), main_url)); // The main page has a child iframe with url `iframe_url_1`. EXPECT_EQ(1U, main_frame()->child_count()); FrameTreeNode* child = main_frame()->child_at(0); EXPECT_EQ(iframe_url_1, child->current_url()); EXPECT_FALSE(child->Credentialless()); EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(false, EvalJs(child->current_frame_host(), "window.credentialless")); // Changes to the iframe 'credentialless' attribute are propagated to the // FrameTreeNode. The RenderFrameHost, however, is updated only on navigation. EXPECT_TRUE( ExecJs(main_frame(), "document.getElementById('test_iframe').credentialless = true;")); EXPECT_TRUE(child->Credentialless()); EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(false, EvalJs(child->current_frame_host(), "window.credentialless")); // Create a grandchild iframe. EXPECT_TRUE(ExecJs( child, JsReplace("let grandchild = document.createElement('iframe');" "grandchild.src = $1;" "document.body.appendChild(grandchild);", iframe_url_2))); WaitForLoadStop(web_contents()); EXPECT_EQ(1U, child->child_count()); FrameTreeNode* grandchild = child->child_at(0); // The grandchild FrameTreeNode does not set the 'credentialless' // attribute. The grandchild RenderFrameHost is not credentialless, since its // parent RenderFrameHost is not credentialless. EXPECT_FALSE(grandchild->Credentialless()); EXPECT_FALSE(grandchild->current_frame_host()->IsCredentialless()); EXPECT_EQ(false, EvalJs(grandchild->current_frame_host(), "window.credentialless")); // Navigate the child iframe same-document. This does not change anything. EXPECT_TRUE(ExecJs(main_frame(), JsReplace("document.getElementById('test_iframe')" " .contentWindow.location.href = $1;", iframe_url_1.Resolve("#here").spec()))); WaitForLoadStop(web_contents()); EXPECT_TRUE(child->Credentialless()); EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(false, EvalJs(child->current_frame_host(), "window.credentialless")); // Now navigate the child iframe cross-document. EXPECT_TRUE(ExecJs( main_frame(), JsReplace("document.getElementById('test_iframe').src = $1", iframe_url_2))); WaitForLoadStop(web_contents()); EXPECT_TRUE(child->Credentialless()); EXPECT_TRUE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless")); // A credentialless document has a storage key with a nonce. EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); base::UnguessableToken credentialless_nonce = current_frame_host()->GetPage().credentialless_iframes_nonce(); EXPECT_EQ(credentialless_nonce, child->current_frame_host()->GetStorageKey().nonce().value()); // Create a grandchild iframe. EXPECT_TRUE(ExecJs( child, JsReplace("let grandchild = document.createElement('iframe');" "grandchild.id = 'grandchild_iframe';" "document.body.appendChild(grandchild);", iframe_url_1))); EXPECT_EQ(1U, child->child_count()); grandchild = child->child_at(0); // The grandchild does not set the 'credentialless' attribute, but the // grandchild document is credentialless. EXPECT_FALSE(grandchild->Credentialless()); EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(grandchild->current_frame_host(), "window.credentialless")); // The storage key's nonce is the same for all credentialless documents in the // same page. EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); EXPECT_EQ(credentialless_nonce, child->current_frame_host()->GetStorageKey().nonce().value()); // Now navigate the grandchild iframe. EXPECT_TRUE(ExecJs( child, JsReplace("document.getElementById('grandchild_iframe').src = $1", iframe_url_2))); WaitForLoadStop(web_contents()); EXPECT_TRUE(grandchild->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(grandchild->current_frame_host(), "window.credentialless")); // The storage key's nonce is still the same. EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); EXPECT_EQ(credentialless_nonce, child->current_frame_host()->GetStorageKey().nonce().value()); // Remove the 'credentialless' attribute from the iframe. This propagates to // the FrameTreeNode. The RenderFrameHost, however, is updated only on // navigation. EXPECT_TRUE( ExecJs(main_frame(), "document.getElementById('test_iframe').credentialless = false;")); EXPECT_FALSE(child->Credentialless()); EXPECT_TRUE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(child->current_frame_host(), "window.credentialless")); EXPECT_TRUE(child->current_frame_host()->GetStorageKey().nonce().has_value()); EXPECT_EQ(credentialless_nonce, child->current_frame_host()->GetStorageKey().nonce().value()); // Create another grandchild iframe. Even if the parent iframe element does // not have the 'credentialless' attribute anymore, the grandchild document is // still loaded inside of a credentialless RenderFrameHost, so it will be // credentialless. EXPECT_TRUE(ExecJs( child, JsReplace("let grandchild2 = document.createElement('iframe');" "document.body.appendChild(grandchild2);", iframe_url_1))); EXPECT_EQ(2U, child->child_count()); FrameTreeNode* grandchild2 = child->child_at(1); EXPECT_FALSE(grandchild2->Credentialless()); EXPECT_TRUE(grandchild2->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(grandchild2->current_frame_host(), "window.credentialless")); EXPECT_TRUE( grandchild2->current_frame_host()->GetStorageKey().nonce().has_value()); EXPECT_EQ(credentialless_nonce, grandchild2->current_frame_host()->GetStorageKey().nonce().value()); // Navigate the child iframe. Since the iframe element does not set the // 'credentialless' attribute, the resulting RenderFrameHost will not be // credentialless. EXPECT_TRUE( ExecJs(main_frame(), JsReplace("document.getElementById('test_iframe').src = $1;", iframe_url_2))); WaitForLoadStop(web_contents()); EXPECT_FALSE(child->Credentialless()); EXPECT_FALSE(child->current_frame_host()->IsCredentialless()); EXPECT_EQ(false, EvalJs(child->current_frame_host(), "window.credentialless")); EXPECT_FALSE( child->current_frame_host()->GetStorageKey().nonce().has_value()); // Now navigate the whole page away. GURL main_url_b = embedded_test_server()->GetURL( "b.com", "/page_with_credentialless_iframe.html"); GURL iframe_url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); EXPECT_TRUE(NavigateToURL(shell(), main_url_b)); // The main page has a credentialless child iframe with url `iframe_url_b`. EXPECT_EQ(1U, main_frame()->child_count()); FrameTreeNode* child_b = main_frame()->child_at(0); EXPECT_EQ(iframe_url_b, child_b->current_url()); EXPECT_TRUE(child_b->Credentialless()); EXPECT_TRUE(child_b->current_frame_host()->IsCredentialless()); EXPECT_EQ(true, EvalJs(child_b->current_frame_host(), "window.credentialless")); EXPECT_TRUE( child_b->current_frame_host()->GetStorageKey().nonce().has_value()); base::UnguessableToken credentialless_nonce_b = current_frame_host()->GetPage().credentialless_iframes_nonce(); EXPECT_NE(credentialless_nonce, credentialless_nonce_b); EXPECT_EQ(credentialless_nonce_b, child_b->current_frame_host()->GetStorageKey().nonce().value()); } // Ensures that OpenURLParams::FromNavigationHandle translates navigation params // correctly when used to initiate a navigation in another WebContents. IN_PROC_BROWSER_TEST_F( NavigationBrowserTest, FromNavigationHandleTranslatesNavigationParamsCorrectly) { // Test that the params are translated correctly for a redirected navigation. const GURL kRedirectedURL( embedded_test_server()->GetURL("/server-redirect?/simple_page.html")); NavigationController::LoadURLParams load_params(kRedirectedURL); TestNavigationManager first_tab_manager(web_contents(), kRedirectedURL); web_contents()->GetController().LoadURLWithParams(load_params); // Wait for response to allow the navigation to resolve the redirect. EXPECT_TRUE(first_tab_manager.WaitForResponse()); // Create LoadURLParams from the navigation after redirection. NavigationController::LoadURLParams load_url_params( OpenURLParams::FromNavigationHandle( first_tab_manager.GetNavigationHandle())); Shell* second_tab = CreateBrowser(); TestNavigationManager second_tab_manager(second_tab->web_contents(), load_url_params.url); second_tab->web_contents()->GetController().LoadURLWithParams( load_url_params); EXPECT_TRUE(second_tab_manager.WaitForResponse()); // Ensure params from the navigation in the first tab are translated to the // navigation in the second tab as expected. auto* first_tab_handle = first_tab_manager.GetNavigationHandle(); auto* second_tab_handle = second_tab_manager.GetNavigationHandle(); EXPECT_EQ(embedded_test_server()->GetURL("/simple_page.html"), second_tab_handle->GetURL()); EXPECT_EQ(first_tab_handle->GetReferrer(), second_tab_handle->GetReferrer()); EXPECT_TRUE( ui::PageTransitionCoreTypeIs(first_tab_handle->GetPageTransition(), second_tab_handle->GetPageTransition())); EXPECT_EQ(first_tab_handle->IsRendererInitiated(), second_tab_handle->IsRendererInitiated()); EXPECT_EQ(first_tab_handle->GetInitiatorOrigin(), second_tab_handle->GetInitiatorOrigin()); EXPECT_EQ(first_tab_handle->GetSourceSiteInstance(), second_tab_handle->GetSourceSiteInstance()); EXPECT_EQ(first_tab_handle->HasUserGesture(), second_tab_handle->HasUserGesture()); EXPECT_EQ(first_tab_handle->WasStartedFromContextMenu(), second_tab_handle->WasStartedFromContextMenu()); EXPECT_EQ(first_tab_handle->GetHrefTranslate(), second_tab_handle->GetHrefTranslate()); EXPECT_EQ(first_tab_handle->GetReloadType(), second_tab_handle->GetReloadType()); EXPECT_EQ(first_tab_handle->GetRedirectChain(), second_tab_handle->GetRedirectChain()); } // Regression test for https://crbug.com/1392653. Ensure that loading a URL // that doesn't go through the network stack but does assign a site for its // SiteInstance in an unassigned SiteInstance does not fail. An example of // such a URL is about:srcdoc. This ensures that the SiteInstance's site is set // even on the WillCommitWithoutUrlLoader() path in NavigationRequest. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutSrcdocInjectedOnAboutBlankPage) { // Start on an about:blank page, which should stay in an unassigned // SiteInstance. EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); SiteInstanceImpl* site_instance = current_frame_host()->GetSiteInstance(); EXPECT_FALSE(site_instance->HasSite()); // The process should be considered unused at this point. EXPECT_TRUE(site_instance->GetProcess()->IsUnused()); // Inject a srcdoc iframe into the blank document. This shouldn't really be // possible on the open web, since an about:blank page with an unassigned // SiteInstance shouldn't be scriptable by other pages, but it could still // happen in automation scenarios or through DevTools. TestNavigationObserver navigation_observer(web_contents()); EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"( let frame = document.createElement('iframe'); frame.srcdoc = 'test'; document.body.appendChild(frame); )"))); navigation_observer.Wait(); EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); EXPECT_EQ("about:srcdoc", navigation_observer.last_navigation_url()); // The srcdoc child should stay in its about:blank parent SiteInstance. EXPECT_EQ(1U, main_frame()->child_count()); FrameTreeNode* child = main_frame()->child_at(0); EXPECT_EQ(child->current_frame_host()->GetSiteInstance(), site_instance); // Committing an about:srcdoc navigation currently forces the SiteInstance's // site to be set. Prior to fixing https://crbug.com/1392653, this happened // after the actual commit was processed at DidNavigate() time, which is a // path that is no longer supported, and hence this triggered a NOTREACHED(). // Now, the site should be set before we send the CommitNavigation IPC. EXPECT_TRUE(site_instance->HasSite()); if (AreAllSitesIsolatedForTesting()) { // When we get into this situation with strict site isolation, the site URL // currently used is "about:". This may be changed in the future (e.g., to // an opaque ID). EXPECT_EQ("about:", site_instance->GetSiteInfo().site_url()); } else { EXPECT_TRUE(site_instance->IsDefaultSiteInstance()); EXPECT_EQ(SiteInstanceImpl::GetDefaultSiteURL(), site_instance->GetSiteInfo().site_url()); } // Ensure that the process was marked as used as part of setting the site. EXPECT_FALSE(site_instance->GetProcess()->IsUnused()); } class NavigationBrowserTestWarnSandboxIneffective : public NavigationBrowserTest { public: static constexpr char kSandboxEscapeWarningMessage[] = "An iframe which has both allow-scripts and allow-same-origin for its " "sandbox attribute can escape its sandboxing."; }; IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, WarnEscapableSandboxSameOrigin) { EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern(kSandboxEscapeWarningMessage); // Create same-origin iframe. EXPECT_TRUE(ExecJs(current_frame_host(), R"( const iframe = document.createElement("iframe"); iframe.src = location.href; // Same-origin iframe. iframe.sandbox = "allow-same-origin allow-scripts"; document.body.appendChild(iframe); )")); ASSERT_TRUE(console_observer.Wait()); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, WarnEscapableSandboxCrossOrigin) { EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern(kSandboxEscapeWarningMessage); // Create cross-origin iframe. EXPECT_TRUE(ExecJs(current_frame_host(), R"( const iframe = document.createElement("iframe"); // Cross-origin iframe: iframe.src = location.href.replace("a.com", "b.com"); iframe.sandbox = "allow-same-origin allow-scripts"; document.body.appendChild(iframe); )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_EQ(console_observer.messages().size(), 0u); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTestWarnSandboxIneffective, WarnEscapableSandboxSameOriginGrandChild) { EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/empty.html"))); WebContentsConsoleObserver console_observer(web_contents()); console_observer.SetPattern(kSandboxEscapeWarningMessage); // Create a same-origin doubly nested sandboxed iframe. EXPECT_TRUE(ExecJs(current_frame_host(), R"( const child = document.createElement("iframe"); document.body.appendChild(child); const grand_child = child.contentDocument.createElement("iframe"); grand_child.src = location.href; grand_child.sandbox = "allow-same-origin allow-scripts"; child.contentDocument.body.appendChild(grand_child); )")); EXPECT_TRUE(WaitForLoadStop(web_contents())); EXPECT_EQ(console_observer.messages().size(), 0u); } // We may have an unload handler in the main frame or a subframe or nowhere. enum class UnloadFrameType { kMainFrame, kSubFrame, kNone, }; static std::string ToString(UnloadFrameType v) { switch (v) { case UnloadFrameType::kMainFrame: return "MainFrame"; case UnloadFrameType::kSubFrame: return "SubFrame"; case UnloadFrameType::kNone: return "None"; } } // We may navigate the main frame, the subframe that may have an unload handler // or another subframe that will never have an unload handler. enum class NavigateFrameType { kMainFrame, kSubFrame, kOther, }; static std::string ToString(NavigateFrameType v) { switch (v) { case NavigateFrameType::kMainFrame: return "MainFrame"; case NavigateFrameType::kSubFrame: return "SubFrame"; case NavigateFrameType::kOther: return "Other"; } } void AddUnloadHandler(RenderFrameHostImpl* rfh) { ASSERT_TRUE(ExecJs(rfh, "addEventListener('unload', () => {})")); ASSERT_TRUE(rfh->GetSuddenTerminationDisablerState( blink::mojom::SuddenTerminationDisablerType::kUnloadHandler)); } class NavigationSuddenTerminationDisablerTypeBrowserTest : public NavigationBrowserTest { public: NavigationSuddenTerminationDisablerTypeBrowserTest() { feature_list_.InitWithFeaturesAndParameters( /*enabled_features=*/{}, /*disabled_features=*/{network::features::kDeprecateUnload}); } private: base::test::ScopedFeatureList feature_list_; }; class NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest : public NavigationSuddenTerminationDisablerTypeBrowserTest, public ::testing::WithParamInterface< std::tuple<UnloadFrameType, NavigateFrameType>> { public: static std::string DescribeParams( const ::testing::TestParamInfo<ParamType>& info) { return "Unload" + ToString(std::get<0>(info.param)) + "Navigate" + ToString(std::get<1>(info.param)); } protected: UnloadFrameType GetUnloadFrameType() { return std::get<0>(GetParam()); } NavigateFrameType GetNavigateFrameType() { return std::get<1>(GetParam()); } void MaybeAddUnloadHandler() { RenderFrameHostImpl* rfh = nullptr; switch (GetUnloadFrameType()) { case UnloadFrameType::kMainFrame: rfh = current_frame_host(); break; case UnloadFrameType::kSubFrame: rfh = DescendantRenderFrameHostImplAt(current_frame_host(), {0}); break; case UnloadFrameType::kNone: break; } if (rfh) { AddUnloadHandler(rfh); } } RenderFrameHostImpl* GetFrameToNavigate() { switch (GetNavigateFrameType()) { case NavigateFrameType::kMainFrame: return current_frame_host(); case NavigateFrameType::kSubFrame: return DescendantRenderFrameHostImplAt(current_frame_host(), {0}); case NavigateFrameType::kOther: return DescendantRenderFrameHostImplAt(current_frame_host(), {1}); } } bool NavigatedFrameHasUnload() { switch (GetNavigateFrameType()) { case NavigateFrameType::kOther: // It never has unload. return false; case NavigateFrameType::kMainFrame: // Navigating the main will have unload if there is any unload. return GetUnloadFrameType() != UnloadFrameType::kNone; case NavigateFrameType::kSubFrame: // Navigating the subframe will have unload if the subframe has unload. return GetUnloadFrameType() == UnloadFrameType::kSubFrame; } } }; INSTANTIATE_TEST_SUITE_P( All, NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest, ::testing::Combine(::testing::Values(UnloadFrameType::kMainFrame, UnloadFrameType::kSubFrame, UnloadFrameType::kNone), ::testing::Values(NavigateFrameType::kMainFrame, NavigateFrameType::kSubFrame, NavigateFrameType::kOther)), &NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest:: DescribeParams); // Set up a page with 2 subframes. The main frame or one of the subframes may // have an unload handler. Then navigate one of the frames and verify that we // correctly record which type of frame navigates combined with whether it // involved an unload handler. IN_PROC_BROWSER_TEST_P( NavigationSuddenTerminationDisablerTypeWithFrameTypeBrowserTest, RecordUma) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(a,a)"))); current_frame_host()->DisableUnloadTimerForTesting(); // Set up the unload handler if needed. MaybeAddUnloadHandler(); // Navigate the relevant frame and capture histograms. base::HistogramTester histograms; ASSERT_TRUE(NavigateFrameToURL(GetFrameToNavigate()->frame_tree_node(), GURL("about:blank"))); // Check that we got the expected histogram value. uint32_t expected_histogram_value = 0; if (GetNavigateFrameType() == NavigateFrameType::kMainFrame) { expected_histogram_value |= RenderFrameHostImpl:: NavigationSuddenTerminationDisablerType::kMainFrame; } if (NavigatedFrameHasUnload()) { expected_histogram_value |= RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload; } histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.AllOrigins", expected_histogram_value, 1); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.SameOrigin", expected_histogram_value, 1); } // Test that "SameOrigin" only considers frames that have an unbroken path of // same-origin frames from the frame that navigates. IN_PROC_BROWSER_TEST_F( NavigationSuddenTerminationDisablerTypeBrowserTest, NavigationSuddenTerminationDisablerTypeRecordUmaSameOrigin) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b(a))"))); // Set up the unload handler in the a.com subframe. AddUnloadHandler( DescendantRenderFrameHostImplAt(current_frame_host(), {0, 0})); // Navigate the main frame and capture histograms. base::HistogramTester histograms; ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.AllOrigins", RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, 1); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.SameOrigin", RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame, 1); } // Test that we record when the navigation involves restoring from BFCache. // This is tested because the code path for a navigation involving activation // is different from one involving a pageload. IN_PROC_BROWSER_TEST_F( NavigationSuddenTerminationDisablerTypeBrowserTest, NavigationSuddenTerminationDisablerTypeRecordUmaActivation) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); // Set up the unload handler in the b.com page. AddUnloadHandler(current_frame_host()); // Navigate the main frame and capture histograms. base::HistogramTester histograms; ASSERT_TRUE(HistoryGoBack(web_contents())); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.AllOrigins", RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, 1); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.SameOrigin", RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload, 1); } // Ensure that the first navigation of a subframe away from the initial empty // document is recorded correctly. This does not test all possibilities of // histogram value, just that the scenario is counted under the correct // histogram. IN_PROC_BROWSER_TEST_F( NavigationSuddenTerminationDisablerTypeBrowserTest, NavigationSuddenTerminationDisablerTypeRecordUmaInitialEmptyDocument) { GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), url)); // Create a subframe with an unload handler. ASSERT_TRUE(ExecJs(web_contents(), R"( var i = document.createElement("iframe"); document.body.appendChild(i); )")); AddUnloadHandler(DescendantRenderFrameHostImplAt(current_frame_host(), {0})); // Navigate the subframe and capture histograms. base::HistogramTester histograms; uint32_t expected_histogram_value = RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType:: kInitialEmptyDocument | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp; ASSERT_TRUE(NavigateToURLFromRenderer( DescendantRenderFrameHostImplAt(current_frame_host(), {0}), url)); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.AllOrigins", expected_histogram_value, 1); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.SameOrigin", expected_histogram_value, 1); } // Ensure that navigations from non-HTTP(S) pages are recorded correctly. IN_PROC_BROWSER_TEST_F( NavigationSuddenTerminationDisablerTypeBrowserTest, NavigationSuddenTerminationDisablerTypeRecordUmaNotHttp) { GURL blank_url("about:blank"); GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), blank_url)); AddUnloadHandler(current_frame_host()); // Navigate the subframe and capture histograms. base::HistogramTester histograms; uint32_t expected_histogram_value = RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kMainFrame | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kUnload | RenderFrameHostImpl::NavigationSuddenTerminationDisablerType::kNotHttp; ASSERT_TRUE(NavigateToURL(web_contents(), url)); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.AllOrigins", expected_histogram_value, 1); histograms.ExpectUniqueSample( "Navigation.SuddenTerminationDisabler.SameOrigin", expected_histogram_value, 1); } // This is a regression test against https://crbug.com/1145717 - navigating to // invalid/weird URLs (e.g. `about:mumble` or `about://mumble`) shouldn't crash. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, AboutMumble) { // First navigate to an arbitrary http site to lock the renderer process. GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), http_url)); // Verify that browser-initiated navigation to `about:mumble` doesn't crash. // // The renderer will try to commit the original "about:mumble" URL, which will // still show up via `window.location.href`, but the URL passed back in the // DidCommit IPC will be rewritten to `about:blank#blocked` due to // `RenderFrameImpl`'s `IsValidCommitUrl` and therefore this is what we expect // in `GetLastCommittedURL`. ASSERT_FALSE(NavigateToURL(shell(), GURL("about:mumble"))); EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about:mumble"); EXPECT_EQ( shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(), GURL("about:blank#blocked")); // Verify that browser-initiated navigation to `about://mumble` doesn't crash. ASSERT_FALSE(NavigateToURL(shell(), GURL("about://mumble"))); EXPECT_EQ(EvalJs(shell(), "window.location.href"), "about://mumble"); EXPECT_EQ( shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(), GURL("about:blank#blocked")); } // Ensure that the browser process doesn't see a javascript: URL when opening a // new window to a javascript: URL. These URLs are typically handled on the // renderer side, and the renderer should not send the javascript: URL to the // browser in a navigation request. Previously, this was not correctly handled // for initial navigations to javascript: URLs. See https://crbug.com/1357515. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_JavascriptURLs) { GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), http_url)); { SCOPED_TRACE(testing::Message() << "Testing opener case."); base::HistogramTester histograms; ShellAddedObserver new_shell_observer; EXPECT_TRUE( ExecJs(shell(), "window.open('javascript:window.foo=\"bar\"');")); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); // No commit message is sent to the browser in this case, so the last // committed URL is still empty. EXPECT_EQ(GURL(), popup_contents->GetLastCommittedURL()); histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", 0); // The javascript: URL should have run. EXPECT_EQ("bar", EvalJs(popup_contents, "window.foo")); } { SCOPED_TRACE(testing::Message() << "Testing noopener case."); base::HistogramTester histograms; ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs( shell(), "window.open('javascript:window.foo=\"bar\"', '', 'noopener');")); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", 0); // The Javascript URL should not have run in the noopener case, because the // origin should not be inherited according to spec. See: // https://html.spec.whatwg.org/multipage/document-sequences.html#navigable-target-names%3Acreating-a-new-top-level-traversable // https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context // TODO(crbug.com/40236679): Also prevent the origin from being // inherited. EXPECT_EQ(nullptr, EvalJs(popup_contents, "window.foo")); } } // Ensure that opening popups to empty URLs does not fail FilterURL. The // renderer process treats empty URLs as about:blank, but the browser process // does not consider them valid and may treat them as attempts to go to the NTP // in some cases. As a result, the renderer should map empty URLs to about:blank // before making navigation requests. See https://crbug.com/1357515. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FilterURL_EmptyURL) { GURL http_url = embedded_test_server()->GetURL("a.com", "/title1.html"); ASSERT_TRUE(NavigateToURL(shell(), http_url)); { SCOPED_TRACE(testing::Message() << "Testing opener case."); base::HistogramTester histograms; ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), "window.open('');")); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", 0); } { SCOPED_TRACE(testing::Message() << "Testing noopener case."); base::HistogramTester histograms; ShellAddedObserver new_shell_observer; EXPECT_TRUE(ExecJs(shell(), "window.open('', '', 'noopener');")); WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); EXPECT_EQ(url::kAboutBlankURL, EvalJs(popup_contents, "location.href")); EXPECT_EQ(url::kAboutBlankURL, popup_contents->GetLastCommittedURL()); histograms.ExpectTotalCount("BrowserRenderProcessHost.BlockedByFilterURL", 0); } } // Check that an about:blank popup opened from a WebUI page is not allowed to // execute Javascript URLs. chrome:// pages don't allow executing javascript: // URLs, so an about:blank popup opened by one should not be allowed to either, // despite its URL not having the chrome: scheme. See // https://crbug.com/1471305. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, JavascriptURLBlockedInAboutBlankWebUiPopup) { GURL webui_url = GetWebUIURL(kChromeUIGpuHost); ASSERT_TRUE(NavigateToURL(shell(), webui_url)); // Open an about:blank popup which should inherit the WebUI origin, and set // some state in window.foo. Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "popup"); EXPECT_TRUE(new_shell); EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;")); EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); // Try to execute a Javascript URL that modifies window.foo in the popup via // a browser-initiated navigation. This should be blocked, and the value of // window.foo should stay unchanged. new_shell->LoadURL(GURL("javascript:window.foo=456")); EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); } // Same test as above, but with a sandboxed about:blank WebUI popup, which // should still not be allowed to execute Javascript URLs. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, JavascriptURLBlockedInSandboxedWebUiPopup) { GURL webui_url = GetWebUIURL(kChromeUIGpuHost); ASSERT_TRUE(NavigateToURL(shell(), webui_url)); // Add a sandboxed about:blank iframe. { std::string script = "var frame = document.createElement('iframe');\n" "frame.sandbox = 'allow-scripts allow-popups';\n" "document.body.appendChild(frame);\n"; EXPECT_TRUE(ExecJs(shell(), script)); } // Open an about:blank popup from that sandboxed iframe, which should have an // opaque origin with the WebUI origin as the precursor. Set some state in // window.foo in that popup. FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) ->GetPrimaryFrameTree() .root(); Shell* new_shell = OpenPopup(root->child_at(0), GURL(url::kAboutBlankURL), "popup"); EXPECT_TRUE(new_shell); EXPECT_TRUE(ExecJs(new_shell, "window.foo = 123;")); EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); // Try to execute a Javascript URL that modifies window.foo in the popup via // a browser-initiated navigation. This should be blocked, and the value of // window.foo should stay unchanged. new_shell->LoadURL(GURL("javascript:window.foo=456")); EXPECT_EQ(123, EvalJs(new_shell, "window.foo")); } // Test navigation with site instances whose storage partitions are fixed. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FixedStoragePartition) { auto* browser_context = shell()->web_contents()->GetBrowserContext(); auto storage_partition_config = StoragePartitionConfig::Create( browser_context, "NavigationBrowserTest", "FixedStoragePartition", true); auto url = embedded_test_server()->GetURL("/"); auto* shell = Shell::CreateNewWindow( browser_context, url, SiteInstanceImpl::CreateForFixedStoragePartition( browser_context, url, storage_partition_config), gfx::Size()); auto GetSiteInstance = [](Shell* shell) { return static_cast<SiteInstanceImpl*>( shell->web_contents()->GetSiteInstance()); }; EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); // Check navigation. ASSERT_TRUE( NavigateToURL(shell, embedded_test_server()->GetURL("/title1.html"))); EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); // Check opening a window. The new window should stay in the same // BrowsingInstance and StoragePartition. { ShellAddedObserver observer; auto destination = embedded_test_server()->GetURL("a.com", "/title1.html"); EXPECT_TRUE(ExecJs(shell, JsReplace("window.open($1)", destination), EXECUTE_SCRIPT_NO_USER_GESTURE)); auto* popup = observer.GetShell(); EXPECT_TRUE(WaitForLoadStop(popup->web_contents())); EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination); EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(), GetSiteInstance(shell)->GetBrowsingInstanceId()); EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); } // Check opening a window with about:blank at the beginning, and then navigate // it. It should stay in the same BrowsingInstance and StoragePartition. { ShellAddedObserver observer; EXPECT_TRUE(ExecJs(shell, "newWindow = window.open()", EXECUTE_SCRIPT_NO_USER_GESTURE)); auto* popup = observer.GetShell(); EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); auto destination = embedded_test_server()->GetURL("a.com", "/title1.html"); EXPECT_TRUE(ExecJs(shell, JsReplace("newWindow.location.href = $1", destination), EXECUTE_SCRIPT_NO_USER_GESTURE)); EXPECT_TRUE(WaitForLoadStop(popup->web_contents())); EXPECT_EQ(popup->web_contents()->GetLastCommittedURL(), destination); EXPECT_EQ(GetSiteInstance(popup)->GetBrowsingInstanceId(), GetSiteInstance(shell)->GetBrowsingInstanceId()); EXPECT_EQ(GetSiteInstance(popup)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(popup)->IsFixedStoragePartition()); } // Check navigation again. ASSERT_TRUE( NavigateToURL(shell, embedded_test_server()->GetURL("/title2.html"))); EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); // Check navigation that triggers a BrowsingInstance swap, and the storage // partition config should also be preserved. auto browsing_instance_id = GetSiteInstance(shell)->GetBrowsingInstanceId(); ASSERT_TRUE(NavigateToURL( shell, embedded_test_server()->GetURL("c.com", "/title2.html"))); EXPECT_NE(GetSiteInstance(shell)->GetBrowsingInstanceId(), browsing_instance_id); EXPECT_EQ(GetSiteInstance(shell)->GetStoragePartitionConfig(), storage_partition_config); EXPECT_TRUE(GetSiteInstance(shell)->IsFixedStoragePartition()); } // Exercises the restored session history traversal code path which uses // RESTORE navigation types, rather than HISTORY_{SAME|DIFFERENT}_DOCUMENT, // which code might erroneously expect. See https://crbug.com/40068335. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, TraversingToRestoredEntryUsesRestoreType) { ASSERT_TRUE( web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); const GURL url1(embedded_test_server()->GetURL("/title1.html")); const GURL url2(embedded_test_server()->GetURL("/title2.html")); const GURL url3(embedded_test_server()->GetURL("/title2.html#samedoc")); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(NavigateToURL(shell(), url2)); EXPECT_TRUE(NavigateToURL(shell(), url3)); // Clone the tab and load the page. std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone(); WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get()); NavigationController& new_controller = new_tab_impl->GetController(); { TestNavigationObserver clone_observer(new_tab.get()); new_controller.LoadIfNecessary(); clone_observer.Wait(); } // Back to url2 which is a same document navigation but uses RESTORE. { NavigationHandleCommitObserver observer(new_tab.get(), url2); ASSERT_TRUE(HistoryGoBack(new_tab_impl)); EXPECT_EQ(observer.navigation_type(), blink::mojom::NavigationType::RESTORE); } // Back to url1 which is a cross document navigation but uses RESTORE. { NavigationHandleCommitObserver observer(new_tab.get(), url1); ASSERT_TRUE(HistoryGoBack(new_tab_impl)); EXPECT_EQ(observer.navigation_type(), blink::mojom::NavigationType::RESTORE); } } class NavigationBrowserTestDeprecateUnloadOptOut : public NavigationBrowserTest, public ::testing::WithParamInterface<bool> { void SetUpCommandLine(base::CommandLine* command_line) override { NavigationBrowserTest::SetUpCommandLine(command_line); if (IsOptOutEnabled()) { scoped_feature_list_.InitWithFeatures( {network::features::kDeprecateUnload, blink::features::kDeprecateUnloadOptOut}, {}); } else { scoped_feature_list_.InitWithFeatures( {network::features::kDeprecateUnload}, {blink::features::kDeprecateUnloadOptOut}); } } protected: bool IsOptOutEnabled() const { return GetParam(); } private: base::test::ScopedFeatureList scoped_feature_list_; }; INSTANTIATE_TEST_SUITE_P(All, NavigationBrowserTestDeprecateUnloadOptOut, ::testing::Bool()); // Test that enabled/disabled kDeprecateUnloadOptOut has the desired effect. IN_PROC_BROWSER_TEST_P(NavigationBrowserTestDeprecateUnloadOptOut, DeprecateUnloadOptOutFlagRespected) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); // Unload will not run on Android if the page is cacheable. web_contents()->GetController().GetBackForwardCache().DisableForTesting( BackForwardCacheImpl::TEST_USES_UNLOAD_EVENT); // Navigate to a page and install an unload handler with a side-effect. ASSERT_TRUE(NavigateToURL(web_contents(), url_1)); ASSERT_TRUE(ExecJs(web_contents(), R"( localStorage.setItem("unload", "not_dispatched"); addEventListener("unload", () => { localStorage.setItem("unload", "dispatched"); }) )")); // Navigate to a same-site page (to ensure that the unload handler's // side-effect is reliably visible). ASSERT_TRUE(NavigateToURL(web_contents(), url_2)); // Check for the side-effect. ASSERT_EQ(EvalJs(web_contents(), "localStorage.getItem('unload')"), IsOptOutEnabled() ? "dispatched" : "not_dispatched"); } IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FCPMetrics) { GURL url_1(embedded_test_server()->GetURL("/title1.html")); ASSERT_TRUE(NavigateToURL(shell(), url_1)); GURL url_2(embedded_test_server()->GetURL("/title2.html")); NavigationController::LoadURLParams params(url_2); params.transition_type = ui::PageTransitionFromInt( ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); web_contents()->GetController().LoadURLWithParams(params); WaitForHistogramRecordedInChildProcess( "Navigation.FCPFrameSubmittedBeforeSurfaceEmbed"); } // Tests that if the main frame has focus before a same-site navigation, it's // kept after navigation. // Regression test for crbug.com/360705823. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FocusPreservedOnNavigation_MainFrame) { GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); ASSERT_TRUE(NavigateToURL(shell(), url_1)); // Set focus on a button in the main frame. ASSERT_TRUE(ExecJs(current_frame_host(), R"( let button = document.createElement('button'); document.body.appendChild(button); button.focus(); )")); // The main document should have focus. ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();")); // After navigation, the main document should still have focus. ASSERT_TRUE(NavigateToURL(shell(), url_2)); ASSERT_EQ(true, EvalJs(current_frame_host(), "document.hasFocus();")); } // Same as the above test, but the focus is on the iframe. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FocusPreservedOnNavigation_Subframe) { GURL url_1(embedded_test_server()->GetURL("/page_with_iframe.html")); GURL url_2(embedded_test_server()->GetURL("/title2.html")); ASSERT_TRUE(NavigateToURL(shell(), url_1)); // Set focus on a button in the iframe. FrameTreeNode* child_ftn = current_frame_host()->child_at(0); ASSERT_TRUE(ExecJs(child_ftn, R"( let button = document.createElement('button'); document.body.appendChild(button); button.focus(); )")); // The iframe document should have focus. ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); // After navigation, the child document should still have focus. ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2)); ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); } // When the navigation is cross-site, focus is not preserved. IN_PROC_BROWSER_TEST_F(NavigationBrowserTest, FocusNotPreservedOnNavigation_SubframeCrossSite) { if (!AreAllSitesIsolatedForTesting()) { GTEST_SKIP() << "Test needs local -> remote swap"; } GURL url_1(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html")); GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); ASSERT_TRUE(NavigateToURL(shell(), url_1)); // Set focus on a button in the iframe. FrameTreeNode* child_ftn = current_frame_host()->child_at(0); ASSERT_TRUE(ExecJs(child_ftn, R"( let button = document.createElement('button'); document.body.appendChild(button); button.focus(); )")); // The iframe document should have focus. ASSERT_EQ(true, EvalJs(child_ftn, "document.hasFocus();")); // After navigation, the child document should no longer have focus. ASSERT_TRUE(NavigateFrameToURL(child_ftn, url_2)); ASSERT_EQ(false, EvalJs(child_ftn, "document.hasFocus();")); } class NavigationWithPageSwapBrowserTest : public NavigationBrowserTest { public: NavigationWithPageSwapBrowserTest() { feature_list_.InitAndEnableFeature(blink::features::kPageSwapEvent); } bool NavigateBack(WebContentsImpl* contents) { auto result = EvalJs(contents, JsReplace( R"( (async () => { let pageswapfired = new Promise((resolve) => { onpageswap = (e) => { activation = e.activation; resolve(activation); }; }); history.back(); let result = await pageswapfired; return result != null; })(); )")); return result.ExtractBool(); } private: base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest, PageSwapForInitialEntry) { ASSERT_TRUE( web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); // TODO(khushalsagar): Assert that pageswap is fired without activation. The // test script is hitting an issue for the initial Document. ASSERT_TRUE(NavigateToURL(web_contents(), embedded_test_server()->GetURL("/title1.html"))); } IN_PROC_BROWSER_TEST_F(NavigationWithPageSwapBrowserTest, PageSwapWhenTraversingToRestoredEntry) { ASSERT_TRUE( web_contents()->GetController().GetActiveEntry()->IsInitialEntry()); const GURL url1(embedded_test_server()->GetURL("/title1.html")); const GURL url2(embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), url1)); EXPECT_TRUE(NavigateToURL(shell(), url2)); // Clone the tab and load the page. std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone(); WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get()); NavigationController& new_controller = new_tab_impl->GetController(); { TestNavigationObserver clone_observer(new_tab.get()); new_controller.LoadIfNecessary(); clone_observer.Wait(); } ASSERT_TRUE(NavigateBack(new_tab_impl)); } class NavigationBrowserTestPaintHoldingSubframe : public NavigationBrowserTest, public ::testing::WithParamInterface<bool> { public: NavigationBrowserTestPaintHoldingSubframe() { // Paint holding for in-process iframes is only enabled when there is a // ViewTransition. paint_holding_feature_.InitWithFeatures( {blink::features::kPaintHoldingForIframes, blink::features::kViewTransitionOnNavigationForIframes}, {}); const bool enable_render_document = GetParam(); if (enable_render_document) { InitAndEnableRenderDocumentFeature( &render_document_feature_, GetRenderDocumentLevelName(RenderDocumentLevel::kSubframe)); } else { InitAndEnableRenderDocumentFeature( &render_document_feature_, GetRenderDocumentLevelName(RenderDocumentLevel::kCrashedFrame)); } auto* command_line = base::CommandLine::ForCurrentProcess(); // This test requires cross-process iframes. command_line->AppendSwitch(switches::kSitePerProcess); } void SetUp() override { EnablePixelOutput(); NavigationBrowserTest::SetUp(); } void SetUpOnMainThread() override { embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &NavigationBrowserTestPaintHoldingSubframe::HandleSlowStyleSheet, base::Unretained(this))); NavigationBrowserTest::SetUpOnMainThread(); } protected: void WaitForStylesheetRequest() { if (start_response_) { return; } base::RunLoop run_loop; run_loop_ = &run_loop; run_loop.Run(); run_loop_ = nullptr; } void FinishStylesheetRequest(RenderFrameHost* rfh) { std::move(start_response_).Run(); std::move(finish_response_).Run(); // Ensure that the stylesheet response unblocks rendering for this Document. ASSERT_TRUE(ExecJs(rfh, JsReplace( R"( (async () => { let rafFired = new Promise((resolve) => { requestAnimationFrame(resolve); }); await rafFired; })(); )"))); } SkBitmap CopyView(RenderWidgetHostView* view) { base::RunLoop run_loop; run_loop_ = &run_loop; constexpr gfx::Size kOutputSize(10, 10); view->CopyFromSurface( gfx::Rect(), kOutputSize, base::BindOnce(&NavigationBrowserTestPaintHoldingSubframe::OnCopyDone, base::Unretained(this))); run_loop.Run(); run_loop_ = nullptr; return bitmap_; } private: class SlowHttpResponseNoCaching : public SlowHttpResponse { public: explicit SlowHttpResponseNoCaching(GotRequestCallback got_request) : SlowHttpResponse(std::move(got_request)) {} base::StringPairs ResponseHeaders() override { auto response = SlowHttpResponse::ResponseHeaders(); // Disable response caching. response.emplace_back("Cache-Control", "max-age=0"); return response; } }; std::unique_ptr<net::test_server::HttpResponse> HandleSlowStyleSheet( const net::test_server::HttpRequest& request) { if (request.relative_url != "/slow-response") { return nullptr; } return std::make_unique<SlowHttpResponseNoCaching>(base::BindOnce( &NavigationBrowserTestPaintHoldingSubframe::OnStylesheetRequest, base::Unretained(this))); } void OnStylesheetRequest(base::OnceClosure start_response, base::OnceClosure finish_response) { start_response_ = std::move(start_response); finish_response_ = std::move(finish_response); if (run_loop_) { run_loop_->Quit(); } } void OnCopyDone(const SkBitmap& bitmap) { bitmap_ = bitmap; run_loop_->Quit(); } SkBitmap bitmap_; raw_ptr<base::RunLoop> run_loop_; base::OnceClosure start_response_; base::OnceClosure finish_response_; base::test::ScopedFeatureList paint_holding_feature_; base::test::ScopedFeatureList render_document_feature_; }; IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, Basic) { auto* web_contents = shell()->web_contents(); GURL main_url( embedded_test_server()->GetURL("/render-blocking-mainframe.html")); ASSERT_TRUE(NavigateToURL(web_contents, main_url)); const std::string iframe_id = "iframe_id"; RenderFrameHostImpl* subframe_rfh = nullptr; { const std::string kCreateIFrameWithID = R"( const iframe = document.createElement("iframe"); iframe.id = $1; document.body.appendChild(iframe); )"; ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), JsReplace(kCreateIFrameWithID, "iframe_id"))); GURL subframe_url(embedded_test_server()->GetURL( "a.com", "/render-blocking-subframe.html")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // We should have a cross-process iframe which is a local root. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); FinishStylesheetRequest(subframe_rfh); WaitForCopyableViewInWebContents(web_contents); } // The frame is displaying blue. WaitForCopyableViewInFrame(subframe_rfh); auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); { GURL subframe_url(embedded_test_server()->GetURL( "a.com", "/render-blocking-subframe.html?red")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // The subframe RFH could have changed. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); } // The frame should continue to display blue from paint holding. WaitForCopyableViewInWebContents(web_contents); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE); // Respond to the stylesheet request which will resume rendering in the // subframe. FinishStylesheetRequest(subframe_rfh); // Now the frame is displaying red. WaitForCopyableViewInFrame(subframe_rfh); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); } IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, BasicInProcessIframe) { auto* web_contents = shell()->web_contents(); GURL main_url( embedded_test_server()->GetURL("/render-blocking-mainframe.html")); ASSERT_TRUE(NavigateToURL(web_contents, main_url)); const std::string iframe_id = "iframe_id"; RenderFrameHostImpl* subframe_rfh = nullptr; { const std::string kCreateIFrameWithID = R"( const iframe = document.createElement("iframe"); iframe.id = $1; document.body.appendChild(iframe); )"; ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), JsReplace(kCreateIFrameWithID, "iframe_id"))); GURL subframe_url( embedded_test_server()->GetURL("/render-blocking-subframe.html")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // We should have a same-process iframe which is not a local root. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_FALSE(subframe_rfh->is_local_root()); ASSERT_EQ(subframe_rfh->GetProcess(), web_contents->GetPrimaryMainFrame()->GetProcess()); FinishStylesheetRequest(subframe_rfh); WaitForCopyableViewInWebContents(web_contents); } { const std::string kInjectVTOptIn = R"( const style = document.createElement("style"); style.innerHTML = "@view-transition { navigation: auto; }" document.head.appendChild(style); )"; ASSERT_TRUE(ExecJs(subframe_rfh, kInjectVTOptIn)); } // The frame is displaying blue. WaitForCopyableViewInFrame(subframe_rfh); auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); { GURL subframe_url( embedded_test_server()->GetURL("/render-blocking-subframe.html?red")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // The subframe RFH could have changed. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_FALSE(subframe_rfh->is_local_root()); ASSERT_EQ(subframe_rfh->GetProcess(), web_contents->GetPrimaryMainFrame()->GetProcess()); } // The frame should continue to display blue from paint holding. WaitForCopyableViewInWebContents(web_contents); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE); // Respond to the stylesheet request which will resume rendering in the // subframe. FinishStylesheetRequest(subframe_rfh); // Now the frame is displaying red. WaitForCopyableViewInFrame(subframe_rfh); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); } IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, CrossOrigin) { auto* web_contents = shell()->web_contents(); GURL main_url( embedded_test_server()->GetURL("/render-blocking-mainframe.html")); ASSERT_TRUE(NavigateToURL(web_contents, main_url)); const std::string iframe_id = "iframe_id"; RenderFrameHostImpl* subframe_rfh = nullptr; { const std::string kCreateIFrameWithID = R"( const iframe = document.createElement("iframe"); iframe.id = $1; document.body.appendChild(iframe); )"; ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), JsReplace(kCreateIFrameWithID, "iframe_id"))); GURL subframe_url(embedded_test_server()->GetURL( "a.com", "/render-blocking-subframe.html")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // We should have a cross-process iframe which is a local root. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); FinishStylesheetRequest(subframe_rfh); WaitForCopyableViewInWebContents(web_contents); } // The frame is displaying blue. WaitForCopyableViewInFrame(subframe_rfh); auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); { GURL subframe_url(embedded_test_server()->GetURL( "b.com", "/render-blocking-subframe.html?red")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // The subframe RFH could have changed. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); } // The frame is displaying white (from the main frame) because paint holding // is disabled. WaitForCopyableViewInWebContents(web_contents); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE); // Respond to the stylesheet request which will resume rendering in the // subframe. FinishStylesheetRequest(subframe_rfh); // Now the frame is displaying red. WaitForCopyableViewInFrame(subframe_rfh); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); } IN_PROC_BROWSER_TEST_P(NavigationBrowserTestPaintHoldingSubframe, CrashSubframe) { auto* web_contents = shell()->web_contents(); GURL main_url( embedded_test_server()->GetURL("/render-blocking-mainframe.html")); ASSERT_TRUE(NavigateToURL(web_contents, main_url)); const std::string iframe_id = "iframe_id"; RenderFrameHostImpl* subframe_rfh = nullptr; { const std::string kCreateIFrameWithID = R"( const iframe = document.createElement("iframe"); iframe.id = $1; document.body.appendChild(iframe); )"; ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(), JsReplace(kCreateIFrameWithID, "iframe_id"))); GURL subframe_url(embedded_test_server()->GetURL( "a.com", "/render-blocking-subframe.html")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // We should have a cross-process iframe which is a local root. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); FinishStylesheetRequest(subframe_rfh); WaitForCopyableViewInWebContents(web_contents); } // The frame is displaying blue. WaitForCopyableViewInFrame(subframe_rfh); auto bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorBLUE) << cc::GetPNGDataUrl(bitmap); // Crash the subframe. { auto* process = subframe_rfh->GetProcess(); content::ScopedAllowRendererCrashes allow_renderer_crashes(process); RenderProcessHostWatcher watcher( process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); process->Shutdown(content::RESULT_CODE_KILLED); watcher.Wait(); } { GURL subframe_url(embedded_test_server()->GetURL( "a.com", "/render-blocking-subframe.html?red")); TestNavigationObserver load_observer(web_contents); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", subframe_url)); load_observer.WaitForNavigationFinished(); WaitForStylesheetRequest(); // The subframe RFH could have changed. subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); ASSERT_TRUE(subframe_rfh->is_local_root()); } // The frame is displaying white (from the main frame) because paint holding // is disabled. WaitForCopyableViewInWebContents(web_contents); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorWHITE) << cc::GetPNGDataUrl(bitmap); // Respond to the stylesheet request which will resume rendering in the // subframe. FinishStylesheetRequest(subframe_rfh); // Now the frame is displaying red. WaitForCopyableViewInFrame(subframe_rfh); bitmap = CopyView(web_contents->GetRenderWidgetHostView()); EXPECT_EQ(bitmap.getColor(4, 4), SK_ColorRED) << cc::GetPNGDataUrl(bitmap); } INSTANTIATE_TEST_SUITE_P(All, NavigationBrowserTestPaintHoldingSubframe, ::testing::Bool()); RenderFrameHostImpl* GetMainFrameSpeculativeRFH(WebContentsImpl* web_contents) { return web_contents->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); } void VerifyDeferSpeculativeRFHActionUMA(const base::HistogramTester& tester, DeferSpeculativeRFHAction action) { tester.ExpectUniqueSample("Navigation.DeferSpeculativeRFHAction", static_cast<int>(action), 1); } class DeferSpeculativeRFHCreationTest : public NavigationBrowserTest { public: DeferSpeculativeRFHCreationTest() { feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation); // Enable render document for all frames to ensure a speculative RFH // will be created during navigation. InitAndEnableRenderDocumentFeature( &render_document_feature_, GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames)); } private: base::test::ScopedFeatureList feature_list_; base::test::ScopedFeatureList render_document_feature_; }; class DeferSpeculativeRFHCreationRenderProcessTest : public NavigationBrowserTest, public ::testing::WithParamInterface<bool> { public: DeferSpeculativeRFHCreationRenderProcessTest() : warmup_spare_render_process_(GetParam()) { std::map<std::string, std::string> parameters = { {"warmup_spare_process", GetParam() ? "true" : "false"}, }; defer_rfh_feature_list_.InitAndEnableFeatureWithParameters( features::kDeferSpeculativeRFHCreation, parameters); android_spare_rederer_feature_.InitAndEnableFeatureWithParameters( features::kAndroidWarmUpSpareRendererWithTimeout, base::FieldTrialParams{{"spare_renderer_memory_threshold", "0"}}); InitAndEnableRenderDocumentFeature( &render_document_feature_, GetRenderDocumentLevelName(RenderDocumentLevel::kAllFrames)); } // A new renderer process will only be created for a cross-RFH navigation if // it involves a SiteInstanceGroup change, which will happen if site isolation // or BFCache is turned on bool WillWarmupSpareRenderProcess() { return warmup_spare_render_process_; } bool WillAllocateNewProcess() { return AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled(); } private: bool warmup_spare_render_process_; base::test::ScopedFeatureList defer_rfh_feature_list_; base::test::ScopedFeatureList android_spare_rederer_feature_; base::test::ScopedFeatureList render_document_feature_; }; // Verify the common flow for with DeferSpeculativeRFHCreation feature. // The creation of the speculative RFH will be deferred until the network // request is sent. IN_PROC_BROWSER_TEST_P(DeferSpeculativeRFHCreationRenderProcessTest, SpeculativeRFHCreationDeferred) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); RenderProcessHost* first_navigation_process = main_frame()->render_manager()->current_frame_host()->GetProcess(); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); SpareRenderProcessHostManagerImpl::Get().CleanupSparesForTesting(); SpareRenderProcessHostStartedObserver spare_started_observer; GURL url = embedded_test_server()->GetURL("b.com", "/title1.html"); TestNavigationManager nav_manager(web_contents, url); base::HistogramTester histogram_tester; // The speculative RFH shall not be created when the navigation request is // created. ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); DeferSpeculativeRFHAction expected_action = WillWarmupSpareRenderProcess() ? DeferSpeculativeRFHAction::kDeferredWithRenderProcessWarmUp : DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp; VerifyDeferSpeculativeRFHActionUMA(histogram_tester, expected_action); NavigationRequest* navigation_request = NavigationRequest::From(nav_manager.GetNavigationHandle()); RenderProcessHost* created_process = nullptr; if (WillWarmupSpareRenderProcess()) { created_process = spare_started_observer.WaitForSpareRenderProcessStarted(); } ASSERT_EQ(!!created_process, WillWarmupSpareRenderProcess()); ASSERT_TRUE(navigation_request); // The navigation manager pauses the navigation in the WillStartRequest // throttle. The speculative RFH will be created after the throttle completes // and the navigation request is sent. ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); // The loader will not be created until the WillStartRequest throttle check // completed. ASSERT_FALSE(navigation_request->HasLoader()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::NONE); nav_manager.WaitForSpeculativeRenderFrameHostCreation(); // The speculative RFH shall be created after sending the request. ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_TRUE(navigation_request->HasLoader()); RenderFrameHostImplWrapper speculative_rfh( GetMainFrameSpeculativeRFH(web_contents)); ASSERT_TRUE(speculative_rfh); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); if (!WillAllocateNewProcess()) { ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(), first_navigation_process); } else if (WillWarmupSpareRenderProcess()) { ASSERT_EQ(speculative_rfh->GetSiteInstance()->GetProcess(), created_process); } // The speculative RFH shall become the primary RFH when the navigation is // committed. ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), speculative_rfh.get()); } INSTANTIATE_TEST_SUITE_P(All, DeferSpeculativeRFHCreationRenderProcessTest, ::testing::Bool()); // Verify that navigating from a crashed page will create a speculative // RFH at once. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, NavigationFromCrashedFrameNotDeferred) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); // Crash the frame. { auto* process = main_frame() ->GetRenderFrameHostManager() .current_frame_host() ->GetProcess(); content::ScopedAllowRendererCrashes allow_renderer_crashes(process); RenderProcessHostWatcher watcher( process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); process->Shutdown(content::RESULT_CODE_KILLED); watcher.Wait(); } GURL url = embedded_test_server()->GetURL("a.com", "/title2.html"); TestNavigationManager nav_manager(web_contents, url); // Navigation from a crashed frame shall immediately create a speculative RFH. base::HistogramTester histogram_tester; shell()->LoadURL(url); VerifyDeferSpeculativeRFHActionUMA(histogram_tester, DeferSpeculativeRFHAction::kNotDeferred); NavigationRequest* navigation_request = NavigationRequest::From(nav_manager.GetNavigationHandle()); ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_FALSE(navigation_request->HasLoader()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::CURRENT); ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); } // Verify that the creation of the speculative RFH is not deferred for the // web pages. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, CreationNotDeferredForWebUI) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL url = GetWebUIURL(kChromeUIGpuHost); TestNavigationManager nav_manager(web_contents, url); // The speculative RFH shall be created when the navigation starts. base::HistogramTester histogram_tester; shell()->LoadURL(url); VerifyDeferSpeculativeRFHActionUMA(histogram_tester, DeferSpeculativeRFHAction::kNotDeferred); ASSERT_TRUE(nav_manager.WaitForRequestStart()); NavigationRequest* navigation_request = main_frame()->navigation_request(); ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_FALSE(navigation_request->HasLoader()); ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); } // Verify that the creation of the speculative RFH is not deferred for the // pages without a URL loader. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, CreationNotDeferredWithoutURLLoader) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL url("about:blank"); // The speculative RFH shall be created when the navigation starts. base::HistogramTester histogram_tester; ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); VerifyDeferSpeculativeRFHActionUMA(histogram_tester, DeferSpeculativeRFHAction::kNotDeferred); ASSERT_TRUE(WaitForLoadStop(web_contents)); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); } // Verify that the created speculative RFH after the network request will // be correctly replaced if the redirection points to a different site. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, SpeculativeRFHWithRedirect) { ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL redirect_url = embedded_test_server()->GetURL("b.com", "/title1.html"); GURL url = embedded_test_server()->GetURL( "c.com", "/server-redirect?" + redirect_url.spec()); TestNavigationManager nav_manager(web_contents, url); // The speculative RFH shall not be created when the navigation request is // created. base::HistogramTester histogram_tester; ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); VerifyDeferSpeculativeRFHActionUMA( histogram_tester, DeferSpeculativeRFHAction::kDeferredWithoutRenderProcessWarmUp); NavigationRequest* navigation_request = NavigationRequest::From(nav_manager.GetNavigationHandle()); ASSERT_TRUE(navigation_request); // The navigation manager pauses the navigation in the WillStartRequest // throttle. The speculative RFH will be created after the throttle completes // and the navigation request is sent. ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); // The loader will not be created until the WillStartRequest throttle check // completed. ASSERT_FALSE(navigation_request->HasLoader()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::NONE); nav_manager.WaitForSpeculativeRenderFrameHostCreation(); // The speculative RFH shall be created after sending the request. ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_TRUE(navigation_request->HasLoader()); ASSERT_TRUE(GetMainFrameSpeculativeRFH(web_contents)); RenderFrameHostImplWrapper speculative_rfh( GetMainFrameSpeculativeRFH(web_contents)); ASSERT_TRUE(speculative_rfh); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); // After receiving the redirect, a new speculative RFH shall be created for // the new site if site isolation is enabled. ASSERT_TRUE(nav_manager.WaitForResponse()); if (AreAllSitesIsolatedForTesting()) { ASSERT_TRUE(speculative_rfh.IsDestroyed()); } RenderFrameHostImplWrapper new_speculative_rfh( GetMainFrameSpeculativeRFH(web_contents)); ASSERT_TRUE(new_speculative_rfh); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); // The speculative RFH shall become the primary RFH when the navigation is // committed. ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), new_speculative_rfh.get()); } // Test that if there is a navigation pending for commit, the deferred // speculative RFH will not be created event after the request is sent. The new // navigation will be queued until the pending navigation commits. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationTest, NavigateWithPendingCommit) { // TODO(crbug.com/349487596): Enable the test after fixing the unrepsonive // renderer issue. if (!AreAllSitesIsolatedForTesting() && !IsBackForwardCacheEnabled()) { return; } ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); NavigationLogger logger(web_contents); // Create first navigation and pause before commit. GURL url_b = embedded_test_server()->GetURL("b.com", "/title1.html"); TestNavigationManager nav_manager_b(web_contents, url_b); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_b)); nav_manager_b.WaitForSpeculativeRenderFrameHostCreation(); RenderFrameHostImplWrapper speculative_rfh_b( GetMainFrameSpeculativeRFH(web_contents)); ASSERT_TRUE(speculative_rfh_b); ASSERT_TRUE(nav_manager_b.WaitForResponse()); nav_manager_b.ResumeNavigation(); CommitNavigationPauser commit_pauser(speculative_rfh_b.get()); commit_pauser.WaitForCommitAndPause(); // Navigate to a new site, a new speculative RFH will not be created because // of the pending navigation. GURL url_c = embedded_test_server()->GetURL("c.com", "/title1.html"); TestNavigationManager nav_manager_c(web_contents, url_c); ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url_c)); NavigationRequest* navigation_request = NavigationRequest::From(nav_manager_c.GetNavigationHandle()); ASSERT_TRUE(nav_manager_c.WaitForRequestStart()); // Normally, the new navigation will create a speculative RFH after the // network request is sent, but since there is a pre-existing speculative RFH // for a pending commit navigation, the new navigation won't create a // speculative RFH at this point. nav_manager_c.ResumeNavigation(); ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_TRUE(navigation_request->HasLoader()); // Verify that the speculative RFH is not replaced by the new navigation. ASSERT_EQ(speculative_rfh_b.get(), GetMainFrameSpeculativeRFH(web_contents)); ASSERT_FALSE(speculative_rfh_b.IsDestroyed()); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::NONE); commit_pauser.ResumePausedCommit(); ASSERT_TRUE(nav_manager_b.WaitForNavigationFinished()); // Verify that a new speculative RFH will be created after the pending // navigation is committed. ASSERT_TRUE(nav_manager_c.WaitForResponse()); RenderFrameHostImplWrapper new_speculative_rfh( GetMainFrameSpeculativeRFH(web_contents)); ASSERT_TRUE(new_speculative_rfh); ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_PROCESS_RESPONSE); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE); ASSERT_TRUE(nav_manager_c.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(main_frame()->render_manager()->current_frame_host(), new_speculative_rfh.get()); // Check that all the navigations has been committed. auto results = logger.results(); ASSERT_EQ(2u, results.size()); EXPECT_TRUE(results[0].committed); EXPECT_EQ(url_b, results[0].url); EXPECT_TRUE(results[1].committed); EXPECT_EQ(url_c, results[1].url); } class DeferSpeculativeRFHCreationReuseRFHTest : public NavigationBrowserTest { public: DeferSpeculativeRFHCreationReuseRFHTest() { feature_list_.InitAndEnableFeature(features::kDeferSpeculativeRFHCreation); render_document_feature_.InitAndDisableFeature(features::kRenderDocument); } private: base::test::ScopedFeatureList feature_list_; base::test::ScopedFeatureList render_document_feature_; }; // Verify that navigating with the same RFH will reuse the RFH at once. IN_PROC_BROWSER_TEST_F(DeferSpeculativeRFHCreationReuseRFHTest, ReuseSameRFHNotDeferred) { ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); GURL url = embedded_test_server()->GetURL("a.com", "/title1.html"); TestNavigationManager nav_manager(web_contents, url); // Navigation from about:blank will reuse the render frame host. ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents, url)); NavigationRequest* navigation_request = NavigationRequest::From(nav_manager.GetNavigationHandle()); ASSERT_EQ(navigation_request->state(), NavigationRequest::NavigationState::WILL_START_REQUEST); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_FALSE(navigation_request->HasLoader()); ASSERT_TRUE(nav_manager.WaitForResponse()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); ASSERT_EQ(navigation_request->GetAssociatedRFHType(), NavigationRequest::AssociatedRenderFrameHostType::CURRENT); ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); ASSERT_FALSE(GetMainFrameSpeculativeRFH(web_contents)); } class VisualPropertiesSynchronization : public NavigationBrowserTest { public: VisualPropertiesSynchronization() { // The deferral of the RFH prevents the potential race condition that this // regression test is attempting to check. feature_list_.InitAndDisableFeature(features::kDeferSpeculativeRFHCreation); auto* command_line = base::CommandLine::ForCurrentProcess(); // This test requires cross-process iframes. command_line->AppendSwitch(switches::kSitePerProcess); } private: base::test::ScopedFeatureList feature_list_; }; // Regression test for https://crbug.com/352093463. // Verify that when a cross-origin subframe initiates a top-level navigation to // a same-origin (with respect to itself) URL, that the visual properties // are invalidated correctly. IN_PROC_BROWSER_TEST_F(VisualPropertiesSynchronization, RemoteToLocalTransition) { GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); GURL url_b_top_level(embedded_test_server()->GetURL("b.com", "/title1.html")); GURL url_b_iframe(embedded_test_server()->GetURL("b.com", "/title2.html")); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); EXPECT_TRUE(NavigateToURL(shell(), url_a)); EXPECT_TRUE(ExecJs(shell(), "let iframe = document.createElement('iframe');" "iframe.id = 'iframe_id';" "iframe.src = 'about:blank';" "iframe.style = 'width: 0px; height: 0px;';" "document.body.appendChild(iframe);")); EXPECT_TRUE(WaitForLoadStop(web_contents)); // Start a navigation of the top-level document to b.com. Before we leave // the original a.com, load an iframe to b.com which will be hosted in the // same b.com process. content::TestNavigationManager top_level_navigation(web_contents, url_b_top_level); EXPECT_TRUE(ExecJs( shell(), JsReplace("window.location.replace($1);", url_b_top_level))); // Don't proceed with the top-level navigation (wait while we complete our // iframe nav to the same origin to commit). EXPECT_TRUE(top_level_navigation.WaitForLoaderStart()); EXPECT_FALSE(top_level_navigation.was_committed()); // Navigate the iframe to a 'b.com' URL, making a remote frame within the // a.com page. FrameTreeNode* root = FrameTreeNode::From(web_contents->GetPrimaryMainFrame()); CHECK(root->child_count() > 0u); FrameTreeNode* iframe = root->child_at(0); TestFrameNavigationObserver iframe_load_observer_first( iframe->current_frame_host()); ASSERT_TRUE( BeginNavigateIframeToURL(web_contents, "iframe_id", url_b_iframe)); iframe_load_observer_first.WaitForCommit(); EXPECT_EQ(url_b_iframe, iframe_load_observer_first.last_committed_url()); EXPECT_TRUE(iframe_load_observer_first.last_navigation_succeeded()); // Confirm the cross-process iframe process is not (yet) the main frame's // process. RenderFrameHostImpl* subframe_rfh = nullptr; subframe_rfh = static_cast<RenderFrameHostImpl*>( ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); ASSERT_TRUE(subframe_rfh); RenderProcessHost* cross_origin_iframe_process = subframe_rfh->GetProcess(); ASSERT_NE(cross_origin_iframe_process, web_contents->GetPrimaryMainFrame()->GetProcess()); // Allow the top-level navigation to proceed. EXPECT_FALSE(top_level_navigation.was_committed()); EXPECT_TRUE(top_level_navigation.WaitForNavigationFinished()); EXPECT_TRUE(top_level_navigation.was_committed()); // The main frame should now be using the same 'b.com' renderer. ASSERT_EQ(cross_origin_iframe_process, web_contents->GetPrimaryMainFrame()->GetProcess()); // Verify that the browser side's VisualProperties' visible viewport size is // non-zero. root = FrameTreeNode::From(web_contents->GetPrimaryMainFrame()); auto* root_rwh = root->current_frame_host()->GetRenderWidgetHost(); std::optional<blink::VisualProperties> visual_properties = root_rwh->LastComputedVisualProperties(); EXPECT_TRUE(visual_properties); EXPECT_NE(gfx::Size(0, 0), visual_properties->visible_viewport_size_device_px); // Ensure a frame has been produced. ASSERT_TRUE( EvalJsAfterLifecycleUpdate(web_contents->GetPrimaryMainFrame(), "", "") .error.empty()); // Verify the renderer received the correct size for the viewport. EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerWidth;") .ExtractDouble(), 0); EXPECT_GT(EvalJs(web_contents->GetPrimaryMainFrame(), "window.innerHeight;") .ExtractDouble(), 0); } #if BUILDFLAG(IS_ANDROID) class AndroidPrewarmSpareRendererTest : public NavigationBrowserTest, public ::testing::WithParamInterface<std::tuple<std::string, bool>> { public: AndroidPrewarmSpareRendererTest() { std::map<std::string, std::string> parameters = { {"spare_renderer_creation_timing", std::get<0>(GetParam())}, {"spare_renderer_timeout_seconds", std::get<1>(GetParam()) ? "10" : "-1"}, }; feature_list_.InitWithFeaturesAndParameters( /*enabled_features=*/{{features::kAndroidWarmUpSpareRendererWithTimeout, parameters}}, /*disabled_features=*/{{features::kSpareRendererForSitePerProcess}}); } void SetUpCommandLine(base::CommandLine* command_line) override { // Enable site per process so that the navigation will take // the spare process. command_line->AppendSwitch(switches::kSitePerProcess); } bool SpareRendererHasTimeout() { return std::get<1>(GetParam()); } private: base::test::ScopedFeatureList feature_list_; }; INSTANTIATE_TEST_SUITE_P( All, AndroidPrewarmSpareRendererTest, testing::Combine( testing::Values( features::kAndroidSpareRendererCreationAfterLoading, features::kAndroidSpareRendererCreationAfterFirstPaint, features::kAndroidSpareRendererCreationDelayedDuringLoading), testing::Bool())); IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, ReuseSpareRenderer) { auto& spare_manager = SpareRenderProcessHostManagerImpl::Get(); spare_manager.CleanupSparesForTesting(); SpareRenderProcessHostStartedObserver spare_started_observer; ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); RenderProcessHost* created_process = spare_started_observer.WaitForSpareRenderProcessStarted(); ASSERT_TRUE(!!created_process); ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process)); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); ASSERT_EQ(web_contents->GetSiteInstance()->GetProcess(), created_process); } IN_PROC_BROWSER_TEST_P(AndroidPrewarmSpareRendererTest, RendererTimeout) { scoped_refptr<base::TestMockTimeTaskRunner> task_runner = new base::TestMockTimeTaskRunner(); auto& spare_manager = SpareRenderProcessHostManagerImpl::Get(); spare_manager.SetDeferTimerTaskRunnerForTesting(task_runner); const base::TimeDelta kTimeout = base::Seconds(10); spare_manager.CleanupSparesForTesting(); SpareRenderProcessHostStartedObserver spare_started_observer; ASSERT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); RenderProcessHost* created_process = spare_started_observer.WaitForSpareRenderProcessStarted(); ASSERT_TRUE(!!created_process); ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process)); if (!SpareRendererHasTimeout()) { // Warming up a spare renderer with a timeout shall not override // a spare renderer without a timeout. spare_manager.WarmupSpare(shell()->web_contents()->GetBrowserContext(), kTimeout); } task_runner->FastForwardBy(kTimeout); base::RunLoop().RunUntilIdle(); if (SpareRendererHasTimeout()) { EXPECT_TRUE(spare_manager.GetSpares().empty()); } else { ASSERT_THAT(spare_manager.GetSpares(), testing::ElementsAre(created_process)); } } #endif // BUILDFLAG(IS_ANDROID) class HstsUpgradeBrowserTest : public NavigationBrowserTest { public: HstsUpgradeBrowserTest() { feature_list_.InitAndEnableFeature( net::features::kHstsTopLevelNavigationsOnly); } void SetUpOnMainThread() override { NavigationBrowserTest::SetUpOnMainThread(); ASSERT_TRUE(embedded_https_test_server().Start()); } content::test::FencedFrameTestHelper& fenced_frame_test_helper() { return fenced_frame_test_helper_; } private: content::test::FencedFrameTestHelper fenced_frame_test_helper_; base::test::ScopedFeatureList feature_list_; }; // Tests that when HstsTopLevelNavigationsOnly is enabled only top-level // navigations will be upgraded by HSTS. IN_PROC_BROWSER_TEST_F(HstsUpgradeBrowserTest, UpgradeTopLevelOnly) { // Url that loads a page with the HSTS url, http://b.com, as an iframe under // an http://a.com main frame. GURL hsts_url_in_iframe_http = embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)"); // The expected url of the HSTS url, http://b.com, iframe. GURL url_of_hsts_frame_http = embedded_test_server()->GetURL( "b.com", "/cross_site_iframe_factory.html?b()"); { // Add hostname to the TransportSecurityState. base::Time expiry = base::Time::Now() + base::Days(100); bool include_subdomains = false; auto* network_context = web_contents() ->GetBrowserContext() ->GetDefaultStoragePartition() ->GetNetworkContext(); base::RunLoop run_loop; network_context->AddHSTS(url_of_hsts_frame_http.host(), expiry, include_subdomains, run_loop.QuitClosure()); run_loop.Run(); } // Navigate the main frame to the HSTS url, http://b.com. // Note: Because the http and https embedded test servers run on different // (non-default) ports the test will fail if we try to navigate to // `url_of_hsts_frame_http` because HSTS will simply change the scheme to // https, but the port will remain the http server's port. To work around this // we can take an https url, `hsts_url_main_frame_https`, and change its // scheme to http which will then be upgraded by HSTS back to https and will // load correctly. // Url of an https://b.com page. GURL hsts_url_main_frame_https = embedded_https_test_server().GetURL("b.com", "/title1.html"); GURL::Replacements scheme_replacement; scheme_replacement.SetSchemeStr("http"); // The navigation should get upgraded to https://b.com. EXPECT_TRUE(NavigateToURL( web_contents(), /*url=*/hsts_url_main_frame_https.ReplaceComponents(scheme_replacement), /*expected_commit_url=*/hsts_url_main_frame_https)); // Now navigate to an http://a.com page that embeds an http://b.com iframe. EXPECT_TRUE(NavigateToURL(web_contents(), hsts_url_in_iframe_http)); auto* sub_frame = main_frame()->child_at(0); // The http://b.com iframe should not have been upgraded. EXPECT_EQ(url_of_hsts_frame_http, sub_frame->current_frame_host()->GetLastCommittedURL()); // Fenced Frames are treated as top-level frames in many cases, but not for // HSTS upgrades. Requests for fenced frames should not be upgraded. content::RenderFrameHost* fenced_frame = fenced_frame_test_helper().CreateFencedFrame( main_frame()->current_frame_host(), url_of_hsts_frame_http); ASSERT_TRUE(fenced_frame); EXPECT_EQ(url_of_hsts_frame_http, fenced_frame->GetLastCommittedURL()); } } // namespace content