0
Files
src/content/browser/navigation_browsertest.cc
Joshua Thomas eb08480876 Exclude unpartitioned cookies from anonymous context reads
Before, unpartitioned cookies were simply ignored by the
CookieMonster. This meant that there was no trace of unpartitioned
cookies in things like DevTools.

This change makes DevTools aware of these unpartitioned cookies
from anonymous contexts so that they can be shown as blocked in
DevTools.

Bug: 399826579
Change-Id: Ieff737dc624c275b9ace9d2ce25a0b9f991333d9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6343479
Reviewed-by: Chris Fredrickson <cfredric@chromium.org>
Commit-Queue: Joshua Thomas <masnoble@chromium.org>
Reviewed-by: Camille Lamy <clamy@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1438725}
2025-03-27 07:23:11 -07:00

9911 lines
415 KiB
C++

// 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/to_string.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/test/values_test_util.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_devtools_protocol_client.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,
CheckCookiesForCredentiallessIframeNavigation) {
GURL main_url =
embedded_test_server()->GetURL("/page_with_credentialless_iframe.html");
GURL iframe_url_1 = embedded_test_server()->GetURL("/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Set a cookie on the main frame
EXPECT_TRUE(ExecJs(main_frame(), "document.cookie = 'name=main;';"));
EXPECT_EQ("name=main", EvalJs(main_frame(), "document.cookie;"));
// 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_TRUE(child->Credentialless());
EXPECT_TRUE(child->current_frame_host()->IsCredentialless());
EXPECT_TRUE(ExecJs(child->current_frame_host(), "window.credentialless"));
// Set up devtools client so we can check which cookies were sent and blocked
// with frame navigation
TestDevToolsProtocolClient fenced_frame_devtools_client;
fenced_frame_devtools_client.AttachToFrameTreeHost(
child->current_frame_host());
fenced_frame_devtools_client.SendCommandAsync("Network.enable");
fenced_frame_devtools_client.ClearNotifications();
// Set a cookie on the frame and reload so we can see which cookies are
// included in the network request
EXPECT_TRUE(ExecJs(child->current_frame_host(),
"document.cookie = 'name=credentialless;';"));
EXPECT_EQ("name=credentialless",
EvalJs(child->current_frame_host(), "document.cookie;"));
EXPECT_TRUE(ExecJs(child->current_frame_host(), "location.reload();"));
// Check associated cookies according to devtools
base::Value::Dict params = fenced_frame_devtools_client.WaitForNotification(
"Network.requestWillBeSentExtraInfo", /*allow_existing=*/true);
const base::Value::List* associated_cookies =
params.FindList("associatedCookies");
EXPECT_THAT(
associated_cookies,
testing::Pointee(testing::UnorderedElementsAre(
base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({
"blockedReasons": [ "AnonymousContext" ],
"cookie" : {
"name": "name",
"value": "main"
}
})")),
testing::AllOf(
base::test::IsSupersetOfValue(base::test::ParseJsonDict(R"({
"blockedReasons": [ ],
"cookie" : {
"name": "name",
"value": "credentialless"
}
})")),
testing::ResultOf(
[](const base::Value& dict) {
return dict.GetDict().FindList("blockedReasons");
},
testing::Pointee(testing::IsEmpty()))))));
fenced_frame_devtools_client.DetachProtocolClient();
}
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", base::ToString(GetParam())},
};
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