blob: 2dde21fa609ee1df229f796dd30034ddb9c1f27a [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/render_frame_host_manager_browsertest.h"
#include <stddef.h>
#include <stdint.h>
#include <functional>
#include <memory>
#include <set>
#include "base/cfi_buildflags.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_proxy_host.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/spare_render_process_host_manager_impl.h"
#include "content/browser/site_info.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webui/web_ui_controller_factory_registry.h"
#include "content/browser/webui/web_ui_impl.h"
#include "content/common/content_constants_internal.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/public/browser/browser_child_process_host.h"
#include "content/public/browser/child_process_launcher_utils.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_ui_message_handler.h"
#include "content/public/common/bindings_policy.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/isolated_world_ids.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/commit_message_delayer.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/navigation_handle_observer.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_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/render_document_feature.h"
#include "content/test/storage_partition_test_helpers.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "net/test/url_request/url_request_failed_job.h"
#include "services/network/public/cpp/features.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
using base::ASCIIToUTF16;
namespace content {
namespace {
// Helper function that return true in cases where the current process model
// will return the same SiteInstance for a cross-process navigation.
bool ExpectSameSiteInstance() {
return !AreStrictSiteInstancesEnabled() &&
!CanCrossSiteNavigationsProactivelySwapBrowsingInstances();
}
class TestWebUIMessageHandler : public WebUIMessageHandler {
public:
using WebUIMessageHandler::AllowJavascript;
using WebUIMessageHandler::IsJavascriptAllowed;
protected:
void RegisterMessages() override {}
};
// This class implements waiting for RenderFrameHost destruction. It relies on
// the fact that RenderFrameDeleted event is fired when RenderFrameHost is
// destroyed.
// Note: RenderFrameDeleted is also fired when the process associated with the
// RenderFrameHost crashes, so this cannot be used in cases where process dying
// is expected.
class RenderFrameHostDestructionObserver : public WebContentsObserver {
public:
explicit RenderFrameHostDestructionObserver(RenderFrameHost* rfh)
: WebContentsObserver(WebContents::FromRenderFrameHost(rfh)),
message_loop_runner_(new MessageLoopRunner),
deleted_(false),
render_frame_host_(rfh) {}
~RenderFrameHostDestructionObserver() override = default;
bool deleted() const { return deleted_; }
void Wait() {
if (deleted_) {
return;
}
message_loop_runner_->Run();
}
// WebContentsObserver implementation:
void RenderFrameDeleted(RenderFrameHost* rfh) override {
if (rfh == render_frame_host_) {
CHECK(!deleted_);
deleted_ = true;
}
if (deleted_ && message_loop_runner_->loop_running()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, message_loop_runner_->QuitClosure());
}
}
private:
scoped_refptr<MessageLoopRunner> message_loop_runner_;
bool deleted_;
raw_ptr<RenderFrameHost, AcrossTasksDanglingUntriaged> render_frame_host_;
};
// A NavigationThrottle implementation that blocks all outgoing navigation
// requests for a specific WebContents. It is used to block navigations to
// WebUI URLs in tests.
class RequestBlockingNavigationThrottle : public NavigationThrottle {
public:
explicit RequestBlockingNavigationThrottle(
NavigationThrottleRegistry& registry)
: NavigationThrottle(registry) {}
RequestBlockingNavigationThrottle(const RequestBlockingNavigationThrottle&) =
delete;
RequestBlockingNavigationThrottle& operator=(
const RequestBlockingNavigationThrottle&) = delete;
static void Create(NavigationThrottleRegistry& registry) {
registry.AddThrottle(
std::make_unique<RequestBlockingNavigationThrottle>(registry));
}
private:
ThrottleCheckResult WillStartRequest() override {
return NavigationThrottle::BLOCK_REQUEST;
}
const char* GetNameForLogging() override {
return "RequestBlockingNavigationThrottle";
}
};
// Helper function for error page navigations that makes sure that the last
// committed origin on |node| is an opaque origin with a precursor that matches
// |url|'s origin.
// Returns true if the frame has an opaque origin with the expected precursor
// information. Otherwise returns false.
bool IsOriginOpaqueAndCompatibleWithURL(FrameTreeNode* node, const GURL& url) {
url::Origin frame_origin =
node->current_frame_host()->GetLastCommittedOrigin();
if (!frame_origin.opaque()) {
LOG(ERROR) << "Frame origin was not opaque. " << frame_origin;
return false;
}
const GURL url_origin = url.DeprecatedGetOriginAsURL();
const GURL precursor_origin =
frame_origin.GetTupleOrPrecursorTupleIfOpaque().GetURL();
if (url_origin != precursor_origin) {
LOG(ERROR) << "url_origin '" << url_origin << "' != precursor_origin '"
<< precursor_origin << "'";
return false;
}
return true;
}
bool IsMainFrameOriginOpaqueAndCompatibleWithURL(Shell* shell,
const GURL& url) {
return IsOriginOpaqueAndCompatibleWithURL(
static_cast<WebContentsImpl*>(shell->web_contents())
->GetPrimaryFrameTree()
.root(),
url);
}
bool HasErrorPageSiteInfo(SiteInstance* site_instance) {
auto* site_instance_impl = static_cast<SiteInstanceImpl*>(site_instance);
return site_instance_impl->GetSiteInfo().is_error_page();
}
bool HasErrorPageProcessLock(SiteInstance* site_instance) {
return site_instance->GetProcess()->GetProcessLock().is_error_page();
}
} // anonymous namespace
RenderFrameHostManagerTest::RenderFrameHostManagerTest() : foo_com_("foo.com") {
replace_host_.SetHostStr(foo_com_);
InitAndEnableRenderDocumentFeature(&feature_list_, GetParam());
}
RenderFrameHostManagerTest::~RenderFrameHostManagerTest() = default;
void RenderFrameHostManagerTest::SetUpOnMainThread() {
// Support multiple sites on the test server.
host_resolver()->AddRule("*", "127.0.0.1");
}
void RenderFrameHostManagerTest::DisableBackForwardCache(
BackForwardCacheImpl::DisableForTestingReason reason) const {
return static_cast<WebContentsImpl*>(shell()->web_contents())
->GetController()
.GetBackForwardCache()
.DisableForTesting(reason);
}
void RenderFrameHostManagerTest::StartServer() {
ASSERT_TRUE(embedded_test_server()->Start());
foo_host_port_ = embedded_test_server()->host_port_pair();
foo_host_port_.set_host(foo_com_);
}
void RenderFrameHostManagerTest::StartEmbeddedServer() {
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
std::unique_ptr<content::URLLoaderInterceptor>
RenderFrameHostManagerTest::SetupRequestFailForURL(const GURL& url) {
return URLLoaderInterceptor::SetupRequestFailForURL(url,
net::ERR_DNS_TIMED_OUT);
}
// Returns a URL on foo.com with the given path.
GURL RenderFrameHostManagerTest::GetCrossSiteURL(const std::string& path) {
GURL cross_site_url(embedded_test_server()->GetURL(path));
return cross_site_url.ReplaceComponents(replace_host_);
}
void RenderFrameHostManagerTest::NavigateToPageWithLinks(Shell* shell) {
EXPECT_TRUE(NavigateToURL(
shell, embedded_test_server()->GetURL("/click-noreferrer-links.html")));
// Rewrite selected links on the page to be actual cross-site (bar.com)
// URLs. This does not use the /cross-site/ redirector, since that creates
// links that initially look same-site.
std::string script = "setOriginForLinks('http://bar.com:" +
embedded_test_server()->base_url().port() + "/');";
EXPECT_TRUE(ExecJs(shell, script));
}
// Web pages should not have script access to the unloaded page.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, NoScriptAccessAfterUnload) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Open a same-site link in a new window.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
EXPECT_EQ(orig_site_instance, new_shell->web_contents()->GetSiteInstance());
// We should have access to the opened window's location.
EXPECT_EQ(true, EvalJs(shell(), "testScriptAccessToWindow();"));
// Now navigate the new window to a different site.
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(
new_shell, embedded_test_server()->GetURL("foo.com", "/title1.html")));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
if (AreStrictSiteInstancesEnabled()) {
EXPECT_NE(orig_site_instance, new_site_instance);
} else {
EXPECT_EQ(orig_site_instance, new_site_instance);
}
// We should no longer have script access to the opened window's location.
EXPECT_EQ(false, EvalJs(shell(), "testScriptAccessToWindow();"));
// We now navigate the window to an about:blank page.
TestNavigationObserver navigation_observer(new_shell->web_contents());
EXPECT_EQ(true, EvalJs(shell(), "clickBlankTargetedLink();"));
// Wait for the navigation in the new window to finish.
navigation_observer.Wait();
GURL blank_url(url::kAboutBlankURL);
EXPECT_EQ(blank_url, new_shell->web_contents()->GetLastCommittedURL());
EXPECT_EQ(orig_site_instance, new_shell->web_contents()->GetSiteInstance());
// We should have access to the opened window's location.
EXPECT_EQ(true, EvalJs(shell(), "testScriptAccessToWindow();"));
}
// Test for crbug.com/24447. Following a cross-site link with rel=noreferrer
// and target=_blank should create a new SiteInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SwapProcessWithRelNoreferrerAndTargetBlank) {
StartEmbeddedServer();
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a rel=noreferrer + target=blank link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickNoRefTargetBlankLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
EXPECT_EQ("/title2.html", new_shell->web_contents()->GetVisibleURL().path());
// Check that `window.opener` is not set.
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
// Should have a new SiteInstance.
scoped_refptr<SiteInstance> noref_blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, noref_blank_site_instance);
EXPECT_FALSE(noref_blank_site_instance->IsRelatedSiteInstance(
orig_site_instance.get()));
}
// Same as above, but for 'noopener'
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SwapProcessWithRelNoopenerAndTargetBlank) {
StartEmbeddedServer();
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a rel=noreferrer + target=blank link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickNoOpenerTargetBlankLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
EXPECT_EQ("/title2.html", new_shell->web_contents()->GetVisibleURL().path());
// Check that `window.opener` is not set.
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
// Check that the referrer is set correctly.
std::string expected_referrer =
embedded_test_server()->GetURL("/").DeprecatedGetOriginAsURL().spec();
EXPECT_EQ(true, EvalJs(new_shell,
"document.referrer == '" + expected_referrer + "';"));
// Should have a new SiteInstance.
scoped_refptr<SiteInstance> noopener_blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, noopener_blank_site_instance);
EXPECT_FALSE(noopener_blank_site_instance->IsRelatedSiteInstance(
orig_site_instance.get()));
}
// 'noopener' also works from 'window.open'
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SwapProcessWithWindowOpenAndNoopener) {
StartEmbeddedServer();
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get());
// Test opening a window with the 'noopener' feature.
ShellAddedObserver new_shell_observer;
// We should not get a reference to the opened window.
EXPECT_EQ(
false,
EvalJs(
shell(),
"openWindowWithTargetAndFeatures('/title2.html', '', 'noopener');"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/title2.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Check that `window.opener` is not set.
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
// Check that the referrer is set correctly.
std::string expected_referrer =
embedded_test_server()->GetURL("/click-noreferrer-links.html").spec();
EXPECT_EQ(true, EvalJs(new_shell,
"document.referrer == '" + expected_referrer + "';"));
// Should have a new SiteInstance.
scoped_refptr<SiteInstance> noopener_blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, noopener_blank_site_instance);
EXPECT_FALSE(noopener_blank_site_instance->IsRelatedSiteInstance(
orig_site_instance.get()));
}
// As of crbug.com/69267, we create a new BrowsingInstance (and SiteInstance)
// for rel=noreferrer links in new windows, even to same site pages and named
// targets.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SwapProcessWithSameSiteRelNoreferrer) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a same-site rel=noreferrer + target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteNoRefTargetedLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
// Opens in new window.
EXPECT_EQ("/title2.html", new_shell->web_contents()->GetVisibleURL().path());
// Check that `window.opener` is not set.
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
// Should have a new SiteInstance (in a new BrowsingInstance).
scoped_refptr<SiteInstance> noref_blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, noref_blank_site_instance);
EXPECT_FALSE(noref_blank_site_instance->IsRelatedSiteInstance(
orig_site_instance.get()));
}
// Same as above, but for 'noopener'
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SwapProcessWithSameSiteRelNoopener) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a same-site rel=noopener + target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteNoOpenerTargetedLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
// Opens in new window.
EXPECT_EQ("/title2.html", new_shell->web_contents()->GetVisibleURL().path());
// Check that `window.opener` is not set.
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
// Should have a new SiteInstance (in a new BrowsingInstance).
scoped_refptr<SiteInstance> noref_blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, noref_blank_site_instance);
EXPECT_FALSE(noref_blank_site_instance->IsRelatedSiteInstance(
orig_site_instance.get()));
}
// Test for crbug.com/24447. Following a cross-site link with just
// target=_blank should not create a new SiteInstance, unless we
// are running in --site-per-process mode.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
DontSwapProcessWithOnlyTargetBlank) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a target=blank link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickTargetBlankLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the cross-site transition in the new tab to finish.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/title2.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance unless we're in site-per-process mode, or
// default SiteInstanceGroups mode.
scoped_refptr<SiteInstance> blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
if (AreStrictSiteInstancesEnabled()) {
EXPECT_NE(orig_site_instance, blank_site_instance);
} else {
EXPECT_EQ(orig_site_instance, blank_site_instance);
}
}
// Test for crbug.com/24447. Following a cross-site link with rel=noreferrer
// and no target=_blank should not create a new SiteInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
DontSwapProcessWithOnlyRelNoreferrer) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a rel=noreferrer link.
EXPECT_EQ(true, EvalJs(shell(), "clickNoRefLink();"));
// Wait for the cross-site transition in the current tab to finish.
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Opens in same window.
EXPECT_EQ(1u, Shell::windows().size());
EXPECT_EQ("/title2.html",
shell()->web_contents()->GetLastCommittedURL().path());
scoped_refptr<SiteInstance> noref_site_instance(
shell()->web_contents()->GetSiteInstance());
if (ExpectSameSiteInstance()) {
EXPECT_EQ(orig_site_instance, noref_site_instance);
} else {
EXPECT_NE(orig_site_instance, noref_site_instance);
}
}
// Same as above, but for 'noopener'
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
DontSwapProcessWithOnlyRelNoOpener) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a rel=noreferrer link.
EXPECT_EQ(true, EvalJs(shell(), "clickNoRefLink();"));
// Wait for the cross-site transition in the current tab to finish.
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Opens in same window.
EXPECT_EQ(1u, Shell::windows().size());
EXPECT_EQ("/title2.html",
shell()->web_contents()->GetLastCommittedURL().path());
scoped_refptr<SiteInstance> noref_site_instance(
shell()->web_contents()->GetSiteInstance());
if (ExpectSameSiteInstance()) {
EXPECT_EQ(orig_site_instance, noref_site_instance);
} else {
EXPECT_NE(orig_site_instance, noref_site_instance);
}
}
// Test for crbug.com/116192. Targeted links should still work after the
// named target window has swapped processes.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
AllowTargetedNavigationsAfterSwap) {
StartEmbeddedServer();
// Ensure that the first and second page are isolated from each other (even on
// Android, where site-per-process is not the default).
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetedLink()"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new tab to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
scoped_refptr<SiteInstance> blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, blank_site_instance);
// Now navigate the new tab to a different site.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
// Clicking the original link in the first tab should cause us to swap back.
{
TestNavigationObserver navigation_observer(new_shell->web_contents());
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetedLink()"));
navigation_observer.Wait();
}
// Should have swapped back and shown the new window again.
scoped_refptr<SiteInstance> revisit_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, revisit_site_instance);
// If it navigates away to another process, the original window should
// still be able to close it (using a cross-process close message).
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
EXPECT_EQ(new_site_instance.get(),
new_shell->web_contents()->GetSiteInstance());
WebContentsDestroyedWatcher close_watcher(new_shell->web_contents());
EXPECT_EQ(true, EvalJs(shell(), "testCloseWindow()"));
close_watcher.Wait();
}
// Test that setting the opener to null in a window affects cross-process
// navigations, including those to existing entries. http://crbug.com/156669.
// This test crashes under ThreadSanitizer, http://crbug.com/356758.
#if defined(THREAD_SANITIZER)
#define MAYBE_DisownOpener DISABLED_DisownOpener
#else
#define MAYBE_DisownOpener DisownOpener
#endif
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, MAYBE_DisownOpener) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed to get a non-default
// SiteInstance for navigations to this origin.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a target=_blank link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetBlankLink();"));
Shell* new_shell = new_shell_observer.GetShell();
EXPECT_TRUE(new_shell->web_contents()->HasOpener());
// Wait for the navigation in the new tab to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/title2.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
scoped_refptr<SiteInstance> blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, blank_site_instance);
// Now navigate the new tab to a different site.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
EXPECT_TRUE(new_shell->web_contents()->HasOpener());
// Now disown the opener.
EXPECT_TRUE(ExecJs(new_shell, "window.opener = null;"));
EXPECT_FALSE(new_shell->web_contents()->HasOpener());
// Go back and ensure the opener is still null.
{
TestNavigationObserver back_nav_load_observer(new_shell->web_contents());
new_shell->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
EXPECT_FALSE(new_shell->web_contents()->HasOpener());
// Now navigate forward again (creating a new process) and check opener.
EXPECT_TRUE(NavigateToURL(new_shell, cross_site_url));
EXPECT_EQ(true, EvalJs(new_shell, "window.opener == null;"));
EXPECT_FALSE(new_shell->web_contents()->HasOpener());
}
// Test that subframes can disown their openers. http://crbug.com/225528.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, DisownSubframeOpener) {
const GURL frame_url("data:text/html,<iframe name=\"foo\"></iframe>");
EXPECT_TRUE(NavigateToURL(shell(), frame_url));
// Give the frame an opener using window.open.
EXPECT_TRUE(ExecJs(shell(), "window.open('about:blank','foo');"));
// Check that the browser process updates the subframe's opener.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(root, root->child_at(0)->opener());
EXPECT_EQ(
nullptr,
root->child_at(0)->first_live_main_frame_in_original_opener_chain());
// Now disown the frame's opener. Shouldn't crash.
EXPECT_TRUE(ExecJs(shell(), "window.frames[0].opener = null;"));
// Check that the subframe's opener in the browser process is disowned.
EXPECT_EQ(nullptr, root->child_at(0)->opener());
EXPECT_EQ(
nullptr,
root->child_at(0)->first_live_main_frame_in_original_opener_chain());
}
// Check that window.name is preserved for top frames when they navigate
// cross-process. See https://crbug.com/504164.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
PreserveTopFrameWindowNameOnCrossProcessNavigations) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed it is placed in a different
// process.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
GURL main_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Open a popup using window.open with a 'foo' window.name.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
EXPECT_TRUE(new_shell);
// The window.name for the new popup should be "foo".
EXPECT_EQ("foo", EvalJs(new_shell, "window.name;"));
// Now navigate the new tab to a different site.
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, foo_url));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance->GetProcess(), new_site_instance->GetProcess());
// window.name should still be "foo".
EXPECT_EQ("foo", EvalJs(new_shell, "window.name;"));
// Open another popup from the 'foo' popup and navigate it cross-site.
Shell* new_shell2 = OpenPopup(new_shell, GURL(url::kAboutBlankURL), "bar");
EXPECT_TRUE(new_shell2);
GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(new_shell2, bar_url));
// Check that the new popup's window.opener has name "foo", which verifies
// that new swapped-out RenderViews also propagate window.name. This has to
// be done via window.open, since window.name isn't readable cross-origin.
EXPECT_EQ(true,
EvalJs(new_shell2, "window.opener === window.open('','foo');"));
}
// Test for crbug.com/99202. PostMessage calls should still work after
// navigating the source and target windows to different sites.
// Specifically:
// 1) Create 3 windows (opener, "foo", and _blank) and send "foo" cross-process.
// 2) Fail to post a message from "foo" to opener with the wrong target origin.
// 3) Post a message from "foo" to opener, which replies back to "foo".
// 4) Post a message from _blank to "foo".
// 5) Post a message from "foo" to a subframe of opener, which replies back.
// 6) Post a message from _blank to a subframe of "foo".
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SupportCrossProcessPostMessage) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed it is placed in a different
// process.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance and RVHM for later comparison.
WebContents* opener_contents = shell()->web_contents();
scoped_refptr<SiteInstance> orig_site_instance(
opener_contents->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
RenderFrameHostManager* opener_manager =
static_cast<WebContentsImpl*>(opener_contents)
->GetPrimaryFrameTree()
.root()
->render_manager();
// 1) Open two more windows, one named. These initially have openers but no
// reference to each other. We will later post a message between them.
// First, a named target=foo window.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(opener_contents, "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't, then
// send it to post_message.html on a different site.
WebContents* foo_contents = new_shell->web_contents();
EXPECT_TRUE(WaitForLoadStop(foo_contents));
EXPECT_EQ("/navigate_opener.html",
foo_contents->GetLastCommittedURL().path());
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(
new_shell,
embedded_test_server()->GetURL("foo.com", "/post_message.html")));
scoped_refptr<SiteInstance> foo_site_instance(
foo_contents->GetSiteInstance());
EXPECT_NE(orig_site_instance, foo_site_instance);
EXPECT_NE(static_cast<SiteInstanceImpl*>(orig_site_instance.get())->group(),
static_cast<SiteInstanceImpl*>(foo_site_instance.get())->group());
// Second, a target=_blank window.
ShellAddedObserver new_shell_observer2;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetBlankLink();"));
// Wait for the navigation in the new window to finish, if it hasn't, then
// send it to post_message.html on the original site.
Shell* new_shell2 = new_shell_observer2.GetShell();
WebContents* new_contents = new_shell2->web_contents();
EXPECT_TRUE(WaitForLoadStop(new_contents));
EXPECT_EQ("/title2.html", new_contents->GetLastCommittedURL().path());
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(
new_shell2, embedded_test_server()->GetURL("/post_message.html")));
EXPECT_EQ(orig_site_instance.get(), new_contents->GetSiteInstance());
RenderFrameHostManager* new_manager =
static_cast<WebContentsImpl*>(new_contents)
->GetPrimaryFrameTree()
.root()
->render_manager();
// We now have three windows. The opener should have a RenderFrameProxyHost
// for the new SiteInstanceGroup, but the _blank window should not.
EXPECT_EQ(3u, Shell::windows().size());
EXPECT_TRUE(opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(foo_site_instance.get())
->group()));
EXPECT_FALSE(new_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(foo_site_instance.get())
->group()));
// 2) Fail to post a message from the foo window to the opener if the target
// origin is wrong. We won't see an error, but we can check for the right
// number of received messages below.
EXPECT_EQ(true,
EvalJs(foo_contents, "postToOpener('msg', 'http://google.com');"));
ASSERT_FALSE(opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(orig_site_instance.get())
->group()));
// 3) Post a message from the foo window to the opener. The opener will
// reply, causing the foo window to update its own title.
std::u16string expected_title = u"msg";
TitleWatcher title_watcher(foo_contents, expected_title);
EXPECT_EQ(true, EvalJs(foo_contents, "postToOpener('msg','*');"));
ASSERT_FALSE(opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(orig_site_instance.get())
->group()));
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// We should have received only 1 message in the opener and "foo" tabs,
// and updated the title.
EXPECT_EQ(1, EvalJs(opener_contents, "window.receivedMessages;"));
EXPECT_EQ(1, EvalJs(foo_contents, "window.receivedMessages;"));
EXPECT_EQ(u"msg", foo_contents->GetTitle());
// 4) Now post a message from the _blank window to the foo window. The
// foo window will update its title and will not reply.
expected_title = u"msg2";
TitleWatcher title_watcher2(foo_contents, expected_title);
EXPECT_EQ(true, EvalJs(new_contents, "postToFoo('msg2');"));
ASSERT_EQ(expected_title, title_watcher2.WaitAndGetTitle());
// This postMessage should have created a RenderFrameProxyHost for the new
// SiteInstanceGroup in the target=_blank window.
EXPECT_TRUE(new_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(foo_site_instance.get())
->group()));
// TODO(nasko): Test subframe targeting of postMessage once
// http://crbug.com/153701 is fixed.
}
// Test for crbug.com/278336. MessagePorts should work cross-process. Messages
// which contain Transferables that need to be forwarded between processes via
// RenderFrameProxy::willCheckAndDispatchMessageEvent should work.
// Specifically:
// 1) Create 2 windows (opener and "foo") and send "foo" cross-process.
// 2) Post a message containing a message port from opener to "foo".
// 3) Post a message from "foo" back to opener via the passed message port.
// The test will be enabled when the feature implementation lands.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SupportCrossProcessPostMessageWithMessagePort) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed it is placed in a different
// process.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance and RVHM for later comparison.
WebContents* opener_contents = shell()->web_contents();
scoped_refptr<SiteInstance> orig_site_instance(
opener_contents->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
RenderFrameHostManager* opener_manager =
static_cast<WebContentsImpl*>(opener_contents)
->GetPrimaryFrameTree()
.root()
->render_manager();
// 1) Open a named target=foo window. We will later post a message between the
// opener and the new window.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(opener_contents, "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't, then
// send it to post_message.html on a different site.
WebContents* foo_contents = new_shell->web_contents();
EXPECT_TRUE(WaitForLoadStop(foo_contents));
EXPECT_EQ("/navigate_opener.html",
foo_contents->GetLastCommittedURL().path());
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(
new_shell,
embedded_test_server()->GetURL("foo.com", "/post_message.html")));
scoped_refptr<SiteInstance> foo_site_instance(
foo_contents->GetSiteInstance());
EXPECT_NE(orig_site_instance, foo_site_instance);
// We now have two windows. The opener should have a RenderFrameProxyHost
// for the new SiteInstanceGroup.
EXPECT_EQ(2u, Shell::windows().size());
EXPECT_TRUE(opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(foo_site_instance.get())
->group()));
// 2) Post a message containing a MessagePort from opener to the the foo
// window. The foo window will reply via the passed port, causing the opener
// to update its own title.
std::u16string expected_title = u"msg-back-via-port";
TitleWatcher title_observer(opener_contents, expected_title);
EXPECT_EQ(true, EvalJs(opener_contents, "postWithPortToFoo();"));
ASSERT_FALSE(opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(orig_site_instance.get())
->group()));
ASSERT_EQ(expected_title, title_observer.WaitAndGetTitle());
// Check message counts.
EXPECT_EQ(1, EvalJs(opener_contents, "window.receivedMessagesViaPort;"));
EXPECT_EQ(1, EvalJs(foo_contents, "window.receivedMessages;"));
EXPECT_EQ(1, EvalJs(foo_contents, "window.receivedMessagesWithPort;"));
EXPECT_EQ(u"msg-with-port", foo_contents->GetTitle());
EXPECT_EQ(u"msg-back-via-port", opener_contents->GetTitle());
}
// Test for crbug.com/116192. Navigations to a window's opener should
// still work after a process swap.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
AllowTargetedNavigationsInOpenerAfterSwap) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed to get a non-default
// SiteInstance for navigations to this origin.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original tab and SiteInstance for later comparison.
WebContents* orig_contents = shell()->web_contents();
scoped_refptr<SiteInstance> orig_site_instance(
orig_contents->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(orig_contents, "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
scoped_refptr<SiteInstance> blank_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, blank_site_instance);
// Now navigate the original (opener) tab to a different site.
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(
shell(), embedded_test_server()->GetURL("foo.com", "/title1.html")));
scoped_refptr<SiteInstance> new_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
// The opened tab should be able to navigate the opener back to its process.
TestNavigationObserver navigation_observer(orig_contents);
EXPECT_EQ(true, EvalJs(new_shell, "navigateOpener();"));
navigation_observer.Wait();
// Should have swapped back into this process.
scoped_refptr<SiteInstance> revisit_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, revisit_site_instance);
}
// Test that subframes do not crash when sending a postMessage to the top frame
// from an unload handler while the top frame is being replaced as part of
// navigating cross-process. https://crbug.com/475651.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
PostMessageFromSubframeUnloadHandler) {
StartEmbeddedServer();
GURL frame_url(embedded_test_server()->GetURL("/post_message.html"));
GURL main_url("data:text/html,<iframe name='foo' src='" + frame_url.spec() +
"'></iframe>");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_NE(nullptr, orig_site_instance.get());
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
ASSERT_EQ(1U, root->child_count());
EXPECT_EQ(frame_url, root->child_at(0)->current_url());
// Register an unload handler that sends a postMessage to the top frame.
EXPECT_TRUE(ExecJs(root->child_at(0), "registerUnload();"));
// Navigate the top frame cross-site. This will cause the top frame to be
// unloaded, and the original renderer process should then terminate since
// it's not rendering any other frames.
RenderProcessHostWatcher exit_observer(
root->current_frame_host()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_HOST_DESTRUCTION);
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("foo.com", "/title1.html")));
scoped_refptr<SiteInstance> new_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
// Ensure that the original renderer process exited cleanly without crashing.
exit_observer.Wait();
EXPECT_TRUE(exit_observer.did_exit_normally());
}
// Test that opening a new window in the same SiteInstance and then navigating
// both windows to a different SiteInstance allows the first process to exit.
// See http://crbug.com/126333.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ProcessExitWithSwappedOutViews) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed to get a non-default
// SiteInstance for navigations to this origin.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Test clicking a target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
scoped_refptr<SiteInstance> opened_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, opened_site_instance);
// Now navigate the opened window to a different site.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
// The original process should still be alive, since it is still used in the
// first window.
RenderProcessHost* orig_process = orig_site_instance->GetProcess();
EXPECT_TRUE(orig_process->IsInitializedAndNotDead());
// Navigate the first window to a different site as well. The original
// process should exit, since all of its active frames are gone.
RenderProcessHostWatcher exit_observer(
orig_process, RenderProcessHostWatcher::WATCH_FOR_HOST_DESTRUCTION);
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(shell(), cross_site_url));
exit_observer.Wait();
scoped_refptr<SiteInstance> new_site_instance2(
shell()->web_contents()->GetSiteInstance());
EXPECT_EQ(new_site_instance, new_site_instance2);
}
// Test for crbug.com/76666. A cross-site navigation that fails with a 204
// error should not make us ignore future renderer-initiated navigations.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, ClickLinkAfter204Error) {
StartServer();
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance.get() != nullptr);
// Load a cross-site page that fails with a 204 error.
EXPECT_TRUE(
NavigateToURLAndExpectNoCommit(shell(), GetCrossSiteURL("/nocontent")));
// We should still be looking at the normal page. Because we started from a
// blank new tab, the typed URL will still be visible until the user clears it
// manually. The last committed URL will be the previous page.
scoped_refptr<SiteInstance> post_nav_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, post_nav_site_instance);
EXPECT_EQ("/nocontent", shell()->web_contents()->GetVisibleURL().path());
NavigationEntry* current_entry =
shell()->web_contents()->GetController().GetLastCommittedEntry();
EXPECT_TRUE(!current_entry || current_entry->IsInitialEntry());
// Renderer-initiated navigations should work.
std::u16string expected_title = u"Title Of Awesomeness";
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
GURL url = embedded_test_server()->GetURL("/title2.html");
EXPECT_TRUE(ExecJs(
shell(), base::StringPrintf("location.href = '%s'", url.spec().c_str())));
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Opens in same tab.
EXPECT_EQ(1u, Shell::windows().size());
EXPECT_EQ("/title2.html",
shell()->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
scoped_refptr<SiteInstance> new_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_EQ(orig_site_instance, new_site_instance);
}
// A collection of tests to prevent URL spoofs when showing pending URLs above
// initial empty documents, ensuring that the URL reverts to about:blank if the
// document is accessed. See https://crbug.com/9682.
class RenderFrameHostManagerSpoofingTest : public RenderFrameHostManagerTest {
public:
void SetUpInProcessBrowserTestFixture() override {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
base::CommandLine new_command_line(command_line->GetProgram());
base::CommandLine::SwitchMap switches = command_line->GetSwitches();
// Injecting the DOM automation controller causes false positives, since it
// triggers the DidAccessInitialDocument() callback by mutating the global
// object.
switches.erase(switches::kDomAutomationController);
for (const auto& it : switches) {
new_command_line.AppendSwitchNative(it.first, it.second);
}
*command_line = new_command_line;
}
protected:
// Custom ExecuteScript() helper that doesn't depend on DOM automation
// controller. This is used to guarantee the script has completed execution,
// but the spoofing tests synchronize execution using window title changes.
void ExecuteScript(const ToRenderFrameHost& adapter, const char* script) {
adapter.render_frame_host()->ExecuteJavaScriptForTests(
base::UTF8ToUTF16(script), base::NullCallback(),
ISOLATED_WORLD_ID_GLOBAL);
}
};
// Helper to wait until a WebContent's NavigationController has a visible entry
// that is not the initial NavigationEntry.
class VisibleEntryWaiter : public WebContentsObserver {
public:
explicit VisibleEntryWaiter(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
void Wait() {
if (auto* entry = web_contents()->GetController().GetVisibleEntry()) {
if (entry && !entry->IsInitialEntry()) {
return;
}
}
run_loop_.Run();
}
// WebContentsObserver overrides:
void DidStartNavigation(NavigationHandle* navigation_handle) override {
run_loop_.Quit();
}
private:
base::RunLoop run_loop_;
};
// Sanity test that a newly opened window shows the pending URL if the initial
// empty document is not modified. This is intentionally structured as similarly
// as possible to the subsequent ShowLoadingURLUntil*Spoof tests: it performs
// the same operations as the subsequent tests except DOM modification. This
// should help catch instances where the subsequent tests incorrectly pass due
// to a side effect of the test infrastructure.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerSpoofingTest,
ShowLoadingURLIfNotModified) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load a page that can open a URL that won't commit in a new window.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/click-nocontent-link.html")));
WebContents* orig_contents = shell()->web_contents();
// Click a /nocontent link that opens in a new window but never commits.
ShellAddedObserver new_shell_observer;
ExecuteScript(orig_contents, "clickNoContentTargetedLink();");
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
WebContents* contents = new_shell->web_contents();
// Make sure the new window has started the provisional load, so the
// associated navigation controller will have a visible entry.
{
VisibleEntryWaiter waiter(contents);
waiter.Wait();
}
// Ensure the destination URL is visible, because it is considered the
// initial navigation.
EXPECT_TRUE(contents->GetController().IsInitialNavigation());
EXPECT_EQ("/nocontent",
contents->GetController().GetVisibleEntry()->GetURL().path());
// Now get another reference to the window object, but don't otherwise access
// it. This is to ensure that DidAccessInitialDocument() notifications are not
// incorrectly generated when nothing is modified.
std::u16string expected_title = u"Modified Title";
TitleWatcher title_watcher(orig_contents, expected_title);
ExecuteScript(orig_contents, "getNewWindowReference();");
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// The destination URL should still be visible, since nothing was modified.
EXPECT_TRUE(contents->GetController().IsInitialNavigation());
EXPECT_EQ("/nocontent",
contents->GetController().GetVisibleEntry()->GetURL().path());
}
// Test for crbug.com/9682. We should show the URL for a pending renderer-
// initiated navigation in a new tab, until the content of the initial
// about:blank page is modified by another window. At that point, we should
// revert to showing about:blank to prevent a URL spoof.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerSpoofingTest,
ShowLoadingURLUntilSpoof) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load a page that can open a URL that won't commit in a new window.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/click-nocontent-link.html")));
WebContents* orig_contents = shell()->web_contents();
// Change the link to be cross-site.
GURL target_url = embedded_test_server()->GetURL("foo.com", "/nocontent");
ExecuteScript(
orig_contents,
base::StringPrintf(
"document.getElementById('nocontent_targeted_link').href = '%s';",
target_url.spec().c_str())
.c_str());
// Click a /nocontent link that opens in a new window but never commits.
ShellAddedObserver new_shell_observer;
ExecuteScript(orig_contents, "clickNoContentTargetedLink();");
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
WebContents* contents = new_shell->web_contents();
// Make sure the new window has started the navigation, so the associated
// navigation controller will have a visible entry.
{
VisibleEntryWaiter waiter(contents);
waiter.Wait();
}
// Ensure the destination URL is visible, because it is considered the
// initial navigation.
EXPECT_TRUE(contents->GetController().IsInitialNavigation());
EXPECT_EQ(target_url, contents->GetController().GetVisibleEntry()->GetURL());
// Now modify the contents of the new window from the opener. This will also
// modify the title of the document to give us something to listen for.
std::u16string expected_title = u"Modified Title";
TitleWatcher title_watcher(orig_contents, expected_title);
ExecuteScript(orig_contents, "modifyNewWindow();");
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// At this point, we should no longer be showing the destination URL.
// The visible entry should be the initial entry (or null), resulting in
// about:blank in the address bar.
NavigationEntry* visible_entry =
new_shell->web_contents()->GetController().GetLastCommittedEntry();
EXPECT_TRUE(!visible_entry || visible_entry->IsInitialEntry());
}
// Same as ShowLoadingURLUntilSpoof, but reloads the new popup before modifying
// it, to test https://crbug.com/847718. The reload should not cause the
// visible entry to stick around after the modification, even though it is
// triggered in the browser process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerSpoofingTest,
ShowLoadingURLUntilSpoofAfterReload) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load a page that can open a URL that won't commit in a new window.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/click-nocontent-link.html")));
WebContents* orig_contents = shell()->web_contents();
// Change the link to be cross-site.
GURL target_url = embedded_test_server()->GetURL("foo.com", "/nocontent");
ExecuteScript(
orig_contents,
base::StringPrintf(
"document.getElementById('nocontent_targeted_link').href = '%s';",
target_url.spec().c_str())
.c_str());
// Click a /nocontent link that opens in a new window but never commits.
ShellAddedObserver new_shell_observer;
ExecuteScript(orig_contents, "clickNoContentTargetedLink();");
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
WebContents* contents = new_shell->web_contents();
// Make sure the new window has started the navigation, so the associated
// navigation controller will have a visible entry.
{
VisibleEntryWaiter waiter(contents);
waiter.Wait();
}
// Ensure the destination URL is visible, because it is considered the
// initial navigation.
EXPECT_TRUE(contents->GetController().IsInitialNavigation());
EXPECT_EQ(target_url, contents->GetController().GetVisibleEntry()->GetURL());
// Reload the popup before modifying it. See https://crbug.com/847718.
contents->GetController().Reload(ReloadType::NORMAL, false);
// Now modify the contents of the new window from the opener. This will also
// modify the title of the document to give us something to listen for.
std::u16string expected_title = u"Modified Title";
TitleWatcher title_watcher(orig_contents, expected_title);
ExecuteScript(orig_contents, "modifyNewWindow();");
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// At this point, we should no longer be showing the destination URL.
// The visible entry should be the initial entry (or null), resulting in
// about:blank in the address bar.
NavigationEntry* visible_entry =
new_shell->web_contents()->GetController().GetLastCommittedEntry();
EXPECT_TRUE(!visible_entry || visible_entry->IsInitialEntry());
}
// Similar but using document.open(): once a Document is opened, subsequent
// document.write() calls can insert arbitrary content into the target Document.
// Since this could result in URL spoofing, the pending URL should no longer be
// shown in the omnibox.
//
// Note: document.write() implicitly invokes document.open() if the Document has
// not already been opened, so there's no need to test document.write()
// separately.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerSpoofingTest,
ShowLoadingURLUntilDocumentOpenSpoof) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load a page that can open a URL that won't commit in a new window.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/click-nocontent-link.html")));
WebContents* orig_contents = shell()->web_contents();
// Click a /nocontent link that opens in a new window but never commits.
ShellAddedObserver new_shell_observer;
ExecuteScript(orig_contents, "clickNoContentTargetedLink();");
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
WebContents* contents = new_shell->web_contents();
// Make sure the new window has started the provisional load, so the
// associated navigation controller will have a visible entry.
{
VisibleEntryWaiter waiter(contents);
waiter.Wait();
}
// Ensure the destination URL is visible, because it is considered the
// initial navigation.
EXPECT_TRUE(contents->GetController().IsInitialNavigation());
EXPECT_EQ("/nocontent",
contents->GetController().GetVisibleEntry()->GetURL().path());
// Now modify the contents of the new window from the opener. This will also
// modify the title of the document to give us something to listen for.
std::u16string expected_title = u"Modified Title";
TitleWatcher title_watcher(orig_contents, expected_title);
ExecuteScript(orig_contents, "modifyNewWindowWithDocumentOpen();");
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// At this point, we should no longer be showing the destination URL.
// The visible entry should be the initial entry (or null), resulting in
// about:blank in the address bar.
NavigationEntry* visible_entry =
new_shell->web_contents()->GetController().GetLastCommittedEntry();
EXPECT_TRUE(!visible_entry || visible_entry->IsInitialEntry());
}
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
WasDiscardedWhenNavigationInterruptsReload) {
EXPECT_TRUE(embedded_test_server()->Start());
GURL discarded_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), discarded_url));
// Discard the page.
shell()->web_contents()->SetWasDiscarded(true);
// Reload the discarded page, but pretend that it's slow to commit.
TestNavigationManager first_reload(shell()->web_contents(), discarded_url);
shell()->web_contents()->GetController().LoadOriginalRequestURL();
EXPECT_TRUE(first_reload.WaitForRequestStart());
// Before the response is received, simulate user navigating to another URL.
GURL second_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
TestNavigationManager second_navigation(shell()->web_contents(), second_url);
shell()->LoadURL(second_url);
ASSERT_TRUE(second_navigation.WaitForNavigationFinished());
const char kDiscardedStateJS[] = "window.document.wasDiscarded;";
EXPECT_EQ(false, content::EvalJs(shell(), kDiscardedStateJS));
}
// Ensures that a pending navigation's URL is no longer visible after the
// speculative RFH is discarded due to a concurrent renderer-initiated
// navigation. See https://crbug.com/760342.
// TODO(crbug.com/41448629): Disabled due to flaky timeouts.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
DISABLED_ResetVisibleURLOnCrossProcessNavigationInterrupted) {
const std::string kVictimPath = "/victim.html";
const std::string kAttackPath = "/attack.html";
net::test_server::ControllableHttpResponse victim_response(
embedded_test_server(), kVictimPath);
net::test_server::ControllableHttpResponse attack_response(
embedded_test_server(), kAttackPath);
EXPECT_TRUE(embedded_test_server()->Start());
const GURL kVictimURL = embedded_test_server()->GetURL("a.com", kVictimPath);
const GURL kAttackURL = embedded_test_server()->GetURL("b.com", kAttackPath);
// First navigate to the attacker page. This page will be cross-site compared
// to the next navigations we will attempt.
const GURL kAttackInitialURL =
embedded_test_server()->GetURL("b.com", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), kAttackInitialURL));
EXPECT_EQ(kAttackInitialURL, shell()->web_contents()->GetVisibleURL());
// Now, start a browser-initiated cross-site navigation to a new page that
// will hang at ready to commit stage.
TestNavigationManager victim_navigation(shell()->web_contents(), kVictimURL);
shell()->LoadURL(kVictimURL);
EXPECT_TRUE(victim_navigation.WaitForRequestStart());
victim_navigation.ResumeNavigation();
victim_response.WaitForRequest();
victim_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
EXPECT_TRUE(victim_navigation.WaitForResponse());
victim_navigation.ResumeNavigation();
// The navigation is ready to commit: it has been handed to the speculative
// RenderFrameHost for commit.
RenderFrameHostImpl* speculative_rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
CHECK(speculative_rfh);
EXPECT_TRUE(speculative_rfh->is_loading());
// Since we have a browser-initiated pending navigation, the navigation URL is
// showing in the address bar.
EXPECT_EQ(kVictimURL, shell()->web_contents()->GetVisibleURL());
// The attacker page requests a navigation to a new document while the
// browser-initiated navigation hasn't committed yet.
TestNavigationManager attack_navigation(shell()->web_contents(), kAttackURL);
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"location.href = \"" + kAttackURL.spec() + "\";",
EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_TRUE(attack_navigation.WaitForRequestStart());
// This deletes the speculative RenderFrameHost that was supposed to commit
// the browser-initiated navigation.
speculative_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
EXPECT_FALSE(speculative_rfh);
// The URL of the browser-initiated navigation should no longer be shown in
// the address bar since the RenderFrameHost that was supposed to commit it
// has been discarded. Instead, we should be showing the URL of the current
// page as we do not show the URL of pending navigations when they are
// renderer-initiated.
EXPECT_NE(kVictimURL, shell()->web_contents()->GetVisibleURL());
EXPECT_EQ(kAttackInitialURL, shell()->web_contents()->GetVisibleURL());
// The attacker navigation results in a 204.
attack_navigation.ResumeNavigation();
attack_response.WaitForRequest();
attack_response.Send(
"HTTP/1.1 204 OK\r\n"
"Connection: close\r\n"
"\r\n");
ASSERT_TRUE(attack_navigation.WaitForNavigationFinished());
speculative_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
EXPECT_FALSE(speculative_rfh);
// We are still showing the URL of the current page.
EXPECT_EQ(kAttackInitialURL, shell()->web_contents()->GetVisibleURL());
}
// Ensures that deleting a speculative RenderFrameHost trying to commit a
// navigation to the pending NavigationEntry will not crash if it happens
// because a new navigation to the same pending NavigationEntry started. This is
// a regression test for crbug.com/796135.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted1) {
if (!AreAllSitesIsolatedForTesting()) {
GTEST_SKIP() << "This test requires speculative RenderFrameHosts, so skip "
"it when site isolation is turned off";
}
const std::string kOriginalPath = "/original.html";
const std::string kFirstRedirectPath = "/redirect1.html";
const std::string kSecondRedirectPath = "/reidrect2.html";
net::test_server::ControllableHttpResponse original_response1(
embedded_test_server(), kOriginalPath);
net::test_server::ControllableHttpResponse original_response2(
embedded_test_server(), kOriginalPath);
net::test_server::ControllableHttpResponse original_response3(
embedded_test_server(), kOriginalPath);
net::test_server::ControllableHttpResponse first_redirect_response(
embedded_test_server(), kFirstRedirectPath);
net::test_server::ControllableHttpResponse second_redirect_response(
embedded_test_server(), kSecondRedirectPath);
EXPECT_TRUE(embedded_test_server()->Start());
const GURL kOriginalURL =
embedded_test_server()->GetURL("a.com", kOriginalPath);
const GURL kFirstRedirectURL =
embedded_test_server()->GetURL("b.com", kFirstRedirectPath);
const GURL kSecondRedirectURL =
embedded_test_server()->GetURL("c.com", kSecondRedirectPath);
// First navigate to the initial URL. This page will have a cross-site
// redirect.
shell()->LoadURL(kOriginalURL);
original_response1.WaitForRequest();
original_response1.Send(
"HTTP/1.1 302 FOUND\r\n"
"Location: " +
kFirstRedirectURL.spec() +
"\r\n"
"\r\n");
original_response1.Done();
first_redirect_response.WaitForRequest();
first_redirect_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
first_redirect_response.Send(
"<html>"
"<body></body>"
"</html>");
first_redirect_response.Done();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(kFirstRedirectURL, shell()->web_contents()->GetLastCommittedURL());
// Now reload the original request, but redirect to yet another site.
TestNavigationManager first_reload(shell()->web_contents(), kOriginalURL);
shell()->web_contents()->GetController().LoadOriginalRequestURL();
EXPECT_TRUE(first_reload.WaitForRequestStart());
first_reload.ResumeNavigation();
original_response2.WaitForRequest();
original_response2.Send(
"HTTP/1.1 302 FOUND\r\n"
"Location: " +
kSecondRedirectURL.spec() +
"\r\n"
"\r\n");
original_response2.Done();
EXPECT_TRUE(first_reload.WaitForRequestRedirected());
first_reload.ResumeNavigation();
second_redirect_response.WaitForRequest();
second_redirect_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
EXPECT_TRUE(first_reload.WaitForResponse());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImplWrapper first_speculative_rfh(
root->render_manager()->speculative_frame_host());
EXPECT_TRUE(first_speculative_rfh.get());
// The user requests a new reload while the previous reload hasn't committed
// yet. This second reload starts immediately after pausing the commit of the
// first reload. It might delete the speculative RenderFrameHost that was
// supposed to commit the first reload. This should not crash.
TestNavigationManager second_reload(shell()->web_contents(), kOriginalURL);
CommitNavigationPauser commit_pauser(first_speculative_rfh.get());
first_reload.ResumeNavigation();
commit_pauser.WaitForCommitAndPause();
shell()->web_contents()->GetController().LoadOriginalRequestURL();
EXPECT_TRUE(second_reload.WaitForRequestStart());
RenderFrameHostImplWrapper second_speculative_rfh(
root->render_manager()->speculative_frame_host());
EXPECT_TRUE(second_speculative_rfh.get());
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// When navigation queueing is enabled, the first speculative RFH is still
// kept around as it is pending commit.
EXPECT_TRUE(first_speculative_rfh.get());
EXPECT_EQ(first_speculative_rfh.get(), second_speculative_rfh.get());
} else {
// Otherwise, the first speculative RFH will be deleted and replaced by a
// new speculative RFH.
EXPECT_FALSE(first_speculative_rfh.get());
}
// The second reload results in a 204.
second_reload.ResumeNavigation();
original_response3.WaitForRequest();
original_response3.Send(
"HTTP/1.1 204 OK\r\n"
"Connection: close\r\n"
"\r\n");
ASSERT_TRUE(second_reload.WaitForNavigationFinished());
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// If navigation queuing is enabled, the first reload's speculative RFH
// will be kept.
EXPECT_TRUE(root->render_manager()->speculative_frame_host());
} else {
// If navigation queueing is turned off, the second reload will delete the
// first reload's speculative RFH, and we end up with no speculative RFH
// after the second reload commits.
EXPECT_FALSE(root->render_manager()->speculative_frame_host());
}
}
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) || \
BUILDFLAG(IS_MAC)
#define MAYBE_DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted2 \
DISABLED_DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted2
#else
#define MAYBE_DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted2 \
DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted2
#endif
// Ensures that deleting a speculative RenderFrameHost trying to commit a
// navigation to the pending NavigationEntry will not crash if it happens
// because a new navigation to the same pending NavigationEntry started. This
// is a variant of the previous test, where we destroy the speculative
// RenderFrameHost to create another speculative RenderFrameHost. This is a
// regression test for crbug.com/796135.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
MAYBE_DeleteSpeculativeRFHPendingCommitOfPendingEntryOnInterrupted2) {
if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
// When navigation queueing is enabled, starting a new navigation won't
// delete an existing pending commit RFH, so this test can't run as
// intended.
return;
}
const std::string kOriginalPath = "/original.html";
const std::string kRedirectPath = "/redirect.html";
net::test_server::ControllableHttpResponse original_response1(
embedded_test_server(), kOriginalPath);
net::test_server::ControllableHttpResponse original_response2(
embedded_test_server(), kOriginalPath);
net::test_server::ControllableHttpResponse redirect_response(
embedded_test_server(), kRedirectPath);
EXPECT_TRUE(embedded_test_server()->Start());
const GURL kOriginalURL =
embedded_test_server()->GetURL("a.com", kOriginalPath);
const GURL kRedirectURL =
embedded_test_server()->GetURL("b.com", kRedirectPath);
const GURL kCrossSiteURL =
embedded_test_server()->GetURL("c.com", "/title1.html");
IsolationContext isolation_context(
shell()->web_contents()->GetBrowserContext());
const auto kOriginalSiteInfo =
SiteInfo::CreateForTesting(isolation_context, kOriginalURL);
const auto kRedirectSiteInfo =
SiteInfo::CreateForTesting(isolation_context, kRedirectURL);
// First navigate to the initial URL.
shell()->LoadURL(kOriginalURL);
original_response1.WaitForRequest();
original_response1.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
"Pragma: no-cache\r\n"
"\r\n");
original_response1.Send(
"<html>"
"<body></body>"
"</html>");
original_response1.Done();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(kOriginalURL, shell()->web_contents()->GetLastCommittedURL());
// Navigate cross-site.
EXPECT_TRUE(NavigateToURL(shell(), kCrossSiteURL));
// Now go back to the original request, which will do a cross-site redirect.
TestNavigationManager first_back(shell()->web_contents(), kOriginalURL);
shell()->GoBackOrForward(-1);
EXPECT_TRUE(first_back.WaitForRequestStart());
first_back.ResumeNavigation();
original_response2.WaitForRequest();
original_response2.Send(
"HTTP/1.1 302 FOUND\r\n"
"Location: " +
kRedirectURL.spec() +
"\r\n"
"\r\n");
original_response2.Done();
redirect_response.WaitForRequest();
redirect_response.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
EXPECT_TRUE(first_back.WaitForResponse());
first_back.ResumeNavigation();
// The navigation is ready to commit: it has been handed to the speculative
// RenderFrameHost for commit.
RenderFrameHostImpl* speculative_rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
CHECK(speculative_rfh);
EXPECT_TRUE(speculative_rfh->is_loading());
if (AreAllSitesIsolatedForTesting()) {
EXPECT_EQ(kRedirectSiteInfo,
speculative_rfh->GetSiteInstance()->GetSiteInfo());
} else {
EXPECT_TRUE(speculative_rfh->GetSiteInstance()->IsDefaultSiteInstance());
}
auto site_instance_id = speculative_rfh->GetSiteInstance()->GetId();
// The user starts a navigation towards the redirected URL, for which we have
// a speculative RenderFrameHost. This shouldn't delete the speculative
// RenderFrameHost.
TestNavigationManager navigation_to_redirect(shell()->web_contents(),
kRedirectURL);
shell()->LoadURL(kRedirectURL);
EXPECT_TRUE(navigation_to_redirect.WaitForRequestStart());
speculative_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
CHECK(speculative_rfh);
if (AreAllSitesIsolatedForTesting()) {
EXPECT_EQ(kRedirectSiteInfo,
speculative_rfh->GetSiteInstance()->GetSiteInfo());
EXPECT_EQ(site_instance_id, speculative_rfh->GetSiteInstance()->GetId());
} else {
EXPECT_TRUE(speculative_rfh->GetSiteInstance()->IsDefaultSiteInstance());
}
// The user requests to go back again while the previous back hasn't committed
// yet. This should delete the speculative RenderFrameHost trying to commit
// the back, and re-create a new speculative RenderFrameHost. This shouldn't
// crash.
TestNavigationManager second_back(shell()->web_contents(), kOriginalURL);
shell()->GoBackOrForward(-1);
EXPECT_TRUE(second_back.WaitForRequestStart());
speculative_rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
CHECK(speculative_rfh);
if (AreAllSitesIsolatedForTesting()) {
EXPECT_EQ(kOriginalSiteInfo,
speculative_rfh->GetSiteInstance()->GetSiteInfo());
EXPECT_NE(site_instance_id, speculative_rfh->GetSiteInstance()->GetId());
} else {
EXPECT_TRUE(speculative_rfh->GetSiteInstance()->IsDefaultSiteInstance());
}
}
// Test for crbug.com/9682. We should not show the URL for a pending renderer-
// initiated navigation in a new tab if it is not the initial navigation. In
// this case, the renderer will not notify us of a modification, so we cannot
// show the pending URL without allowing a spoof.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
DontShowLoadingURLIfNotInitialNav) {
ASSERT_TRUE(embedded_test_server()->Start());
// Load a page that can open a URL that won't commit in a new window.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/click-nocontent-link.html")));
WebContents* orig_contents = shell()->web_contents();
// Click a /nocontent link that opens in a new window but never commits.
// By using an onclick handler that first creates the window, the slow
// navigation is not considered an initial navigation.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true,
EvalJs(orig_contents, "clickNoContentScriptedTargetedLink();"));
// Wait for the window to open.
Shell* new_shell = new_shell_observer.GetShell();
// Ensure the destination URL is not visible, because it is not the initial
// navigation.
WebContents* contents = new_shell->web_contents();
EXPECT_FALSE(contents->GetController().IsInitialNavigation());
// The visible entry should be the entry for the synchronously committed
// about:blank, resulting in about:blank in the address bar.
EXPECT_EQ(GURL(url::kAboutBlankURL),
contents->GetController().GetVisibleEntry()->GetURL());
}
// Crashes under ThreadSanitizer, http://crbug.com/356758.
#if defined(THREAD_SANITIZER)
#define MAYBE_BackForwardNotStale DISABLED_BackForwardNotStale
#else
#define MAYBE_BackForwardNotStale BackForwardNotStale
#endif
// Test for http://crbug.com/93427. Ensure that cross-site navigations
// do not cause back/forward navigations to be considered stale by the
// renderer.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, MAYBE_BackForwardNotStale) {
StartEmbeddedServer();
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
// Visit a page on first site.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
// Visit three pages on second site.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("foo.com", "/title1.html")));
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("foo.com", "/title2.html")));
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("foo.com", "/title3.html")));
// History is now [blank, A1, B1, B2, *B3].
WebContents* contents = shell()->web_contents();
EXPECT_EQ(5, contents->GetController().GetEntryCount());
// Open another window in same process to keep this process alive.
Shell* new_shell = CreateBrowser();
EXPECT_TRUE(NavigateToURL(
new_shell, embedded_test_server()->GetURL("foo.com", "/title1.html")));
// Go back three times to first site.
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
// Now go forward twice to B2. Shouldn't be left spinning.
{
TestNavigationObserver forward_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoForward();
forward_nav_load_observer.Wait();
}
{
TestNavigationObserver forward_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoForward();
forward_nav_load_observer.Wait();
}
// Go back twice to first site.
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
// Now go forward directly to B3. Shouldn't be left spinning.
{
TestNavigationObserver forward_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoToIndex(4);
forward_nav_load_observer.Wait();
}
}
// This class ensures that all the given RenderViewHosts have properly been
// shutdown.
class RenderViewHostDestructionObserver : public WebContentsObserver {
public:
explicit RenderViewHostDestructionObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
~RenderViewHostDestructionObserver() override = default;
void EnsureRVHGetsDestructed(RenderViewHost* rvh) {
watched_render_view_hosts_.insert(rvh);
}
size_t GetNumberOfWatchedRenderViewHosts() const {
return watched_render_view_hosts_.size();
}
private:
// WebContentsObserver implementation:
void RenderViewDeleted(RenderViewHost* rvh) override {
watched_render_view_hosts_.erase(rvh);
}
std::set<raw_ptr<RenderViewHost, SetExperimental>> watched_render_view_hosts_;
};
// Crashes under ThreadSanitizer, http://crbug.com/356758.
#if defined(THREAD_SANITIZER)
#define MAYBE_LeakingRenderViewHosts DISABLED_LeakingRenderViewHosts
#else
#define MAYBE_LeakingRenderViewHosts LeakingRenderViewHosts
#endif
// Test for crbug.com/90867. Make sure we don't leak render view hosts since
// they may cause crashes or memory corruptions when trying to call dead
// delegate_. This test also verifies crbug.com/117420 and crbug.com/143255 to
// ensure that a separate SiteInstance is created when navigating to view-source
// URLs, regardless of current URL.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
MAYBE_LeakingRenderViewHosts) {
StartEmbeddedServer();
// Observe the created render_view_host's to make sure they will not leak.
RenderViewHostDestructionObserver rvh_observers(shell()->web_contents());
GURL navigated_url(embedded_test_server()->GetURL("/title2.html"));
GURL view_source_url(kViewSourceScheme + std::string(":") +
navigated_url.spec());
// Let's ensure that when we start with a blank window, navigating away to a
// view-source URL, we create a new SiteInstance.
RenderViewHost* blank_rvh =
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost();
SiteInstance* blank_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), GURL());
EXPECT_EQ(blank_site_instance->GetSiteURL(), GURL());
rvh_observers.EnsureRVHGetsDestructed(blank_rvh);
// Now navigate to the view-source URL and ensure we got a different
// SiteInstance and RenderViewHost.
EXPECT_TRUE(NavigateToURL(shell(), view_source_url));
EXPECT_NE(
blank_rvh,
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost());
EXPECT_NE(blank_site_instance,
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
rvh_observers.EnsureRVHGetsDestructed(
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost());
// Load a random page and then navigate to view-source: of it.
// This used to cause two RVH instances for the same SiteInstance, which
// was a problem. This is no longer the case.
EXPECT_TRUE(NavigateToURL(shell(), navigated_url));
SiteInstance* site_instance1 =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
rvh_observers.EnsureRVHGetsDestructed(
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost());
EXPECT_TRUE(NavigateToURL(shell(), view_source_url));
rvh_observers.EnsureRVHGetsDestructed(
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost());
SiteInstance* site_instance2 =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
// Ensure that view-source navigations force a new SiteInstance.
EXPECT_NE(site_instance1, site_instance2);
// Now navigate to a different instance so that we swap out again.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("foo.com", "/title2.html")));
rvh_observers.EnsureRVHGetsDestructed(
shell()->web_contents()->GetPrimaryMainFrame()->GetRenderViewHost());
// This used to leak a render view host.
shell()->Close();
RunAllPendingInMessageLoop(); // Needed on ChromeOS.
EXPECT_EQ(0U, rvh_observers.GetNumberOfWatchedRenderViewHosts());
}
// Test for crbug.com/143155. Frame tree updates during unload should not
// interrupt the intended navigation.
// Specifically:
// 1) Open 2 tabs in an HTTP SiteInstance, with a subframe in the opener.
// 2) Send the second tab to a different foo.com SiteInstance.
// This created an opener proxy for the first tab in the foo process.
// 3) Navigate the first tab to the foo.com SiteInstance, and have the first
// tab's unload handler remove its frame.
// In older versions of Chrome, this caused an update to the frame tree that
// resulted in showing an internal page rather than the real page.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
DontPreemptNavigationWithFrameTreeUpdate) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed to get a non-default
// SiteInstance for navigations to this origin.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
// 1. Load a page that deletes its iframe during unload.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/remove_frame_on_unload.html")));
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
// Open a same-site page in a new window.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "openWindow();"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new window to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/title1.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Should have the same SiteInstance.
EXPECT_EQ(orig_site_instance.get(),
new_shell->web_contents()->GetSiteInstance());
// 2. Send the second tab to a different process.
GURL cross_site_url(
embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
scoped_refptr<SiteInstance> new_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(orig_site_instance, new_site_instance);
// 3. Send the first tab to the second tab's process.
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(shell(), cross_site_url));
// Make sure it ends up at the right page.
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(cross_site_url, shell()->web_contents()->GetLastCommittedURL());
EXPECT_EQ(new_site_instance, shell()->web_contents()->GetSiteInstance());
}
// Ensure that renderer-side debug URLs do not cause a process swap, since they
// are meant to run in the current page. We had a bug where we expected a
// BrowsingInstance swap to occur on pages like view-source and extensions,
// which broke chrome://crash and javascript: URLs.
// See http://crbug.com/335503.
// The test fails on Mac OSX with ASAN.
// See http://crbug.com/699062.
#if BUILDFLAG(IS_MAC) && defined(THREAD_SANITIZER)
#define MAYBE_RendererDebugURLsDontSwap DISABLED_RendererDebugURLsDontSwap
#else
#define MAYBE_RendererDebugURLsDontSwap RendererDebugURLsDontSwap
#endif
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
MAYBE_RendererDebugURLsDontSwap) {
StartEmbeddedServer();
GURL original_url(embedded_test_server()->GetURL("/title2.html"));
GURL view_source_url(kViewSourceScheme + std::string(":") +
original_url.spec());
EXPECT_TRUE(NavigateToURL(shell(), view_source_url));
// Check that javascript: URLs work.
std::u16string expected_title = u"msg";
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
shell()->LoadURL(GURL("javascript:document.title='msg'"));
ASSERT_EQ(expected_title, title_watcher.WaitAndGetTitle());
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
// Crash the renderer of the view-source page.
RenderProcessHostWatcher crash_observer(
shell()->web_contents(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
EXPECT_TRUE(
NavigateToURLAndExpectNoCommit(shell(), GURL(blink::kChromeUICrashURL)));
crash_observer.Wait();
// We should not change SiteInstance and BrowsingInstance on navigations to
// RendererDebug URLs.
auto* new_site_instance = shell()->web_contents()->GetSiteInstance();
EXPECT_EQ(orig_site_instance, new_site_instance);
EXPECT_TRUE(orig_site_instance->IsRelatedSiteInstance(new_site_instance));
}
// Ensure that renderer-side debug URLs don't take effect on crashed renderers.
// Otherwise, we might try to load an unprivileged about:blank page into a
// WebUI-enabled RenderProcessHost, failing a safety check in InitRenderView.
// See http://crbug.com/334214.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
IgnoreRendererDebugURLsWhenCrashed) {
// Visit a WebUI page with bindings.
GURL webui_url = GURL(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID()));
// Crash the renderer of the WebUI page.
RenderProcessHostWatcher crash_observer(
shell()->web_contents(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
EXPECT_TRUE(
NavigateToURLAndExpectNoCommit(shell(), GURL(blink::kChromeUICrashURL)));
crash_observer.Wait();
// Load the crash URL again but don't wait for any action. If it is not
// ignored this time, we will fail the WebUI CHECK in InitRenderView.
shell()->LoadURL(GURL(blink::kChromeUICrashURL));
// Ensure that such URLs can still work as the initial navigation of a tab.
// We postpone the initial navigation of the tab using an empty GURL, so that
// we can add a watcher for crashes.
Shell* shell2 =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
GURL(), nullptr, gfx::Size());
RenderProcessHostWatcher crash_observer2(
shell2->web_contents(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
EXPECT_TRUE(
NavigateToURLAndExpectNoCommit(shell2, GURL(blink::kChromeUIKillURL)));
crash_observer2.Wait();
}
class RFHMProcessPerTabTest : public RenderFrameHostManagerTest {
public:
RFHMProcessPerTabTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kProcessPerTab);
}
};
// Test that we still swap processes for BrowsingInstance changes even in
// --process-per-tab mode. See http://crbug.com/343017.
// Disabled on Android: http://crbug.com/345873.
// Crashes under ThreadSanitizer, http://crbug.com/356758.
#if BUILDFLAG(IS_ANDROID) || defined(THREAD_SANITIZER)
#define MAYBE_BackFromWebUI DISABLED_BackFromWebUI
#else
#define MAYBE_BackFromWebUI BackFromWebUI
#endif
IN_PROC_BROWSER_TEST_P(RFHMProcessPerTabTest, MAYBE_BackFromWebUI) {
StartEmbeddedServer();
GURL original_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), original_url));
// Visit a WebUI page with bindings.
GURL webui_url(GURL(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost)));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID()));
// Go back and ensure we have no WebUI bindings.
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
EXPECT_EQ(original_url, shell()->web_contents()->GetLastCommittedURL());
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID()));
}
// crbug.com/424526
// The test loads a WebUI page in process-per-tab mode, then navigates to a
// blank page and then to a regular page. The bug reproduces if blank page is
// visited in between WebUI and regular page.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ForceSwapAfterWebUIBindings) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kProcessPerTab);
StartEmbeddedServer();
const GURL web_ui_url(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
EXPECT_TRUE(NavigateToURL(shell(), web_ui_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID()));
// Capture the SiteInstance before navigating to about:blank to ensure
// it doesn't change.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_NE(orig_site_instance, shell()->web_contents()->GetSiteInstance());
GURL regular_page_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), regular_page_url));
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID()));
}
// crbug.com/615274
// This test ensures that after an RFH is unloaded, the associated WebUI
// instance is no longer allowed to send JavaScript messages. This is necessary
// because WebUI currently (and unusually) always sends JavaScript messages to
// the current main frame, rather than the RFH that owns the WebUI.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
WebUIJavascriptDisallowedAfterUnload) {
StartEmbeddedServer();
const GURL web_ui_url(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
EXPECT_TRUE(NavigateToURL(shell(), web_ui_url));
RenderFrameHostImpl* rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame();
// Set up a slow unload handler to force the RFH to linger in the unloaded
// but not-yet-deleted state.
EXPECT_TRUE(ExecJs(rfh, "window.onunload=function(e){ while(1); };\n"));
WebUIImpl* web_ui = rfh->web_ui();
EXPECT_TRUE(web_ui->CanCallJavascript());
auto handler_owner = std::make_unique<TestWebUIMessageHandler>();
TestWebUIMessageHandler* handler = handler_owner.get();
web_ui->AddMessageHandler(std::move(handler_owner));
EXPECT_FALSE(handler->IsJavascriptAllowed());
handler->AllowJavascript();
EXPECT_TRUE(handler->IsJavascriptAllowed());
rfh->DisableUnloadTimerForTesting();
RenderFrameHostDestructionObserver rfh_observer(rfh);
// Navigate, but wait for commit, not the actual load to finish.
SiteInstanceImpl* web_ui_site_instance = rfh->GetSiteInstance();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
TestFrameNavigationObserver commit_observer(root);
shell()->LoadURL(GURL(url::kAboutBlankURL));
commit_observer.WaitForCommit();
EXPECT_NE(web_ui_site_instance, shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(web_ui_site_instance->group()));
// The previous RFH should still be pending deletion, as we wait for either
// the mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame or a timeout.
ASSERT_TRUE(rfh->IsRenderFrameLive());
ASSERT_TRUE(rfh->IsPendingDeletion());
// We specifically want verify behavior between unload and RFH destruction.
ASSERT_FALSE(rfh_observer.deleted());
EXPECT_FALSE(handler->IsJavascriptAllowed());
}
// Test for http://crbug.com/703303. Ensures that the renderer process does not
// try to select files whose paths cannot be converted to WebStrings. This
// check is done in the renderer because it is hard to predict which paths will
// turn into empty WebStrings, and the behavior varies by platform.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, DontSelectInvalidFiles) {
StartServer();
base::RunLoop run_loop;
// Use a file path with an invalid encoding, such that it can't be converted
// to a WebString (on all platforms but Windows).
base::FilePath file;
EXPECT_TRUE(base::PathService::Get(base::DIR_TEMP, &file));
file = file.Append(FILE_PATH_LITERAL("foo\337bar"));
// Navigate and try to get page to reference this file in its PageState.
GURL url1(embedded_test_server()->GetURL("/file_input.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
int process_id = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID();
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file, run_loop.QuitClosure()));
shell()->web_contents()->SetDelegate(delegate.get());
EXPECT_TRUE(ExecJs(shell(), "document.getElementById('fileinput').click();"));
run_loop.Run();
// The browser process grants access to the file whether or not the renderer
// process realizes that it can't use it. This is ok, since the user actually
// did select the file from the chooser.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
process_id, file));
// Disable the unload timer so we wait for the UpdateState message.
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame()
->DisableUnloadTimerForTesting();
// Navigate to a different process and wait for the old process to exit.
RenderProcessHostWatcher exit_observer(
shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_HOST_DESTRUCTION);
// With BackForwardCache, old process won't get deleted on navigation as it is
// still in use by the bfcached document, disable back-forward cache to ensure
// that the process gets deleted.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(shell(), GetCrossSiteURL("/title1.html")));
exit_observer.Wait();
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID(),
file));
// The renderer process should not have been killed. This is the important
// part of the test. If this fails, then we didn't get a PageState to check
// below, so use an assert (since the test can't meaningfully proceed).
ASSERT_TRUE(exit_observer.did_exit_normally());
// Ensure that the file did not end up in the PageState of the previous entry,
// except on Windows where the path is valid and WebString can handle it.
NavigationEntry* prev_entry =
shell()->web_contents()->GetController().GetEntryAtIndex(0);
EXPECT_EQ(url1, prev_entry->GetURL());
const std::vector<base::FilePath>& files =
prev_entry->GetPageState().GetReferencedFiles();
#if BUILDFLAG(IS_WIN)
EXPECT_EQ(1U, files.size());
#else
EXPECT_EQ(0U, files.size());
#endif
}
// Test for http://crbug.com/262948.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RestoreFileAccessForHistoryNavigation) {
StartServer();
base::RunLoop run_loop;
base::FilePath file;
EXPECT_TRUE(base::PathService::Get(base::DIR_TEMP, &file));
file = file.AppendASCII("bar");
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to url and get it to reference a file in its PageState.
GURL url1(embedded_test_server()->GetURL("/file_input.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
int process_id = wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file, run_loop.QuitClosure()));
wc->SetDelegate(delegate.get());
EXPECT_TRUE(ExecJs(shell(), "document.getElementById('fileinput').click();"));
run_loop.Run();
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
process_id, file));
// Disable the unload timer so we wait for the UpdateState message.
wc->GetPrimaryMainFrame()->DisableUnloadTimerForTesting();
// Navigate to a different process without access to the file, and wait for
// the old process to exit.
RenderProcessHostWatcher exit_observer(
wc->GetPrimaryMainFrame()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_HOST_DESTRUCTION);
// With BackForwardCache, old process won't get deleted on navigation as it is
// still in use by the bfcached document, disable back-forward cache to ensure
// that the process gets deleted.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(shell(), GetCrossSiteURL("/title1.html")));
exit_observer.Wait();
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), file));
// Ensure that the file ended up in the PageState of the previous entry.
NavigationEntry* prev_entry = wc->GetController().GetEntryAtIndex(0);
EXPECT_EQ(url1, prev_entry->GetURL());
const std::vector<base::FilePath>& files =
prev_entry->GetPageState().GetReferencedFiles();
ASSERT_EQ(1U, files.size());
EXPECT_EQ(file, files.at(0));
// Go back, ending up in a different RenderProcessHost than before.
TestNavigationObserver back_nav_load_observer(wc);
wc->GetController().GoBack();
back_nav_load_observer.Wait();
EXPECT_NE(process_id,
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID());
// Ensure that the file access still exists in the new process ID.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), file));
// Navigate to a same site page to trigger a PageState update and ensure the
// renderer is not killed.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
}
// Same as RenderFrameHostManagerTest.RestoreFileAccessForHistoryNavigation, but
// replace the cross-origin navigation by a crash, followed by a reload.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RestoreFileAccessForHistoryNavigationAfterCrash) {
StartServer();
base::RunLoop run_loop;
base::FilePath file;
EXPECT_TRUE(base::PathService::Get(base::DIR_TEMP, &file));
file = file.AppendASCII("bar");
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to url and get it to reference a file in its PageState.
GURL url1(embedded_test_server()->GetURL("/file_input.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
int process_id = wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file, run_loop.QuitClosure()));
wc->SetDelegate(delegate.get());
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
process_id, file));
EXPECT_TRUE(ExecJs(shell(), "document.getElementById('fileinput').click();"));
run_loop.Run();
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
process_id, file));
// The PageState hasn't been updated yet. It requires a navigation.
{
NavigationEntry* prev_entry = wc->GetController().GetEntryAtIndex(0);
EXPECT_EQ(url1, prev_entry->GetURL());
const std::vector<base::FilePath>& files =
prev_entry->GetPageState().GetReferencedFiles();
ASSERT_EQ(0U, files.size());
}
// Same-document navigation
EXPECT_TRUE(ExecJs(shell(), "history.pushState({},'title','#foo')"));
// The PageState has been updated, it now contains the file.
{
NavigationEntry* prev_entry = wc->GetController().GetEntryAtIndex(0);
EXPECT_EQ(url1, prev_entry->GetURL());
const std::vector<base::FilePath>& files =
prev_entry->GetPageState().GetReferencedFiles();
ASSERT_EQ(1U, files.size());
EXPECT_EQ(file, files.at(0));
}
// Crash.
{
RenderProcessHost* process = wc->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
}
// The renderer process is still allowed to read the file, even if it is
// crashed.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), file));
// Reload
wc->GetController().Reload(ReloadType::NORMAL, false);
EXPECT_TRUE(WaitForLoadStop(wc));
// After recovering from the crash, the renderer process is allowed to read
// the file.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), file));
// Same-document history back navigation.
{
TestNavigationObserver back_nav_load_observer(wc);
wc->GetController().GoBack();
back_nav_load_observer.Wait();
}
// Ensure that the file access still exists in the new process ID.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
wc->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), file));
// Navigate to a same site page to trigger a PageState update and ensure the
// renderer is not killed.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
}
// Test for http://crbug.com/441966.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RestoreSubframeFileAccessForHistoryNavigation) {
StartServer();
base::RunLoop run_loop;
base::FilePath file;
EXPECT_TRUE(base::PathService::Get(base::DIR_TEMP, &file));
file = file.AppendASCII("bar");
// Navigate to url and get it to reference a file in its PageState.
GURL url1(embedded_test_server()->GetURL("/file_input_subframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = wc->GetPrimaryFrameTree().root();
int process_id = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID();
std::unique_ptr<FileChooserDelegate> delegate(
new FileChooserDelegate(file, run_loop.QuitClosure()));
shell()->web_contents()->SetDelegate(delegate.get());
EXPECT_TRUE(ExecJs(root->child_at(0),
"document.getElementById('fileinput').click();"));
run_loop.Run();
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
process_id, file));
// Disable the unload timer so we wait for the UpdateState message.
root->current_frame_host()->DisableUnloadTimerForTesting();
// Do an in-page navigation in the child to make sure we hear a PageState with
// the chosen file before the subframe's FrameTreeNode is deleted. In
// practice, we'll get the PageState 1 second after the file is chosen.
// TODO(creis): Remove this in-page navigation once we keep track of
// FrameTreeNodes that are pending deletion. See https://crbug.com/609963.
{
TestNavigationObserver nav_observer(shell()->web_contents());
std::string script = "location.href='#foo';";
EXPECT_TRUE(ExecJs(root->child_at(0), script));
nav_observer.Wait();
}
// Navigate to a different process without access to the file, and wait for
// the old process to exit.
RenderProcessHostWatcher exit_observer(
shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
// With BackForwardCache, old process won't get deleted on navigation as it is
// still in use by the bfcached document, disable back-forward cache to ensure
// that the process gets deleted.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(shell(), GetCrossSiteURL("/title1.html")));
exit_observer.Wait();
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID(),
file));
// Ensure that the file ended up in the PageState of the previous entry.
NavigationEntry* prev_entry =
shell()->web_contents()->GetController().GetEntryAtIndex(0);
EXPECT_EQ(url1, prev_entry->GetURL());
const std::vector<base::FilePath>& files =
prev_entry->GetPageState().GetReferencedFiles();
ASSERT_EQ(1U, files.size());
EXPECT_EQ(file, files.at(0));
// Go back, ending up in a different RenderProcessHost than before.
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoToIndex(0);
back_nav_load_observer.Wait();
EXPECT_NE(process_id, shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID());
// Ensure that the file access still exists in the new process ID.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID(),
file));
// Do another in-page navigation in the child to make sure we hear a PageState
// with the chosen file.
// TODO(creis): Remove this in-page navigation once we keep track of
// FrameTreeNodes that are pending deletion. See https://crbug.com/609963.
{
TestNavigationObserver nav_observer(shell()->web_contents());
std::string script = "location.href='#foo';";
EXPECT_TRUE(ExecJs(root->child_at(0), script));
nav_observer.Wait();
}
// Also try cloning the tab by creating a new NavigationEntry with the same
// PageState. This exercises a different path, by combining the frame
// specific PageStates into a full-tree PageState and converting back. There
// was a bug where this caused us to lose the list of referenced files. See
// https://crbug.com/620261.
std::unique_ptr<NavigationEntryImpl> cloned_entry =
NavigationEntryImpl::FromNavigationEntry(
NavigationController::CreateNavigationEntry(
url1, Referrer(), /* initiator_origin= */ std::nullopt,
/* initiator_base_url= */ std::nullopt,
ui::PAGE_TRANSITION_RELOAD, false, std::string(),
shell()->web_contents()->GetBrowserContext(),
nullptr /* blob_url_loader_factory */));
prev_entry = shell()->web_contents()->GetController().GetEntryAtIndex(0);
NavigationEntryRestoreContextImpl context;
cloned_entry->SetPageState(prev_entry->GetPageState(), &context);
const std::vector<base::FilePath>& cloned_files =
cloned_entry->GetPageState().GetReferencedFiles();
ASSERT_EQ(1U, cloned_files.size());
EXPECT_EQ(file, cloned_files.at(0));
std::vector<std::unique_ptr<NavigationEntry>> entries;
entries.push_back(std::move(cloned_entry));
Shell* new_shell =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
GURL(), nullptr, gfx::Size());
FrameTreeNode* new_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetPrimaryFrameTree()
.root();
NavigationControllerImpl& new_controller =
static_cast<NavigationControllerImpl&>(
new_shell->web_contents()->GetController());
new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
ASSERT_EQ(0u, entries.size());
{
TestNavigationObserver restore_observer(new_shell->web_contents());
new_controller.LoadIfNecessary();
restore_observer.Wait();
}
ASSERT_EQ(1U, new_root->child_count());
EXPECT_EQ(url1, new_root->current_url());
// Ensure that the file access exists in the new process ID.
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
new_root->current_frame_host()->GetProcess()->GetDeprecatedID(), file));
// Also, extract the file from the renderer process to ensure that the
// response made it over successfully and the proper filename is set.
EXPECT_EQ("bar",
EvalJs(new_root->child_at(0),
"document.getElementById('fileinput').files[0].name;"));
// Navigate to a same site page to trigger a PageState update and ensure the
// renderer is not killed.
EXPECT_TRUE(
NavigateToURL(new_shell, embedded_test_server()->GetURL("/title2.html")));
}
// Ensures that no RenderFrameHost/RenderViewHost objects are leaked when
// doing a simple cross-process navigation.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
CleanupOnCrossProcessNavigation) {
StartEmbeddedServer();
// Do an initial navigation and capture objects we expect to be cleaned up
// on cross-process navigation.
GURL start_url = embedded_test_server()->GetURL("/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), start_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
auto orig_site_instance_id =
root->current_frame_host()->GetSiteInstance()->GetId();
int initial_process_id = root->current_frame_host()
->GetSiteInstance()
->GetProcess()
->GetDeprecatedID();
int initial_rfh_id = root->current_frame_host()->GetRoutingID();
int initial_rvh_id =
root->current_frame_host()->render_view_host()->GetRoutingID();
// Navigate cross-process and ensure that cleanup is performed as expected.
GURL cross_site_url =
embedded_test_server()->GetURL("foo.com", "/title2.html");
RenderFrameHostDestructionObserver rfh_observer(root->current_frame_host());
// The old RenderFrameHost might have entered the BackForwardCache. Disable
// back-forward cache to ensure that the RenderFrameHost gets deleted.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
rfh_observer.Wait();
EXPECT_NE(orig_site_instance_id,
root->current_frame_host()->GetSiteInstance()->GetId());
EXPECT_FALSE(RenderFrameHost::FromID(initial_process_id, initial_rfh_id));
EXPECT_FALSE(RenderViewHost::FromID(initial_process_id, initial_rvh_id));
}
// Ensure that the opener chain proxies and RVHs are properly reinitialized if
// a tab crashes and reloads. See https://crbug.com/505090.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ReinitializeOpenerChainAfterCrashAndReload) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed to get a non-default
// SiteInstance for navigations to this origin.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
GURL main_url = embedded_test_server()->GetURL("/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance);
// Open a popup and navigate it cross-site.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
EXPECT_TRUE(new_shell);
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetPrimaryFrameTree()
.root();
GURL cross_site_url =
embedded_test_server()->GetURL("foo.com", "/title2.html");
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(new_shell, cross_site_url));
scoped_refptr<SiteInstance> foo_site_instance(
new_shell->web_contents()->GetSiteInstance());
EXPECT_NE(foo_site_instance, orig_site_instance);
// Kill the popup's process.
RenderProcessHost* popup_process =
popup_root->current_frame_host()->GetProcess();
RenderProcessHostWatcher crash_observer(
popup_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
popup_process->Shutdown(0);
crash_observer.Wait();
EXPECT_FALSE(popup_root->current_frame_host()->IsRenderFrameLive());
EXPECT_FALSE(
popup_root->current_frame_host()->render_view_host()->IsRenderViewLive());
// The proxy and RVH for the opener page in the foo.com SiteInstanceGroup
// should not be live.
RenderFrameHostManager* opener_manager = root->render_manager();
RenderFrameProxyHost* opener_rfph =
opener_manager->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(
static_cast<SiteInstanceImpl*>(foo_site_instance.get())->group());
EXPECT_TRUE(opener_rfph);
EXPECT_FALSE(opener_rfph->is_render_frame_proxy_live());
RenderViewHostImpl* opener_rvh = opener_rfph->GetRenderViewHost();
EXPECT_TRUE(opener_rvh);
EXPECT_FALSE(opener_rvh->IsRenderViewLive());
// Re-navigate the popup to the same URL and check that this recreates the
// opener's RVH and proxy in the foo.com SiteInstanceGroup.
EXPECT_TRUE(NavigateToURL(new_shell, cross_site_url));
EXPECT_TRUE(opener_rvh->IsRenderViewLive());
EXPECT_TRUE(opener_rfph->is_render_frame_proxy_live());
}
// Test that when a frame's opener is updated via window.open, the browser
// process and the frame's proxies in other processes find out about the new
// opener. Open two popups in different processes, set one popup's opener to
// the other popup, and ensure that the opener is updated in all processes.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, UpdateOpener) {
StartEmbeddedServer();
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
// Isolate "foo.com" so we are guaranteed it is placed in a different
// process.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"foo.com"});
}
GURL main_url = embedded_test_server()->GetURL("/post_message.html");
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// It is safe to obtain the root frame tree node here, as it doesn't change.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_TRUE(orig_site_instance);
// Open a cross-site popup named "foo" and a same-site popup named "bar".
Shell* foo_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
EXPECT_TRUE(foo_shell);
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/post_message.html"));
EXPECT_TRUE(NavigateToURLInSameBrowsingInstance(foo_shell, foo_url));
GURL bar_url(embedded_test_server()->GetURL(
"/frame_tree/page_with_post_message_frames.html"));
Shell* bar_shell = OpenPopup(shell(), bar_url, "bar");
EXPECT_TRUE(bar_shell);
EXPECT_NE(shell()->web_contents()->GetSiteInstance()->GetProcess(),
foo_shell->web_contents()->GetSiteInstance()->GetProcess());
EXPECT_EQ(shell()->web_contents()->GetSiteInstance()->GetProcess(),
bar_shell->web_contents()->GetSiteInstance()->GetProcess());
// Initially, both popups' openers should point to main window.
FrameTreeNode* foo_root =
static_cast<WebContentsImpl*>(foo_shell->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* bar_root =
static_cast<WebContentsImpl*>(bar_shell->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(root, foo_root->opener());
EXPECT_EQ(root, foo_root->first_live_main_frame_in_original_opener_chain());
EXPECT_EQ(root, bar_root->opener());
EXPECT_EQ(root, bar_root->first_live_main_frame_in_original_opener_chain());
// From the bar process, use window.open to update foo's opener to point to
// bar. This is allowed since bar is same-origin with foo's opener. Use
// window.open with an empty URL, which should return a reference to the
// target frame without navigating it.
EXPECT_EQ(true, EvalJs(bar_shell, "!!window.open('','foo');"));
EXPECT_FALSE(foo_shell->web_contents()->IsLoading());
EXPECT_EQ(foo_url, foo_root->current_url());
// Check that updated opener propagated to the browser process.
EXPECT_EQ(bar_root, foo_root->opener());
EXPECT_EQ(root, foo_root->first_live_main_frame_in_original_opener_chain());
// Check that foo's opener was updated in foo's process. Send a postMessage
// to the opener and check that the right window (bar_shell) receives it.
std::u16string expected_title = u"opener-msg";
TitleWatcher title_watcher(bar_shell->web_contents(), expected_title);
EXPECT_EQ(true, EvalJs(foo_shell, "postToOpener('opener-msg', '*');"));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
// Check that a non-null assignment to the opener doesn't change the opener
// in the browser process.
EXPECT_TRUE(ExecJs(foo_shell, "window.opener = window;"));
EXPECT_EQ(bar_root, foo_root->opener());
EXPECT_EQ(root, foo_root->first_live_main_frame_in_original_opener_chain());
}
// Tests that when a popup is opened, which is then navigated cross-process and
// back, it can be still accessed through the original window reference in
// JavaScript. See https://crbug.com/537657
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
PopupKeepsWindowReferenceCrossProcesAndBack) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
NavigateToPageWithLinks(shell());
// Click a target=foo link to open a popup.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(), "clickSameSiteTargetedLink();"));
Shell* new_shell = new_shell_observer.GetShell();
EXPECT_TRUE(new_shell->web_contents()->HasOpener());
// Wait for the navigation in the popup to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Capture the window reference, so we can check that accessing its location
// works after navigating cross-process and back.
GURL expected_url = new_shell->web_contents()->GetLastCommittedURL();
EXPECT_TRUE(ExecJs(shell(), "saveWindowReference();"));
// Now navigate the popup to a different site and then go back.
EXPECT_TRUE(NavigateToURL(
new_shell, embedded_test_server()->GetURL("foo.com", "/title1.html")));
TestNavigationObserver back_nav_load_observer(new_shell->web_contents());
new_shell->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
// Check that the location.href window attribute is accessible and is correct.
EXPECT_EQ(expected_url.spec(),
EvalJs(shell(), "getLastOpenedWindowLocation();"));
}
// Tests that going back to the same SiteInstance as a pending RenderFrameHost
// doesn't create a duplicate RenderFrameProxyHost. For example:
// 1. Navigate to a page on the opener site - a.com
// 2. Navigate to a page on site b.com
// 3. Start a navigation to another page on a.com, but commit is delayed.
// 4. Go back.
// See https://crbug.com/541619.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
PopupPendingAndBackToSameSiteInstance) {
StartEmbeddedServer();
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL popup_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Open a popup to navigate.
Shell* new_shell = OpenPopup(shell(), popup_url, "foo");
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
// Navigate the popup to a different site.
EXPECT_TRUE(NavigateToURL(
new_shell, embedded_test_server()->GetURL("b.com", "/title2.html")));
// Navigate again to the original site, but to a page that will take a while
// to commit.
GURL same_site_url(embedded_test_server()->GetURL("a.com", "/title3.html"));
TestNavigationManager stalled_navigation(new_shell->web_contents(),
same_site_url);
new_shell->LoadURL(same_site_url);
EXPECT_TRUE(stalled_navigation.WaitForRequestStart());
// Going back in history should work and the test should not crash.
TestNavigationObserver back_nav_load_observer(new_shell->web_contents());
new_shell->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
// Tests that navigating cross-process and reusing an existing RenderViewHost
// (whose process has been killed/crashed) recreates properly the
// `blink::WebView` and `blink::RemoteFrame` on the renderer side. See
// https://crbug.com/544271
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RenderViewInitAfterProcessKill) {
StartEmbeddedServer();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a popup to navigate.
Shell* new_shell = OpenPopup(
shell(), embedded_test_server()->GetURL("a.com", "/title2.html"), "foo");
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
// Navigate the popup to a different site.
EXPECT_TRUE(NavigateToURL(
new_shell, embedded_test_server()->GetURL("b.com", "/title2.html")));
EXPECT_NE(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
// Kill the process hosting the popup.
RenderProcessHost* process = popup_root->current_frame_host()->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
EXPECT_FALSE(popup_root->current_frame_host()->IsRenderFrameLive());
EXPECT_FALSE(
popup_root->current_frame_host()->render_view_host()->IsRenderViewLive());
// Navigate the main tab to the site of the popup. This will cause the
// `blink::WebView` for b.com in the main tab to be recreated. If the issue
// is not fixed, this will result in process crash and failing test.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("b.com", "/title3.html")));
}
// Ensure that we don't crash the renderer in CreateRenderView if a proxy goes
// away between unload and the next navigation. See https://crbug.com/581912.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
CreateRenderViewAfterProcessKillAndClosedProxy) {
StartEmbeddedServer();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Give an initial page an unload handler that never completes.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
EXPECT_TRUE(ExecJs(root, "window.onunload=function(e){ while(1); };\n"));
// Open a popup in the same process.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
FrameTreeNode* popup_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
// Navigate the first tab to a different site, and only wait for commit, not
// load stop.
RenderFrameHostImpl* rfh_a = root->current_frame_host();
rfh_a->DisableUnloadTimerForTesting();
scoped_refptr<SiteInstanceImpl> site_instance_a = rfh_a->GetSiteInstance();
TestFrameNavigationObserver commit_observer(root);
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title2.html"));
commit_observer.WaitForCommit();
EXPECT_NE(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(site_instance_a->group()));
// The previous RFH should still be pending deletion, as we wait for either
// the mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame or a timeout.
ASSERT_TRUE(rfh_a->IsRenderFrameLive());
ASSERT_TRUE(rfh_a->IsPendingDeletion());
// The corresponding RVH should still be referenced by the proxy and the old
// frame.
RenderViewHostImpl* rvh_a = rfh_a->render_view_host();
EXPECT_FALSE(rvh_a->HasOneRef());
EXPECT_TRUE(rvh_a->HasAtLeastOneRef());
// Kill the old process.
RenderProcessHost* process = rfh_a->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
EXPECT_FALSE(popup_root->current_frame_host()->IsRenderFrameLive());
// |rfh_a| is now deleted, thanks to the bug fix.
// With |rfh_a| gone, the RVH should only be referenced by the (dead) proxy.
EXPECT_TRUE(rvh_a->HasOneRef());
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(site_instance_a->group()));
EXPECT_FALSE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(site_instance_a->group())
->is_render_frame_proxy_live());
// Close the popup so there is no proxy for a.com in the original tab.
new_shell->Close();
// Verify that there are no proxies, meaning there's no proxy for a.com. At
// this point, |site_instance_group_a| has been freed, so searching the proxy
// host map using it isn't an option.
EXPECT_EQ(nullptr, site_instance_a->group());
EXPECT_EQ(0u, root->current_frame_host()
->browsing_context_state()
->proxy_hosts()
.size());
// This should delete the RVH as well. Check this by verifying that there's
// only one RVH in the frame tree, and it's for the current SiteInstanceGroup,
// not |site_instance_group_a|.
EXPECT_TRUE(root->frame_tree().GetRenderViewHost(
root->current_frame_host()->GetSiteInstance()->group()));
EXPECT_EQ(1u, root->frame_tree().render_view_host_map_.size());
// Go back in the main frame from b.com to a.com. In https://crbug.com/581912,
// the browser process would crash here because there was no main frame
// routing ID or proxy in RVHI::CreateRenderView.
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
}
// Ensure that we don't crash in RenderViewImpl::Init if a proxy is created
// after unload and before navigation. See https://crbug.com/544755.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RenderViewInitAfterNewProxyAndProcessKill) {
StartEmbeddedServer();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Give an initial page a pagehide handler that never completes.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
EXPECT_TRUE(ExecJs(root, "window.onpagehide=function(e){ while(1); };\n"));
// With BackForwardCache, swapped out RenderFrameHost won't have a
// replacement proxy as the document is stored in cache.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
// Navigate the tab to a different site, and only wait for commit, not load
// stop.
RenderFrameHostImpl* rfh_a = root->current_frame_host();
rfh_a->DisableUnloadTimerForTesting();
SiteInstanceImpl* site_instance_a = rfh_a->GetSiteInstance();
TestFrameNavigationObserver commit_observer(root);
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title2.html"));
commit_observer.WaitForCommit();
EXPECT_NE(site_instance_a, shell()->web_contents()->GetSiteInstance());
// The previous RFH should still be pending deletion, as we wait for either
// the unload ACK or a timeout.
ASSERT_TRUE(rfh_a->IsRenderFrameLive());
ASSERT_TRUE(rfh_a->IsPendingDeletion());
// When the previous RFH was unloaded, it should have still gotten a
// replacement proxy even though it's the last active frame in the process.
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(site_instance_a->group()));
// Open a popup in the new B process.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
// Navigate the popup to the original site, but don't wait for commit (which
// won't happen). This should reuse the proxy in the original tab, which at
// this point exists alongside the RFH pending deletion.
new_shell->LoadURL(embedded_test_server()->GetURL("a.com", "/title2.html"));
EXPECT_TRUE(root->current_frame_host()
->browsing_context_state()
->GetRenderFrameProxyHost(site_instance_a->group()));
// Kill the old process.
RenderProcessHost* process = rfh_a->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
// |rfh_a| is now deleted, thanks to the bug fix.
// Go back in the main frame from b.com to a.com.
{
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_nav_load_observer.Wait();
}
// In https://crbug.com/581912, the renderer process would crash here because
// there was a frame, view, and proxy (and is_swapped_out was true).
EXPECT_EQ(site_instance_a, root->current_frame_host()->GetSiteInstance());
EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(
new_shell->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());
}
// Ensure that we use the same pending RenderFrameHost if a second navigation to
// its site occurs before it commits. Otherwise the renderer process will have
// two competing pending RenderFrames that both try to swap with the same
// RenderFrameProxy. See https://crbug.com/545900.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ConsecutiveNavigationsToSite) {
StartEmbeddedServer();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a popup and navigate it to b.com to keep the b.com process alive.
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "popup");
EXPECT_TRUE(NavigateToURL(
new_shell, embedded_test_server()->GetURL("b.com", "/title3.html")));
// Start a cross-site navigation to the same site but don't commit.
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
TestNavigationManager stalled_navigation(shell()->web_contents(),
cross_site_url);
shell()->LoadURL(cross_site_url);
EXPECT_TRUE(stalled_navigation.WaitForResponse());
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* next_rfh = web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
ASSERT_TRUE(next_rfh);
// Navigate to the same new site and verify that we commit in the same RFH.
GURL cross_site_url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
TestNavigationObserver navigation_observer(web_contents, 1);
shell()->LoadURL(cross_site_url2);
EXPECT_EQ(next_rfh, web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host());
navigation_observer.Wait();
EXPECT_EQ(cross_site_url2, web_contents->GetLastCommittedURL());
EXPECT_EQ(next_rfh, web_contents->GetPrimaryMainFrame());
EXPECT_FALSE(web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host());
}
// Check that if a sandboxed subframe opens a cross-process popup such that the
// popup's opener won't be set, the popup still inherits the subframe's sandbox
// flags. This matters for rel=noopener and rel=noreferrer links, as well as
// for some situations in non-site-per-process mode where the popup would
// normally maintain the opener, but loses it due to being placed in a new
// process and not creating subframe proxies. The latter might happen when
// opening the default search provider site. See https://crbug.com/576204.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
CrossProcessPopupInheritsSandboxFlagsWithNoOpener) {
StartEmbeddedServer();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_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));
}
// Navigate iframe to a page with target=_blank links, and rewrite the links
// to point to valid cross-site URLs.
GURL frame_url(
embedded_test_server()->GetURL("a.com", "/click-noreferrer-links.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
std::string script = "setOriginForLinks('http://b.com:" +
embedded_test_server()->base_url().port() + "/');";
EXPECT_TRUE(ExecJs(root->child_at(0), script));
// Helper to click on the 'rel=noreferrer target=_blank' and 'rel=noopener
// target=_blank' links. Checks that these links open a popup that ends up
// in a new SiteInstance even without site-per-process and then verifies that
// the popup is still sandboxed.
auto click_link_and_verify_popup = [this,
root](std::string link_opening_script) {
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(root->child_at(0), link_opening_script));
Shell* new_shell = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_NE(new_shell->web_contents()->GetSiteInstance(),
shell()->web_contents()->GetSiteInstance());
// Check that the popup is sandboxed by checking its self.origin, which
// should be unique.
EXPECT_EQ("null", EvalJs(new_shell, "self.origin"));
};
click_link_and_verify_popup("clickNoOpenerTargetBlankLink()");
click_link_and_verify_popup("clickNoRefTargetBlankLink()");
}
// When two frames are same-origin but cross-process, they should behave as if
// they are not same-origin and should not crash.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SameOriginFramesInDifferentProcesses) {
StartEmbeddedServer();
// Load a page with links that open in a new window.
EXPECT_TRUE(NavigateToURL(
shell(),
embedded_test_server()->GetURL("a.com", "/click-noreferrer-links.html")));
// Get the original SiteInstance for later comparison.
scoped_refptr<SiteInstance> orig_site_instance(
shell()->web_contents()->GetSiteInstance());
EXPECT_NE(nullptr, orig_site_instance.get());
// Test clicking a target=foo link.
ShellAddedObserver new_shell_observer;
EXPECT_EQ(true, EvalJs(shell(),
"const result = clickSameSiteTargetedLink();"
"saveWindowReference();"
"result;"));
Shell* new_shell = new_shell_observer.GetShell();
// Wait for the navigation in the new tab to finish, if it hasn't.
EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
EXPECT_EQ("/navigate_opener.html",
new_shell->web_contents()->GetLastCommittedURL().path());
// Do a cross-site navigation that winds up same-site. The same-site
// navigation to a.com will commit in a different process than the original
// a.com window.
EXPECT_TRUE(NavigateToURL(
new_shell,
embedded_test_server()->GetURL("b.com", "/cross-site/a.com/title1.html"),
/* expected_commit_url */
embedded_test_server()->GetURL("a.com", "/title1.html")));
// The SiteInstance for the navigation is determined after the redirect.
// So both windows will actually be in the same process.
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
new_shell->web_contents()->GetSiteInstance());
std::string result = EvalJs(shell(),
"(function() {\n"
" try {\n"
" return getLastOpenedWindowLocation();\n"
" } catch (e) {\n"
" return e.toString();\n"
" }\n"
"})()")
.ExtractString();
EXPECT_THAT(result, ::testing::MatchesRegex("http://a.com:\\d+/title1.html"));
}
// Test coverage for attempts to open subframe links in new windows, to prevent
// incorrect invariant checks. See https://crbug.com/605055.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, CtrlClickSubframeLink) {
StartEmbeddedServer();
// Load a page with a subframe link.
EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
"/ctrl-click-subframe-link.html")));
// Simulate a ctrl click on the link. This won't actually create a new Shell
// because Shell::OpenURLFromTab only supports CURRENT_TAB, but it's enough to
// trigger the crash from https://crbug.com/605055.
EXPECT_TRUE(
ExecJs(shell(), "window.domAutomationController.send(ctrlClickLink());"));
}
// Ensure that we don't update the wrong NavigationEntry's title after an
// ignored commit during a cross-process navigation.
// See https://crbug.com/577449.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
UnloadPushStateOnCrossProcessNavigation) {
// TODO(sreejakshetty): Replace 'unload' with 'pagehide' and reenable this
// test for BackForwardCache.
DisableBackForwardCache(content::BackForwardCache::TEST_USES_UNLOAD_EVENT);
StartEmbeddedServer();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
// Give an initial page an unload handler that does a pushState, which will be
// ignored by the browser process. It then does a title update which is
// meant for a NavigationEntry that will never be created.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title2.html")));
EXPECT_TRUE(ExecJs(root,
"window.onunload=function(e){"
"history.pushState({}, 'foo', 'foo');"
"document.title='foo'; };\n"));
std::u16string title = web_contents->GetTitle();
NavigationEntryImpl* entry = web_contents->GetController().GetEntryAtIndex(0);
// Navigate the first tab to a different site and wait for the old process to
// complete its unload handler and exit.
RenderFrameHostImpl* rfh_a = root->current_frame_host();
rfh_a->DisableUnloadTimerForTesting();
RenderProcessHostWatcher exit_observer(
rfh_a->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
TestNavigationObserver commit_observer(web_contents);
shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title1.html"));
commit_observer.Wait();
exit_observer.Wait();
// Ensure the entry's title hasn't changed after the ignored commit.
EXPECT_EQ(title, entry->GetTitle());
}
// Ensure that document hosted on file: URL can successfully execute pushState
// with arbitrary origin, when universal access setting is enabled.
// TODO(nasko): The test is disabled on Mac, since universal access from file
// scheme behaves differently. See also https://crbug.com/981018.
#if BUILDFLAG(IS_MAC)
#define MAYBE_EnsureUniversalAccessFromFileSchemeSucceeds \
DISABLED_EnsureUniversalAccessFromFileSchemeSucceeds
#else
#define MAYBE_EnsureUniversalAccessFromFileSchemeSucceeds \
EnsureUniversalAccessFromFileSchemeSucceeds
#endif
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
MAYBE_EnsureUniversalAccessFromFileSchemeSucceeds) {
StartEmbeddedServer();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
auto prefs = web_contents->GetOrCreateWebPreferences();
prefs.allow_universal_access_from_file_urls = true;
web_contents->SetWebPreferences(prefs);
GURL file_url = GetTestUrl("", "title1.html");
ASSERT_TRUE(file_url.SchemeIsFile());
ASSERT_TRUE(NavigateToURL(shell(), file_url));
EXPECT_EQ(1, web_contents->GetController().GetEntryCount());
EXPECT_TRUE(
ExecJs(root, "history.pushState({}, '', 'https://chromium.org');"));
ASSERT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
EXPECT_EQ(2, web_contents->GetController().GetEntryCount());
// At this point, we should still consider the current origin to be file://,
// so that subsequent web or file URLs would still be legal for same-document
// navigations. See https://crbug.com/553418.
const url::Origin file_origin = url::Origin::Create(file_url);
EXPECT_TRUE(file_origin.IsSameOriginWith(
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()));
EXPECT_TRUE(ExecJs(root, "history.pushState({}, '', 'https://foo.com');"));
ASSERT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
EXPECT_EQ(3, web_contents->GetController().GetEntryCount());
EXPECT_TRUE(
ExecJs(root, JsReplace("history.pushState({}, '', $1);", file_url)));
ASSERT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
EXPECT_EQ(4, web_contents->GetController().GetEntryCount());
// Illegal schemes would not normally be allowed to commit by CanCommitURL,
// but they are granted an exception if allow_universal_access_from_file_urls
// is in use.
GURL illegal_url("google:com");
EXPECT_TRUE(ExecJs(
root, JsReplace("history.replaceState({}, '', $1);", illegal_url)));
ASSERT_TRUE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
EXPECT_EQ(4, web_contents->GetController().GetEntryCount());
// Illegal schemes should also work for document.open on same-origin frames,
// where the initiator's URL is inherited (in the renderer process).
std::string create_frame_and_open_script =
"var new_iframe = document.createElement('iframe');"
"document.documentElement.appendChild(new_iframe);"
"new_iframe.contentDocument.open();";
EXPECT_TRUE(ExecJs(shell(), create_frame_and_open_script));
EXPECT_EQ(
illegal_url,
root->child_at(0)->current_frame_host()->last_document_url_in_renderer());
// Ensure the renderer process has not crashed.
ASSERT_TRUE(ExecJs(shell(), "true"));
ASSERT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
// Now disable universal access, while still allowing file URLs to access each
// other. This generally turns off the exemption from commit-time security
// checks, while still allowing document.open to work in file:// origins.
prefs.allow_universal_access_from_file_urls = false;
prefs.allow_file_access_from_file_urls = true;
web_contents->SetWebPreferences(prefs);
// Calling document.open on another iframe should remember that the process
// already had an exemption for file:// origins and continue to work.
// See https://crbug.com/326250356#comment26.
EXPECT_TRUE(ExecJs(shell(), create_frame_and_open_script));
EXPECT_EQ(
illegal_url,
root->child_at(1)->current_frame_host()->last_document_url_in_renderer());
// Ensure the renderer process has not crashed.
ASSERT_TRUE(ExecJs(shell(), "true"));
ASSERT_TRUE(root->child_at(1)->current_frame_host()->IsRenderFrameLive());
}
// Check that file:// URLs are correctly isolated. This test runs with the
// default site isolation of the platform, e.g. enabled on desktop and disabled
// on Android.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, FileURLIsolation) {
StartEmbeddedServer();
// Navigate to a main frame file:// URL.
GURL file_url = GetTestUrl("", "title1.html");
ASSERT_TRUE(file_url.SchemeIsFile());
ASSERT_TRUE(NavigateToURL(shell(), file_url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ChildProcessId file_process_id =
web_contents->GetPrimaryMainFrame()->GetProcess()->GetID();
// Navigate to a regular web page, a.com, with a same-site subframe.
GURL a_url(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
ASSERT_TRUE(NavigateToURL(shell(), a_url));
// The file:// URL and a.com should be in different processes, to avoid file
// permission grants accumulating on a single process.
RenderProcessHost* a_process =
web_contents->GetPrimaryMainFrame()->GetProcess();
EXPECT_NE(a_process->GetID(), file_process_id);
// With full site isolation, the process should be locked to the file:// URL
// like any other site. Otherwise, it will be in an unlocked process. On
// Android WebView (though this test doesn't run there), navigating to a
// file:// URL should not cause a process swap.
EXPECT_EQ(a_process->IsProcessLockedToSiteForTesting(),
AreAllSitesIsolatedForTesting());
// Do a browser initiated navigation of the a.com subframe to a file:// URL.
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
scoped_refptr<SiteInstanceImpl> root_instance =
root->current_frame_host()->GetSiteInstance();
FrameTreeNode* child = root->child_at(0);
NavigateFrameToURL(child, file_url);
scoped_refptr<SiteInstanceImpl> child_instance =
child->current_frame_host()->GetSiteInstance();
RenderProcessHost* child_process = child->current_frame_host()->GetProcess();
// With full site isolation, the file:// URL should get its own process.
// Otherwise, it should share a process with the parent.
if (AreAllSitesIsolatedForTesting()) {
EXPECT_NE(a_process, child_process);
// With full site isolation, the file:// subframe should be in a different
// SiteInstance from the parent.
EXPECT_NE(root_instance, child_instance);
EXPECT_NE(root_instance->group(), child_instance->group());
} else {
// TODO(crbug.com/40704573): When default SiteInstanceGroup supports file://
// URLs on Android WebView and low-end Clank, the file:// SiteInstance
// should be in the default SiteInstanceGroup when site isolation is not
// enabled.
EXPECT_EQ(a_process, child_process);
// With default SiteInstanceGroup enabled, the file:// subframe should be in
// a different SiteInstance from the parent. Otherwise it should share a
// site instance with the parent.
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_NE(root_instance, child_instance);
EXPECT_NE(root_instance->group(), child_instance->group());
} else {
EXPECT_EQ(root_instance, child_instance);
EXPECT_EQ(root_instance->group(), child_instance->group());
}
}
}
// Ensure that navigating back from a sad tab to an existing process works
// correctly. See https://crbug.com/591984.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigateBackToExistingProcessFromSadTab) {
StartEmbeddedServer();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// Open a popup and navigate it to b.com.
Shell* popup = OpenPopup(
shell(), embedded_test_server()->GetURL("a.com", "/title2.html"), "foo");
EXPECT_TRUE(NavigateToURL(
popup, embedded_test_server()->GetURL("b.com", "/title3.html")));
// Kill the b.com process.
RenderProcessHost* b_process =
popup->web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostWatcher crash_observer(
b_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
b_process->Shutdown(0);
crash_observer.Wait();
// The popup should now be showing the sad tab. Main tab should not be.
EXPECT_NE(base::TERMINATION_STATUS_STILL_RUNNING,
popup->web_contents()->GetCrashedStatus());
EXPECT_EQ(base::TERMINATION_STATUS_STILL_RUNNING,
shell()->web_contents()->GetCrashedStatus());
// Go back in the popup from b.com to a.com/title2.html.
TestNavigationObserver back_observer(popup->web_contents());
popup->web_contents()->GetController().GoBack();
back_observer.Wait();
// In the bug, after the back navigation the popup was still showing
// the sad tab. Ensure this is not the case.
EXPECT_EQ(base::TERMINATION_STATUS_STILL_RUNNING,
popup->web_contents()->GetCrashedStatus());
EXPECT_TRUE(
popup->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());
EXPECT_EQ(popup->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(),
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
}
// Verify that GetLastCommittedOrigin() is correct for the full lifetime of a
// RenderFrameHost, including when it's pending, current, and pending deletion.
// This is checked both for main frames and subframes.
// See https://crbug.com/590035.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, LastCommittedOrigin) {
StartEmbeddedServer();
// Disable the back-forward cache so that documents are always deleted when
// navigating.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderFrameHostImpl* rfh_a = root->current_frame_host();
rfh_a->DisableUnloadTimerForTesting();
EXPECT_EQ(url::Origin::Create(url_a), rfh_a->GetLastCommittedOrigin());
EXPECT_EQ(rfh_a, web_contents->GetPrimaryMainFrame());
// Start a navigation to a b.com URL, and don't wait for commit.
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
TestNavigationManager navigation_manager(web_contents, url_b);
RenderFrameDeletedObserver deleted_observer(rfh_a);
shell()->LoadURL(url_b);
navigation_manager.WaitForSpeculativeRenderFrameHostCreation();
// The speculative RFH shouln't have a last committed origin (the default
// value is a unique origin). The current RFH shouldn't change its last
// committed origin before commit.
RenderFrameHostImpl* rfh_b = root->render_manager()->speculative_frame_host();
EXPECT_EQ("null", rfh_b->GetLastCommittedOrigin().Serialize());
EXPECT_EQ(url::Origin::Create(url_a), rfh_a->GetLastCommittedOrigin());
// Verify that the last committed origin is set for the b.com RHF once it
// commits.
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_EQ(url::Origin::Create(url_b), rfh_b->GetLastCommittedOrigin());
EXPECT_EQ(rfh_b, web_contents->GetPrimaryMainFrame());
// The old RFH should now be pending deletion. Verify it still has correct
// last committed origin.
EXPECT_EQ(url::Origin::Create(url_a), rfh_a->GetLastCommittedOrigin());
EXPECT_TRUE(rfh_a->IsPendingDeletion());
// Wait for |rfh_a| to be deleted and double-check |rfh_b|'s origin.
deleted_observer.WaitUntilDeleted();
EXPECT_EQ(url::Origin::Create(url_b), rfh_b->GetLastCommittedOrigin());
// Navigate to a same-origin page with an about:blank iframe. The iframe
// should also have a b.com origin.
GURL url_b_with_frame(embedded_test_server()->GetURL(
"b.com", "/navigation_controller/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_b_with_frame));
if (ShouldCreateNewHostForAllFrames()) {
// If main-frame RenderDocument is enabled, the navigation will result in a
// new RFH.
EXPECT_NE(rfh_b, web_contents->GetPrimaryMainFrame());
rfh_b = web_contents->GetPrimaryMainFrame();
} else {
EXPECT_EQ(rfh_b, web_contents->GetPrimaryMainFrame());
}
EXPECT_EQ(url::Origin::Create(url_b), rfh_b->GetLastCommittedOrigin());
FrameTreeNode* child = root->child_at(0);
RenderFrameHostImpl* child_rfh_b = root->child_at(0)->current_frame_host();
child_rfh_b->DisableUnloadTimerForTesting();
EXPECT_EQ(url::Origin::Create(url_b), child_rfh_b->GetLastCommittedOrigin());
// Navigate subframe to c.com. Wait for commit but not full load, and then
// verify the subframe's origin.
GURL url_c(embedded_test_server()->GetURL("c.com", "/title3.html"));
{
TestFrameNavigationObserver child_commit_observer(root->child_at(0));
EXPECT_TRUE(ExecJs(child, "location.href = '" + url_c.spec() + "';"));
child_commit_observer.WaitForCommit();
}
EXPECT_EQ(url::Origin::Create(url_c),
child->current_frame_host()->GetLastCommittedOrigin());
// With OOPIFs, this navigation used a cross-process transfer. Ensure that
// the iframe's old RFH still has correct origin, even though it's pending
// deletion.
if (AreAllSitesIsolatedForTesting()) {
EXPECT_TRUE(child_rfh_b->IsPendingDeletion());
EXPECT_NE(child_rfh_b, child->current_frame_host());
EXPECT_EQ(url::Origin::Create(url_b),
child_rfh_b->GetLastCommittedOrigin());
}
}
// Ensure that loading a page with cross-site coreferencing iframes does not
// cause an infinite number of nested iframes to be created.
// See https://crbug.com/650332.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, CoReferencingFrames) {
// Load a page with a cross-site coreferencing iframe. "Coreferencing" here
// refers to two separate pages that contain subframes with URLs to each
// other.
StartEmbeddedServer();
GURL url_1(
embedded_test_server()->GetURL("a.com", "/coreferencingframe_1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
// The FrameTree contains two successful instances of each site plus an
// unsuccessfully-navigated third instance of B with a blank URL. When not in
// strict SiteInstance mode, the FrameTreeVisualizer depicts all nodes as
// referencing Site A because iframes are identified with their root site.
if (AreAllSitesIsolatedForTesting()) {
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site A -- proxies for B\n"
" +--Site B -- proxies for A\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
DepictFrameTree(*root));
} else if (ShouldUseDefaultSiteInstanceGroup()) {
// No proxies needed for different SiteInstance in the same
// SiteInstanceGroup.
EXPECT_EQ(
" Site A\n"
" +--Site B\n"
" +--Site A\n"
" +--Site B\n"
" +--Site B\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
DepictFrameTree(*root));
} else {
const GURL kExpectedSiteURL = AreAllSitesIsolatedForTesting()
? GURL("http://a.com/")
: SiteInstanceImpl::GetDefaultSiteURL();
EXPECT_EQ(std::string(" Site A\n"
" +--Site A\n"
" +--Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = ") +
kExpectedSiteURL.spec(),
DepictFrameTree(*root));
}
FrameTreeNode* bottom_child =
root->child_at(0)->child_at(0)->child_at(0)->child_at(0);
EXPECT_TRUE(bottom_child->current_url().is_empty());
EXPECT_TRUE(bottom_child->is_on_initial_empty_document());
}
// Ensures that nested subframes with the same URL but different fragments can
// only be nested once. See https://crbug.com/650332.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SelfReferencingFragmentFrames) {
StartEmbeddedServer();
GURL url(
embedded_test_server()->GetURL("a.com", "/page_with_iframe.html#123"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
// ExecJs is used here and once more below because it is important to
// use renderer-initiated navigations since browser-initiated navigations are
// bypassed in the self-referencing navigation check.
TestFrameNavigationObserver observer1(child);
EXPECT_TRUE(ExecJs(child, "location.href = '" + url.spec() + "456" + "';"));
observer1.Wait();
FrameTreeNode* grandchild = child->child_at(0);
GURL expected_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_EQ(expected_url, grandchild->current_url());
// This navigation should be blocked.
GURL blocked_url(embedded_test_server()->GetURL(
"a.com", "/page_with_iframe.html#123456789"));
TestNavigationManager manager(web_contents, blocked_url);
EXPECT_TRUE(
ExecJs(grandchild, "location.href = '" + blocked_url.spec() + "';"));
// Wait for WillStartRequest and verify that the request is aborted before
// starting it.
EXPECT_FALSE(manager.WaitForRequestStart());
EXPECT_TRUE(WaitForLoadStop(web_contents));
// The FrameTree contains two successful instances of the url plus an
// unsuccessfully-navigated third instance with a blank URL.
const GURL kExpectedSiteURL = AreStrictSiteInstancesEnabled()
? GURL("http://a.com/")
: SiteInstanceImpl::GetDefaultSiteURL();
EXPECT_EQ(std::string(" Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = ") +
kExpectedSiteURL.spec(),
DepictFrameTree(*root));
// The URL of the grandchild has not changed.
EXPECT_EQ(expected_url, grandchild->current_url());
}
// Ensure that loading a page with a meta refresh iframe does not cause an
// infinite number of nested iframes to be created. This test loads a page with
// an about:blank iframe where the page injects html containing a meta refresh
// into the iframe. This test then checks that this does not cause infinite
// nested iframes to be created. See https://crbug.com/527367.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SelfReferencingMetaRefreshFrames) {
// Load a page with a blank iframe.
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL(
"a.com", "/page_with_meta_refresh_frame.html"));
NavigateToURLBlockUntilNavigationsComplete(shell(), url, 3);
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
// The third navigation should fail and be cancelled, leaving a FrameTree with
// a height of 2.
const GURL kExpectedSiteURL = AreStrictSiteInstancesEnabled()
? GURL("http://a.com/")
: SiteInstanceImpl::GetDefaultSiteURL();
// The FrameTreeVisualizer test ensure that the childmost frame is not loaded.
EXPECT_EQ(std::string(" Site A\n"
" +--Site A\n"
" +--Site A\n"
"Where A = ") +
kExpectedSiteURL.spec(),
DepictFrameTree(*root));
EXPECT_EQ(GURL(url::kAboutBlankURL),
root->child_at(0)->child_at(0)->current_url());
// The frame is no longer on the initial empty document.
EXPECT_FALSE(root->child_at(0)->child_at(0)->is_on_initial_empty_document());
}
// Ensure that navigating a subframe to the same URL as its parent twice in a
// row is not blocked by the self-reference check.
// See https://crbug.com/650332.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SelfReferencingSameURLRenavigation) {
StartEmbeddedServer();
GURL first_url(
embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
GURL second_url(first_url.spec() + "#123");
EXPECT_TRUE(NavigateToURL(shell(), first_url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
TestFrameNavigationObserver observer1(child);
EXPECT_TRUE(ExecJs(child, "location.href = '" + second_url.spec() + "';"));
observer1.Wait();
EXPECT_EQ(child->current_url(), second_url);
TestFrameNavigationObserver observer2(child);
// This navigation shouldn't be blocked. Blocking should only occur when more
// than one ancestor has the same URL (excluding fragments), and the
// navigating frame's current URL shouldn't count toward that.
EXPECT_TRUE(ExecJs(child, "location.href = '" + first_url.spec() + "';"));
observer2.Wait();
EXPECT_EQ(child->current_url(), first_url);
}
// Ensures that POST requests bypass self-referential URL checks. See
// https://crbug.com/710008.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SelfReferencingFramesWithPOST) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
GURL child_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_EQ(url, root->current_url());
EXPECT_EQ(child_url, child->current_url());
// Navigate the child frame to the same URL as parent via POST.
std::string script =
"var f = document.createElement('form');\n"
"f.method = 'POST';\n"
"f.action = '/page_with_iframe.html';\n"
"document.body.appendChild(f);\n"
"f.submit();";
{
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(child, script));
observer.Wait();
}
FrameTreeNode* grandchild = child->child_at(0);
EXPECT_EQ(url, child->current_url());
EXPECT_EQ(child_url, grandchild->current_url());
// Now navigate the grandchild to the same URL as its two ancestors. This
// should be allowed since it uses POST; it was blocked prior to
// fixing https://crbug.com/710008.
{
TestFrameNavigationObserver observer(grandchild);
EXPECT_TRUE(ExecJs(grandchild, script));
observer.Wait();
}
EXPECT_EQ(url, grandchild->current_url());
ASSERT_EQ(1U, grandchild->child_count());
EXPECT_EQ(child_url, grandchild->child_at(0)->current_url());
}
// Ensures that we don't reset a speculative RFH if a JavaScript URL is loaded
// while there's an ongoing cross-process navigation. See
// https://crbug.com/793432.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
JavaScriptLoadDoesntResetSpeculativeRFH) {
EXPECT_TRUE(embedded_test_server()->Start());
GURL site1 = embedded_test_server()->GetURL("a.com", "/title1.html");
GURL site2 = embedded_test_server()->GetURL("b.com", "/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), site1));
TestNavigationManager cross_site_navigation(shell()->web_contents(), site2);
shell()->LoadURL(site2);
cross_site_navigation.WaitForSpeculativeRenderFrameHostCreation();
RenderFrameHostImpl* speculative_rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host();
CHECK(speculative_rfh);
shell()->web_contents()->GetController().LoadURL(
GURL("javascript:(0)"), Referrer(), ui::PAGE_TRANSITION_TYPED,
std::string());
ASSERT_TRUE(cross_site_navigation.WaitForNavigationFinished());
// No crash means everything worked!
}
// Test that unrelated browsing contexts cannot find each other's windows,
// even when they end up using the same renderer process (e.g. because of
// hitting a process limit). See also https://crbug.com/718489.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ProcessReuseVsBrowsingInstance) {
// Set max renderers to 1 to force reusing a renderer process between two
// unrelated tabs.
RenderProcessHost::SetMaxRendererProcessCount(1);
// Navigate 2 tabs to a web page (regular web pages can share renderers
// among themselves without any restrictions, unlike extensions, apps, etc.).
ASSERT_TRUE(embedded_test_server()->Start());
GURL url1(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
RenderFrameHost* tab1 = shell()->web_contents()->GetPrimaryMainFrame();
EXPECT_EQ(url1, tab1->GetLastCommittedURL());
GURL url2(embedded_test_server()->GetURL("/title2.html"));
Shell* shell2 = Shell::CreateNewWindow(
shell()->web_contents()->GetBrowserContext(), url2, nullptr, gfx::Size());
EXPECT_TRUE(NavigateToURL(shell2, url2));
RenderFrameHost* tab2 = shell2->web_contents()->GetPrimaryMainFrame();
EXPECT_EQ(url2, tab2->GetLastCommittedURL());
// Sanity-check test setup: 2 frames share a renderer process, but are not in
// a related browsing instance.
if (!AreAllSitesIsolatedForTesting()) {
EXPECT_EQ(tab1->GetProcess(), tab2->GetProcess());
}
EXPECT_FALSE(
tab1->GetSiteInstance()->IsRelatedSiteInstance(tab2->GetSiteInstance()));
// Name the 2 frames.
EXPECT_TRUE(ExecJs(tab1, "window.name = 'tab1';"));
EXPECT_TRUE(ExecJs(tab2, "window.name = 'tab2';"));
// Verify that |tab1| cannot find named frames belonging to |tab2| (i.e. that
// window.open will end up creating a new tab rather than returning the old
// |tab2| tab).
WebContentsAddedObserver new_contents_observer;
EXPECT_EQ(url::kAboutBlankURL, EvalJs(tab1,
"var w = window.open('', 'tab2');\n"
"w.location.href;"));
EXPECT_TRUE(new_contents_observer.GetWebContents());
}
// Verify that cross-site main frame navigations will swap BrowsingInstances
// for certain browser-initiated navigations, such as user typing the URL into
// the address bar. This helps avoid unneeded process sharing and should
// happen even if the current frame has an opener. See
// https://crbug.com/803367.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
BrowserInitiatedNavigationsSwapBrowsingInstance) {
ASSERT_TRUE(embedded_test_server()->Start());
// Start with a page on a.com.
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstance> a_site_instance(
shell()->web_contents()->GetSiteInstance());
// Open a popup for b.com. This should stay in the current BrowsingInstance.
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
Shell* popup = OpenPopup(shell(), b_url, "foo");
EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
scoped_refptr<SiteInstance> b_site_instance(
popup->web_contents()->GetSiteInstance());
EXPECT_TRUE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
// 1. Same-site browser-initiated navigations shouldn't swap BrowsingInstances
// or SiteInstances.
EXPECT_TRUE(NavigateToURL(
popup, embedded_test_server()->GetURL("b.com", "/title2.html")));
EXPECT_EQ(b_site_instance, popup->web_contents()->GetSiteInstance());
// 2. A cross-site browser-initiated navigation should swap BrowsingInstances,
// despite having an opener in the same site as the destination URL.
EXPECT_TRUE(NavigateToURL(
popup, embedded_test_server()->GetURL("a.com", "/title3.html")));
EXPECT_NE(b_site_instance, popup->web_contents()->GetSiteInstance());
EXPECT_NE(a_site_instance, popup->web_contents()->GetSiteInstance());
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(
popup->web_contents()->GetSiteInstance()));
EXPECT_FALSE(b_site_instance->IsRelatedSiteInstance(
popup->web_contents()->GetSiteInstance()));
auto transitions_forcing_browsing_instance_swap = {
ui::PAGE_TRANSITION_AUTO_BOOKMARK, /* user clicking on a bookmark */
ui::PAGE_TRANSITION_GENERATED, /* search query */
ui::PAGE_TRANSITION_KEYWORD, /* search within a site from address bar */
ui::PAGE_TRANSITION_TYPED, /* user typing URL into address bar */
};
auto transitions_not_forcing_browsing_instance_swap = {
ui::PAGE_TRANSITION_LINK, /* user clicked on a link in the document */
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
ui::PAGE_TRANSITION_FORM_SUBMIT,
ui::PAGE_TRANSITION_KEYWORD_GENERATED,
ui::PAGE_TRANSITION_RELOAD,
};
int current_site = 0;
scoped_refptr<SiteInstance> curr_instance(
popup->web_contents()->GetSiteInstance());
// 3. Perform several cross-site browser-initiated navigations in the popup
// that do not force a BrowsingInstance swap.
//
// When ProactivelySwapBrowsingInstance is disabled, the BrowsingInstance
// isn't swapped, even if it could be.
//
// When ProactivelySwapBrowsingInstance is enabled, the BrowsingInstance is
// now swapped. It can be swapped, because (2) caused the popup to live in a
// different BrowsingInstance. The window and the popup are no longer related.
for (auto transition : transitions_not_forcing_browsing_instance_swap) {
GURL cross_site_url(embedded_test_server()->GetURL(
base::StringPrintf("site%d.com", current_site++), "/title1.html"));
SCOPED_TRACE(base::StringPrintf(
"... wrong BrowsingInstance for '%s' transition to %s",
ui::PageTransitionGetCoreTransitionString(transition),
cross_site_url.spec().c_str()));
TestNavigationObserver observer(popup->web_contents());
NavigationController::LoadURLParams params(cross_site_url);
params.transition_type = transition;
popup->web_contents()->GetController().LoadURLWithParams(params);
observer.Wait();
if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
EXPECT_FALSE(curr_instance->IsRelatedSiteInstance(
popup->web_contents()->GetSiteInstance()));
} else {
EXPECT_TRUE(curr_instance->IsRelatedSiteInstance(
popup->web_contents()->GetSiteInstance()));
}
}
// 4. Perform several cross-site browser-initiated navigations in the popup,
// all using page transitions that force BrowsingInstance swaps:
for (auto transition : transitions_forcing_browsing_instance_swap) {
GURL cross_site_url(embedded_test_server()->GetURL(
base::StringPrintf("site%d.com", current_site++), "/title1.html"));
scoped_refptr<SiteInstance> prev_instance(
popup->web_contents()->GetSiteInstance());
SCOPED_TRACE(base::StringPrintf(
"... expected BrowsingInstance swap for '%s' transition to %s",
ui::PageTransitionGetCoreTransitionString(transition),
cross_site_url.spec().c_str()));
TestNavigationObserver observer(popup->web_contents());
NavigationController::LoadURLParams params(cross_site_url);
params.transition_type = transition;
popup->web_contents()->GetController().LoadURLWithParams(params);
observer.Wait();
// This should swap BrowsingInstances.
scoped_refptr<SiteInstance> updated_curr_instance(
popup->web_contents()->GetSiteInstance());
EXPECT_NE(a_site_instance, updated_curr_instance);
EXPECT_FALSE(
a_site_instance->IsRelatedSiteInstance(updated_curr_instance.get()));
EXPECT_NE(prev_instance, updated_curr_instance);
EXPECT_FALSE(
prev_instance->IsRelatedSiteInstance(updated_curr_instance.get()));
}
}
// Verifies that a renderer-initiated navigation from a site in the default
// StoragePartition to one that ContentBrowserClient places in a non-default
// StoragePartition will swap to a new BrowsingInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigationToDifferentPartitionSwapsBrowsingInstance) {
ASSERT_TRUE(embedded_test_server()->Start());
// Set up a ContentBrowserClient that maps b.com to a non-default partition.
CustomStoragePartitionBrowserClient modified_client(GURL("http://b.com/"));
// Load a page on a.com and verify that it uses the default partition.
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), a_url));
RenderFrameHostImpl* rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame();
SiteInstanceImpl* a_site_instance = rfh->GetSiteInstance();
EXPECT_TRUE(
a_site_instance->GetSiteInfo().storage_partition_config().is_default());
// Make sure proactive BrowserInstance swapping doesn't interfere.
rfh->DisableProactiveBrowsingInstanceSwapForTesting();
// Do a renderer-initiated navigation to a page on b.com and verify that we
// swapped BrowsingInstances.
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame();
SiteInstanceImpl* b_site_instance = rfh->GetSiteInstance();
EXPECT_FALSE(
b_site_instance->GetSiteInfo().storage_partition_config().is_default());
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance));
}
// Verifies that iframes inherit their StoragePartition, even if
// ContentBrowserClient would normally place the iframe's URL in a dedicated
// StoragePartition.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
SubframeInheritsStoragePartition) {
ASSERT_TRUE(embedded_test_server()->Start());
// Set up a ContentBrowserClient that maps b.com to a non-default partition.
CustomStoragePartitionBrowserClient modified_client(GURL("http://b.com/"));
// Load a page on b.com to verify that it uses the correct partition.
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), b_url));
RenderFrameHostImpl* rfh =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame();
EXPECT_FALSE(rfh->GetSiteInstance()
->GetSiteInfo()
.storage_partition_config()
.is_default());
// Load a page on a.com that iframes a b.com page.
GURL a_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), a_url));
rfh = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryMainFrame();
SiteInstanceImpl* a_site_instance = rfh->GetSiteInstance();
if (AreStrictSiteInstancesEnabled()) {
EXPECT_EQ("http://a.com/", a_site_instance->GetSiteURL());
} else {
EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
}
EXPECT_TRUE(
a_site_instance->GetSiteInfo().storage_partition_config().is_default());
// Verify that the iframe uses the default StoragePartition.
EXPECT_EQ(1UL, rfh->child_count());
SiteInstanceImpl* b_site_instance = static_cast<SiteInstanceImpl*>(
rfh->child_at(0)->current_frame_host()->GetSiteInstance());
if (AreStrictSiteInstancesEnabled()) {
EXPECT_EQ("http://b.com/", b_site_instance->GetSiteURL());
} else {
EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
}
EXPECT_TRUE(
b_site_instance->GetSiteInfo().storage_partition_config().is_default());
}
// Ensure that these two browser-initiated navigations:
// foo.com -> about:blank -> foo.com
// stay in the same SiteInstance. This isn't technically required for
// correctness, but some tests (e.g., testEnsureHotFromScratch from
// telemetry_unittests) currently depend on this behavior.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigateToAndFromAboutBlank) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
scoped_refptr<SiteInstance> site_instance(
shell()->web_contents()->GetSiteInstance());
// Navigate to about:blank from address bar. This stays in the foo.com
// SiteInstance, unless we do a proactive BrowsingInstance swap due to
// back/forward cache.
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
if (IsBackForwardCacheEnabled()) {
site_instance = shell()->web_contents()->GetSiteInstance();
} else {
EXPECT_EQ(site_instance, shell()->web_contents()->GetSiteInstance());
}
// Perform a browser-initiated navigation to foo.com. This should also stay
// in the original foo.com SiteInstance and BrowsingInstance.
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
EXPECT_EQ(site_instance, shell()->web_contents()->GetSiteInstance());
}
// Check that with the following sequence of navigations:
// foo.com -(1)-> bar.com -(2)-> about:blank -(3)-> foo.com
// where (1) is renderer-initiated and (2)+(3) are browser-initiated, the last
// navigation goes back to the first SiteInstance without --site-per-process,
// and to a new SiteInstance and BrowsingInstance with --site-per-process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigateToFooThenBarThenAboutBlankThenFoo) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
scoped_refptr<SiteInstance> site_instance(
shell()->web_contents()->GetSiteInstance());
// Do a renderer-initiated navigation to bar.com, then navigate to
// about:blank from address bar, then back to foo.com.
GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), bar_url));
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
// This should again go back to the original foo.com SiteInstance without
// --site-per-process, as in that case both the bar.com and
// about:blank navigation will stay in the foo.com SiteInstance, and the
// final navigation to foo.com will be considered same-site with the current
// SiteInstance.
//
// With --site-per-process, bar.com should get its own SiteInstance, the
// about:blank navigation will stay in it, and thus the final foo.com
// navigation should be considered cross-site from the current SiteInstance.
// Since this is a browser-initiated, cross-site navigation, it will swap
// BrowsingInstances, and create a new foo.com SiteInstance, distinct from
// the initial one.
if (ExpectSameSiteInstance()) {
EXPECT_EQ(site_instance, shell()->web_contents()->GetSiteInstance());
} else {
EXPECT_NE(site_instance, shell()->web_contents()->GetSiteInstance());
EXPECT_FALSE(site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetSiteInstance()));
EXPECT_EQ(site_instance->GetSiteURL(),
shell()->web_contents()->GetSiteInstance()->GetSiteURL());
}
}
// Test to verify that navigations in the main frame, which result in an error
// page, properly commit the error page in its own dedicated process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationInMainFrame) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/title1.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
// Start with a successful navigation to a document.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
// Browser-initiated navigation to an error page should result in changing the
// SiteInstance and process.
{
NavigationHandleObserver observer(shell()->web_contents(), error_url);
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_TRUE(observer.is_error());
EXPECT_EQ(net::ERR_DNS_TIMED_OUT, observer.net_error_code());
scoped_refptr<SiteInstance> error_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_NE(success_site_instance, error_site_instance);
if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
// When ProactivelySwapBrowsingInstance is enabled on same-site
// navigations, the navigation above will result in a new
// BrowsingInstance.
EXPECT_FALSE(success_site_instance->IsRelatedSiteInstance(
error_site_instance.get()));
} else {
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
error_site_instance.get()));
}
EXPECT_NE(success_site_instance->GetOrCreateProcessForTesting()
->GetDeprecatedID(),
error_site_instance->GetProcess()->GetDeprecatedID());
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
// Verify that the error page process is locked to origin
EXPECT_TRUE(HasErrorPageProcessLock(error_site_instance.get()));
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
}
// Navigate successfully again to a document, then perform a
// renderer-initiated navigation and verify it behaves the same way.
EXPECT_TRUE(NavigateToURL(shell(), url));
success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_FALSE(HasErrorPageProcessLock(success_site_instance.get()));
{
NavigationHandleObserver observer(shell()->web_contents(), error_url);
TestFrameNavigationObserver frame_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell()->web_contents(),
"location.href = '" + error_url.spec() + "';"));
frame_observer.Wait();
EXPECT_TRUE(observer.is_error());
EXPECT_EQ(net::ERR_DNS_TIMED_OUT, observer.net_error_code());
scoped_refptr<SiteInstance> error_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_NE(success_site_instance, error_site_instance);
if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
// When ProactivelySwapBrowsingInstance is enabled on same-site
// navigations, the navigation above will result in a new
// BrowsingInstance.
EXPECT_FALSE(success_site_instance->IsRelatedSiteInstance(
error_site_instance.get()));
} else {
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
error_site_instance.get()));
}
EXPECT_NE(success_site_instance->GetOrCreateProcessForTesting()
->GetDeprecatedID(),
error_site_instance->GetProcess()->GetDeprecatedID());
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
// Verify that the error page process is locked to origin
EXPECT_TRUE(HasErrorPageProcessLock(error_site_instance.get()));
}
}
// Test to verify that navigations in subframes, which result in an error
// page, commit the error page in the same process and not in the dedicated
// error page process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationInChildFrame) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
// Start with a successful navigation to a document.
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
FrameTreeNode* child = root->child_at(0);
scoped_refptr<SiteInstance> success_site_instance =
child->current_frame_host()->GetSiteInstance();
NavigationHandleObserver observer(web_contents, error_url);
TestFrameNavigationObserver frame_observer(child);
EXPECT_TRUE(ExecJs(child, "location.href = '" + error_url.spec() + "';"));
frame_observer.Wait();
EXPECT_TRUE(observer.is_error());
EXPECT_EQ(net::ERR_DNS_TIMED_OUT, observer.net_error_code());
scoped_refptr<SiteInstance> error_site_instance =
child->current_frame_host()->GetSiteInstance();
EXPECT_TRUE(IsExpectedSubframeErrorTransition(success_site_instance.get(),
error_site_instance.get()));
EXPECT_TRUE(IsOriginOpaqueAndCompatibleWithURL(child, error_url));
}
// Test to verify that navigations in new window, which result in an error
// page, commit the error page in the dedicated error page process and not in
// the one for the destination site.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationInNewWindow) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
// Start with a successful navigation to a document.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
scoped_refptr<SiteInstance> main_site_instance =
root->current_frame_host()->GetSiteInstance();
Shell* new_shell = OpenPopup(shell(), error_url, "foo");
EXPECT_TRUE(new_shell);
scoped_refptr<SiteInstance> error_site_instance =
new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_NE(main_site_instance, error_site_instance);
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
// Verify that the error page process is locked to origin
EXPECT_TRUE(HasErrorPageProcessLock(error_site_instance.get()));
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(new_shell, error_url));
}
// Test to verify that windows that are not part of the same
// BrowsingInstance end up using the same error page process, even though
// their SiteInstances are not related.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationInUnrelatedWindows) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
// Navigate the main window to an error page and verify.
{
NavigationHandleObserver observer(shell()->web_contents(), error_url);
EXPECT_FALSE(NavigateToURL(shell(), error_url));
scoped_refptr<SiteInstance> error_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(observer.is_error());
EXPECT_EQ(net::ERR_DNS_TIMED_OUT, observer.net_error_code());
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
}
// Creat a new, unrelated, window, navigate it to an error page and
// verify.
Shell* new_shell = CreateBrowser();
{
NavigationHandleObserver observer(new_shell->web_contents(), error_url);
EXPECT_FALSE(NavigateToURL(new_shell, error_url));
scoped_refptr<SiteInstance> error_site_instance =
new_shell->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(observer.is_error());
EXPECT_EQ(net::ERR_DNS_TIMED_OUT, observer.net_error_code());
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(new_shell, error_url));
}
// Verify the two SiteInstanes are not related, but they end up using the
// same underlying RenderProcessHost.
EXPECT_FALSE(
shell()->web_contents()->GetSiteInstance()->IsRelatedSiteInstance(
new_shell->web_contents()->GetSiteInstance()));
EXPECT_EQ(shell()->web_contents()->GetSiteInstance()->GetProcess(),
new_shell->web_contents()->GetSiteInstance()->GetProcess());
// Verify that the process is locked to origin
EXPECT_TRUE(
HasErrorPageProcessLock(shell()->web_contents()->GetSiteInstance()));
}
// Test to verify that reloading an error page once the error condition has
// cleared up is successful and does not create a new navigation entry.
// See https://crbug.com/840485.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest, ErrorPageNavigationReload) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
GURL end_url(embedded_test_server()->GetURL("/title2.html"));
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Build session history with three entries, where the middle one will be
// tested for successful and failed reloads. This allows checking whether
// reload accidentally clears the forward session history if it is
// incorrectly classified.
EXPECT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_TRUE(NavigateToURL(shell(), error_url));
EXPECT_TRUE(NavigateToURL(shell(), end_url));
{
TestNavigationObserver back_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_observer.Wait();
EXPECT_TRUE(back_observer.last_navigation_succeeded());
}
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_EQ(error_url, shell()->web_contents()->GetLastCommittedURL());
scoped_refptr<SiteInstance> success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
url::Origin expected_origin =
shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin();
EXPECT_EQ(url::Origin::Create(error_url), expected_origin);
// Install an interceptor which will cause network failure for |error_url|,
// reload the existing entry and verify.
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
{
TestNavigationObserverInternal reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
// TODO(nasko): Investigate making a failing reload of a successful
// navigation be classified as NEW_ENTRY instead, since with error page
// isolation it involves a SiteInstance swap.
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
reload_observer.last_navigation_type());
}
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
int process_id = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID();
EXPECT_TRUE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
// Reload while it will still fail to ensure it stays in the same process.
{
TestNavigationObserverInternal reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
reload_observer.last_navigation_type());
}
EXPECT_EQ(process_id, shell()
->web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID());
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
// Reload the error page after clearing the error condition, such that the
// navigation is successful and verify that no new entry was added to
// session history and forward history is not pruned.
url_interceptor.reset();
{
TestNavigationObserverInternal reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
// The successful reload should be classified as a NEW_ENTRY navigation
// with replacement, since it needs to stay at the same entry in session
// history, but needs a new entry because of the change in SiteInstance.
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
reload_observer.last_navigation_type());
}
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_FALSE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_EQ(
expected_origin,
shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Test the same scenario as above, but this time initiated by the
// renderer process.
url_interceptor = SetupRequestFailForURL(error_url);
{
TestNavigationObserverInternal reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
// TODO(nasko): Investigate making a failing reload of a successful
// navigation be classified as NEW_ENTRY instead, since with error page
// isolation it involves a SiteInstance swap.
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
reload_observer.last_navigation_type());
}
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_TRUE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
url_interceptor.reset();
{
TestNavigationObserverInternal reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
// TODO(nasko): Investigate making renderer initiated reloads that change
// SiteInstance be classified as NEW_ENTRY as well.
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
reload_observer.last_navigation_type());
}
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_FALSE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_EQ(
expected_origin,
shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
}
// Version of ErrorPageNavigationReload test that targets a subframe (because
// subframes are currently [~2019Q1] not subject to error page isolation).
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationReload_InSubframe_NetworkError) {
StartEmbeddedServer();
// Isolating a.com helps more robustly exercise platforms without strict
// site isolation - we want to ensure that enforcing |initiator_origin| in
// BeginNavigation is compatible with process locks, even when only one of
// the frames requires isolation.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"b.com"});
// Start on a page with a main frame and a subframe.
GURL page_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL test_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), page_url));
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// We start at 3, because IsolateOriginsForTesting is sneeking in extra two
// navigations.
EXPECT_EQ(3, nav_controller.GetEntryCount());
// Grab child's site URL.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child = root->child_at(0);
GURL child_site_url =
child->current_frame_host()->GetSiteInstance()->GetSiteURL();
// Navigate the subframe to a URL that is cross-site from the main frame.
{
TestNavigationObserver nav_observer(shell()->web_contents());
ASSERT_TRUE(ExecJs(child, JsReplace("window.location = $1", test_url)));
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(4, nav_controller.GetEntryCount());
EXPECT_EQ(test_url, child->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(test_url),
child->current_frame_host()->GetLastCommittedOrigin());
EXPECT_EQ(child_site_url,
child->current_frame_host()->GetSiteInstance()->GetSiteURL());
}
// Reload the subframe while the network is down.
{
scoped_refptr<SiteInstance> success_site_instance =
child->current_frame_host()->GetSiteInstance();
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(test_url);
TestNavigationObserver nav_observer(shell()->web_contents());
ASSERT_TRUE(ExecJs(child, "window.location.reload()"));
nav_observer.Wait();
EXPECT_FALSE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(4, nav_controller.GetEntryCount());
EXPECT_EQ(test_url, child->current_frame_host()->GetLastCommittedURL());
// Error pages should commit in an opaque origin.
EXPECT_TRUE(IsOriginOpaqueAndCompatibleWithURL(child, test_url));
EXPECT_TRUE(IsExpectedSubframeErrorTransition(
success_site_instance.get(),
child->current_frame_host()->GetSiteInstance()));
}
// Reload the subframe after the network is restored.
{
TestNavigationObserver nav_observer(shell()->web_contents());
// Note that some error pages actually do embed a button that will do an
// equivalent of the renderer-initiated reload that is triggered below.
ASSERT_TRUE(ExecJs(child, "window.location.reload()"));
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(4, nav_controller.GetEntryCount());
EXPECT_EQ(test_url, child->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(test_url),
child->current_frame_host()->GetLastCommittedOrigin());
EXPECT_EQ(child_site_url,
child->current_frame_host()->GetSiteInstance()->GetSiteURL());
}
}
// Version of ErrorPageNavigationReload test that targets a subframe (because
// subframes are currently [~2019Q1] not subject to error page isolation).
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationReload_InSubframe_BlockedByClient) {
StartEmbeddedServer();
// Isolating a.com and b.com helps more robustly exercise platforms without
// strict site isolation - we want to ensure that enforcing |initiator_origin|
// in BeginNavigation is compatible with process locks, even when only some of
// the frames requires isolation.
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
std::vector<std::string>{"a.com", "b.com"});
// Start on a page with a same-site main frame and a subframe.
GURL page_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,b)"));
GURL test_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), page_url));
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// We start at 3, because IsolateOriginsForTesting calls are sneeking in extra
// two navigations.
EXPECT_EQ(3, nav_controller.GetEntryCount());
// Grab child's site URL.
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child1 = root->child_at(0);
FrameTreeNode* child2 = root->child_at(1);
GURL a_site_url = root->current_frame_host()->GetSiteInstance()->GetSiteURL();
EXPECT_EQ("a.com", a_site_url.host());
GURL b_site_url =
child2->current_frame_host()->GetSiteInstance()->GetSiteURL();
EXPECT_EQ("b.com", b_site_url.host());
// Navigate the subframe to a cross-site URL, while blocking the request with
// ERR_BLOCKED_BY_CLIENT.
{
// Name the first child, so it can be found and navigated by the other child
// in the next test step below.
ASSERT_TRUE(ExecJs(child1, "window.name = 'child1';"));
// Have |child2| initiate navigation of |child1| - the navigation should
// result in ERR_BLOCKED_BY_CLIENT (because of the throttle created below).
content::TestNavigationThrottleInserter throttle_inserter(
shell()->web_contents(),
base::BindRepeating(&RequestBlockingNavigationThrottle::Create));
const char kScriptTemplate[] = R"(
var child1 = window.open('', 'child1');
child1.location = $1;
)";
scoped_refptr<SiteInstance> initial_site_instance =
child2->current_frame_host()->GetSiteInstance();
TestNavigationObserver nav_observer(shell()->web_contents());
ASSERT_TRUE(ExecJs(child2, JsReplace(kScriptTemplate, test_url)));
nav_observer.Wait();
EXPECT_FALSE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(4, nav_controller.GetEntryCount());
EXPECT_EQ(test_url, child1->current_frame_host()->GetLastCommittedURL());
// Error pages should commit in an opaque origin.
EXPECT_TRUE(IsOriginOpaqueAndCompatibleWithURL(child1, test_url));
// net::ERR_BLOCKED_BY_CLIENT errors in subframes should commit in the
// the correct process based on whether isolation is enabled or not.
EXPECT_TRUE(IsExpectedSubframeErrorTransition(
initial_site_instance.get(),
child1->current_frame_host()->GetSiteInstance()));
}
// Reload the subframe when no longer blocking the navigation.
{
TestNavigationObserver nav_observer(shell()->web_contents());
// Note that some error pages actually do embed a button that will do an
// equivalent of the renderer-initiated reload that is triggered below.
ASSERT_TRUE(ExecJs(child1, "window.location.reload()"));
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(4, nav_controller.GetEntryCount());
EXPECT_EQ(test_url, child1->current_frame_host()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(test_url),
child1->current_frame_host()->GetLastCommittedOrigin());
SiteInstanceImpl* child1_site_instance =
child1->current_frame_host()->GetSiteInstance();
GURL c_site_url = child1_site_instance->GetSiteURL();
if (AreAllSitesIsolatedForTesting()) {
EXPECT_EQ("c.com", c_site_url.host());
EXPECT_EQ(test_url.host(), c_site_url.host());
} else if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(
child1_site_instance->group(),
child1_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(child1_site_instance->IsDefaultSiteInstance());
}
EXPECT_NE(a_site_url, c_site_url);
EXPECT_NE(b_site_url, c_site_url);
}
}
// Make sure that reload works properly if it redirects to a different site than
// the initial navigation. The initial purpose of this test was to make sure
// the corresponding unit test matches the actual product code behavior
// (e.g. see NavigationControllerTest.Reload_GeneratesNewPage).
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ReloadRedirectsToDifferentCrossSitePage) {
// Set-up http server handlers for |start_url|.
//
// |response1| is only required to make sure that |response2| doesn't kick-in
// and intercept the http request until step 4. Note that step 3 won't hit
// the http server and therefore doesn't require handling here.
auto response1 = std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), "/title1.html");
auto response2 = std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), "/title1.html");
// Start the server after all the required handlers have been already
// registered.
StartEmbeddedServer();
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// URLs used in the test:
// - Step1: Navigate to |start_url|
// - Step2: Navigate to |second_url|
// - Step3: Go back (to |start_url|)
// - Step4: Reload (redirects to a different destination - |redirect_url|).
GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
GURL second_url(embedded_test_server()->GetURL("foo.com", "/title3.html"));
GURL redirect_url(embedded_test_server()->GetURL("bar.com", "/title2.html"));
// Test Step 1: Simple navigation that won't redirect and will just
// successfully commit http://foo.com/title1.html.
{
// Navigate...
TestNavigationObserver nav_observer(shell()->web_contents());
shell()->LoadURL(start_url);
response1->WaitForRequest();
response1->Send(net::HTTP_OK, "text/html; charset=utf-8", "<p>Blah</p>");
response1->Done();
nav_observer.Wait();
// Verify...
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(1, nav_controller.GetEntryCount());
EXPECT_EQ(0, nav_controller.GetLastCommittedEntryIndex());
EXPECT_EQ(start_url, nav_controller.GetLastCommittedEntry()->GetURL());
}
// Test Step 2: Simple navigation that won't redirect and will just
// successfully commit http://foo.com/title3.html.
{
// Navigate...
EXPECT_TRUE(NavigateToURL(shell(), second_url));
// Verify...
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_EQ(second_url, nav_controller.GetLastCommittedEntry()->GetURL());
}
// Test Step 3: Go back (to |start_url|).
{
// Navigate...
TestNavigationObserver nav_observer(shell()->web_contents());
nav_controller.GoBack();
nav_observer.Wait();
// Verify...
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(0, nav_controller.GetLastCommittedEntryIndex());
EXPECT_EQ(start_url, nav_controller.GetLastCommittedEntry()->GetURL());
}
// Test Step 4: Reload that will redirect to http://bar.com/title2.html (which
// is a different, cross-site location compared to what the initial navigation
// committed).
{
// Navigate...
TestNavigationObserverInternal reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
response2->WaitForRequest();
response2->Send("HTTP/1.1 302 Moved Temporarily\n");
response2->Send("Location: " + redirect_url.spec());
response2->Done();
reload_observer.Wait();
// Verify that reload 1) replaced the current entry (rather than creating a
// new entry) and 2) the tail of the history (e.g. the |second_url|
// navigation) was preserved rather than truncated.
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(0, nav_controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirect_url, nav_controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(second_url, nav_controller.GetEntryAtIndex(1)->GetURL());
if (AreAllSitesIsolatedForTesting()) {
// The successful reload should be classified as a NEW_ENTRY navigation
// with replacement, since it needs to stay at the same entry in session
// history, but needs a new entry because of the change in SiteInstance.
// (the same as expectations in the ErrorPageNavigationReload test above).
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
reload_observer.last_navigation_type());
} else {
EXPECT_EQ(NavigationType::NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
reload_observer.last_navigation_type());
}
}
}
// Test to verify that navigating away from an error page results in correct
// change in SiteInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationAfterError) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/title1.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Start with a successful navigation to a document.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(1, nav_controller.GetEntryCount());
// Navigate to an url resulting in an error page.
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_TRUE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
// Navigate again to the initial successful document, expecting a new
// navigation and new SiteInstance. A new SiteInstance is expected here
// because we are doing a cross-site navigation from an error page
// to a site for |url|. This triggers the creation of a new BrowsingInstance
// and therefore a new SiteInstance.
EXPECT_TRUE(NavigateToURL(shell(), url));
SiteInstanceImpl* site_instance = static_cast<SiteInstanceImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(HasErrorPageSiteInfo(site_instance));
// Verify that we get the default SiteInstance or SiteInstanceGroup because
// the original URL does not require a dedicated process.
if (!AreAllSitesIsolatedForTesting()) {
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(site_instance->group(),
site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(site_instance->IsDefaultSiteInstance());
}
}
EXPECT_EQ(success_site_instance->GetSiteURL(), site_instance->GetSiteURL());
EXPECT_NE(success_site_instance, site_instance);
EXPECT_EQ(3, nav_controller.GetEntryCount());
// Repeat again using a renderer-initiated navigation for the successful one.
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_TRUE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_EQ(4, nav_controller.GetEntryCount());
{
TestNavigationObserver observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.href = '" + url.spec() + "';"));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
}
EXPECT_EQ(5, nav_controller.GetEntryCount());
EXPECT_FALSE(HasErrorPageSiteInfo(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
}
// Test to verify that when an error page is hit and its process is terminated,
// a successful reload correctly commits in a different process.
// See https://crbug.com/866549.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationReloadWithTerminatedProcess) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/title1.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
WebContents* web_contents = shell()->web_contents();
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(web_contents->GetController());
// Start with a successful navigation to a document.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> success_site_instance =
web_contents->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(1, nav_controller.GetEntryCount());
// Navigate to an url resulting in an error page.
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_TRUE(HasErrorPageSiteInfo(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
// Terminate the renderer process.
{
RenderProcessHostWatcher termination_observer(
web_contents->GetPrimaryMainFrame()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
web_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown(0);
termination_observer.Wait();
}
// Clear the interceptor so the navigation will succeed on reload.
url_interceptor.reset();
// Reload the URL and execute a Javascript statement to verify that the
// renderer process is still around and responsive.
TestNavigationObserver reload_observer(shell()->web_contents());
nav_controller.Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(error_url.spec(), EvalJs(shell(), "location.href;"));
}
// Test to verify that navigation to existing history entry, which results in
// an error page, is correctly placed in the error page SiteInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationHistoryNavigationFailure) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
// Perform successful navigations to two URLs to establish session history.
GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url2));
WebContents* web_contents = shell()->web_contents();
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(web_contents->GetController());
// There should be two NavigationEntries.
EXPECT_EQ(2, nav_controller.GetEntryCount());
// Ensure that previous document won't be restored from the BackForwardCache,
// to force a network fetch, which would result in a network error.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
// Create an interceptor to cause navigations to url1 to fail and go back
// in session history.
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(url1);
TestNavigationObserver back_observer(web_contents);
nav_controller.GoBack();
back_observer.Wait();
EXPECT_FALSE(back_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(0, nav_controller.GetLastCommittedEntryIndex());
EXPECT_TRUE(HasErrorPageSiteInfo(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), url1));
}
// Test to verify that a successful navigation to existing history entry,
// which initially resulted in an error page, is correctly placed in a
// SiteInstance different than the error page one.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationHistoryNavigationSuccess) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
StartEmbeddedServer();
WebContents* web_contents = shell()->web_contents();
// Start with a successful navigation.
GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
// Navigate to URL that results in an error page and verify its SiteInstance.
GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(url2);
EXPECT_FALSE(NavigateToURL(shell(), url2));
EXPECT_TRUE(HasErrorPageSiteInfo(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(HasErrorPageProcessLock(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), url2));
// There should be two NavigationEntries.
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(web_contents->GetController());
EXPECT_EQ(2, nav_controller.GetEntryCount());
// Navigate once more to create another session history entry.
GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), url3));
EXPECT_EQ(3, nav_controller.GetEntryCount());
// Navigate back, this time remove the interceptor so the navigation will
// succeed.
url_interceptor.reset();
TestNavigationObserver back_observer(web_contents);
nav_controller.GoBack();
back_observer.Wait();
EXPECT_TRUE(back_observer.last_navigation_succeeded());
EXPECT_EQ(3, nav_controller.GetEntryCount());
EXPECT_EQ(1, nav_controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(HasErrorPageSiteInfo(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_FALSE(HasErrorPageProcessLock(
web_contents->GetPrimaryMainFrame()->GetSiteInstance()));
}
// Test to verify that navigations to WebUI URL which results in an error
// commits properly in the error page process and does not give it WebUI
// bindings.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationToWebUIResourceWithError) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
GURL webui_url = GetWebUIURL(kChromeUIGpuHost);
GURL error_url(webui_url.Resolve("/foo"));
// Navigate to the main WebUI URL and ensure it is successful.
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
// Ensure that the subsequent navigation is blocked, resulting in an
// error.
TestNavigationThrottleInserter throttle_inserter(
shell()->web_contents(),
base::BindRepeating(&RequestBlockingNavigationThrottle::Create));
// Navigate to a WebUI URL and verify the resulting error page process does
// not get WebUI bindings.
NavigationHandleObserver observer(shell()->web_contents(), error_url);
EXPECT_FALSE(NavigateToURL(shell(), error_url));
scoped_refptr<SiteInstance> error_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(observer.is_error());
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
EXPECT_TRUE(HasErrorPageProcessLock(error_site_instance.get()));
EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings(
error_site_instance->GetProcess()->GetDeprecatedID()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
}
// If a WebUI page leads to an error page and is then reloaded successfully from
// its NavigationEntry, ensure that WebUI bindings are granted and that we don't
// crash. See https://crbug.com/1046159.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ReloadWebUIErrorPageToValidWebUI) {
// This test is only valid if error page isolation is enabled.
if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
return;
}
GURL webui_url = GetWebUIURL(kChromeUIGpuHost);
// Temporarily insert throttles that will block all navigations, leading to
// error pages instead.
{
TestNavigationThrottleInserter throttle_inserter(
shell()->web_contents(),
base::BindRepeating(&RequestBlockingNavigationThrottle::Create));
// Navigate to a WebUI URL and verify the resulting error page process does
// not get WebUI bindings.
NavigationHandleObserver observer(shell()->web_contents(), webui_url);
EXPECT_FALSE(NavigateToURL(shell(), webui_url));
EXPECT_TRUE(observer.is_error());
scoped_refptr<SiteInstance> error_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(HasErrorPageSiteInfo(error_site_instance.get()));
EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings(
error_site_instance->GetProcess()->GetDeprecatedID()));
}
// Once the throttles are no longer inserted into each navigation, reloading
// the NavigationEntry should succeed and grant WebUI bindings.
{
TestNavigationObserver reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
}
scoped_refptr<SiteInstance> webui_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(webui_url, webui_site_instance->GetSiteURL());
EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings(
webui_site_instance->GetProcess()->GetDeprecatedID()));
// A second reload should work without crashing the browser process.
{
TestNavigationObserver reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
}
}
// A custom ContentBrowserClient that simulates GetEffectiveURL() translation
// for all URLs that are in the same page (including URL with refs).
class PageEffectiveURLContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
PageEffectiveURLContentBrowserClient(const GURL& url_to_modify,
const GURL& url_to_return)
: url_to_modify_(url_to_modify), url_to_return_(url_to_return) {}
PageEffectiveURLContentBrowserClient(
const PageEffectiveURLContentBrowserClient&) = delete;
PageEffectiveURLContentBrowserClient& operator=(
const PageEffectiveURLContentBrowserClient&) = delete;
~PageEffectiveURLContentBrowserClient() override = default;
private:
GURL GetEffectiveURL(BrowserContext* browser_context,
const GURL& url) override {
if (url.EqualsIgnoringRef(url_to_modify_)) {
return url_to_return_;
}
return url;
}
GURL url_to_modify_;
GURL url_to_return_;
};
// Ensure that same-document navigations for URLs with effective URLs don't
// incorrectly swap BrowsingInstance. See crbug.com/1073540.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigationToSameDocumentWithEffectiveURL) {
StartEmbeddedServer();
const GURL page_url(embedded_test_server()->GetURL("/title1.html"));
const GURL anchor_in_page_url(
embedded_test_server()->GetURL("/title1.html#bar"));
const GURL effective_url("http://foo.com");
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
// The effective URL for |page_url| and |anchor_in_page_url| will be
// |effective_url|.
PageEffectiveURLContentBrowserClient modified_client(page_url, effective_url);
// Make a navigation to |page_url|.
EXPECT_TRUE(NavigateToURL(shell(), page_url));
EXPECT_EQ(web_contents->GetLastCommittedURL(), page_url);
scoped_refptr<SiteInstance> orig_site_instance =
web_contents->GetPrimaryMainFrame()->GetSiteInstance();
// Navigate to #bar in the same document.
EXPECT_TRUE(NavigateToURL(shell(), anchor_in_page_url));
EXPECT_EQ(web_contents->GetLastCommittedURL(), anchor_in_page_url);
// We should reuse the same SiteInstance.
EXPECT_EQ(orig_site_instance,
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
}
// A test ContentBrowserClient implementation which enforces
// BrowsingInstance swap on every navigation. It is used to verify that
// reloading of an error page to an URL that requires BrowsingInstance swap
// works correctly.
class BrowsingInstanceSwapContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
BrowsingInstanceSwapContentBrowserClient() = default;
BrowsingInstanceSwapContentBrowserClient(
const BrowsingInstanceSwapContentBrowserClient&) = delete;
BrowsingInstanceSwapContentBrowserClient& operator=(
const BrowsingInstanceSwapContentBrowserClient&) = delete;
bool ShouldIsolateErrorPage(bool in_main_frame) override {
return in_main_frame;
}
bool ShouldSwapBrowsingInstancesForNavigation(
content::SiteInstance* site_instance,
const GURL& current_effective_url,
const GURL& destination_effective_url) override {
return true;
}
};
// Test to verify that reloading of an error page which resulted from a
// navigation to an URL which requires a BrowsingInstance swap, correcly
// reloads in the same SiteInstance for the error page.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ErrorPageNavigationReloadBrowsingInstanceSwap) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL error_url(embedded_test_server()->GetURL("b.com", "/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Start with a successful navigation to a document and verify there is
// only one entry in session history.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(1, nav_controller.GetEntryCount());
// Navigate to an url resulting in an error page and ensure a new entry
// was added to session history.
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_EQ(2, nav_controller.GetEntryCount());
scoped_refptr<SiteInstance> initial_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(HasErrorPageSiteInfo(initial_instance.get()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
EXPECT_FALSE(
success_site_instance->IsRelatedSiteInstance(initial_instance.get()));
} else {
EXPECT_TRUE(
success_site_instance->IsRelatedSiteInstance(initial_instance.get()));
}
// Reload of the error page that still results in an error should stay in
// the same SiteInstance. Ensure this works for both browser-initiated
// reloads and renderer-initiated ones.
{
TestNavigationObserver reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(
initial_instance,
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
}
{
TestNavigationObserver reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_EQ(
initial_instance,
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
}
// Install a client forcing every navigation to swap BrowsingInstances.
BrowsingInstanceSwapContentBrowserClient content_browser_client;
// Allow the navigation to succeed and ensure it swapped to a non-related
// SiteInstance.
url_interceptor.reset();
{
TestNavigationObserver reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_FALSE(initial_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
EXPECT_FALSE(success_site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
}
}
// Helper class to simplify testing of unload handlers. It allows waiting for
// particular HTTP requests to be made to the embedded_test_server(); the tests
// use this to wait for termination pings (e.g., navigator.sendBeacon()) made
// from unload handlers.
class RenderFrameHostManagerUnloadBrowserTest
: public RenderFrameHostManagerTest {
public:
RenderFrameHostManagerUnloadBrowserTest() = default;
RenderFrameHostManagerUnloadBrowserTest(
const RenderFrameHostManagerUnloadBrowserTest&) = delete;
RenderFrameHostManagerUnloadBrowserTest& operator=(
const RenderFrameHostManagerUnloadBrowserTest&) = delete;
// Starts monitoring requests made to the embedded_test_server() looking for
// one made to |url|. To be used together with WaitForMonitoredRequest().
void StartMonitoringRequestsFor(const GURL& url) {
base::AutoLock lock(lock_);
request_url_ = url;
saw_request_url_ = false;
}
// Waits for a request to a URL set earlier via StartMonitoringRequestsFor().
// Returns right away if that request was already made.
void WaitForMonitoredRequest() {
base::AutoLock lock(lock_);
if (saw_request_url_) {
return;
}
run_loop_ = std::make_unique<base::RunLoop>();
{
base::RunLoop* run_loop = run_loop_.get();
base::AutoUnlock unlock(lock_);
run_loop->Run();
}
run_loop_.reset();
}
// Returns the body of the monitored request if it was a POST.
const std::string& GetRequestContent() {
base::AutoLock lock(lock_);
return request_content_;
}
blink::mojom::SuddenTerminationDisablerType DisablerTypeForEvent(
const std::string& event_name) {
if (event_name == "unload") {
return blink::mojom::SuddenTerminationDisablerType::kUnloadHandler;
}
if (event_name == "beforeunload") {
return blink::mojom::SuddenTerminationDisablerType::kBeforeUnloadHandler;
}
if (event_name == "pagehide") {
return blink::mojom::SuddenTerminationDisablerType::kPageHideHandler;
}
if (event_name == "visibilitychange") {
return blink::mojom::SuddenTerminationDisablerType::
kVisibilityChangeHandler;
}
NOTREACHED();
}
// Returns the list of event targets that the given `event_name` should be
// registered on.
// Since the `visibilitychange` event is fired at the document and it
// may bubble up to the window, we should test the cases where the event
// listener is registered on both the document and the window.
std::vector<std::string> EventTargetsForEvent(const std::string& event_name) {
if (event_name == "unload" || event_name == "beforeunload" ||
event_name == "pagehide") {
return {"window"};
}
if (event_name == "visibilitychange") {
return {"window", "document"};
}
NOTREACHED();
}
// Adds an unload event handler (can be for the unload, pagehide, or
// visibilitychange event) to |rfh| and verifies that the unload state
// bookkeeping on |rfh| is updated properly.
void AddUnloadEventHandler(RenderFrameHostImpl* rfh,
const std::string& event_name,
const std::string& event_target,
const std::string& script) {
EXPECT_THAT(event_name,
testing::AnyOf(testing::Eq("unload"), testing::Eq("pagehide"),
testing::Eq("visibilitychange")));
EXPECT_FALSE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
EXPECT_FALSE(rfh->has_unload_handlers());
EXPECT_TRUE(ExecJs(
rfh, base::StringPrintf("%s.addEventListener('%s', (e) => { %s });",
event_target.c_str(), event_name.c_str(),
script.c_str())));
EXPECT_TRUE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
EXPECT_TRUE(rfh->has_unload_handlers());
}
// Extend the timeout for keeping the subframe process alive for unload
// processing to prevent any test flakiness. This is the time that the ping
// request will have to make it from the renderer to the test server.
void ExtendSubframeUnloadTimeoutForTerminationPing(RenderFrameHostImpl* rfh) {
rfh->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
}
void SetUpCommandLine(base::CommandLine* command_line) override {
RenderFrameHostManagerTest::SetUpCommandLine(command_line);
feature_list_.InitAndDisableFeature(network::features::kDeprecateUnload);
}
protected:
void SetUpOnMainThread() override {
// Request interceptor needs to be installed before the test server is
// started.
embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
&RenderFrameHostManagerUnloadBrowserTest::MonitorResourceRequest,
base::Unretained(this)));
RenderFrameHostManagerTest::SetUpOnMainThread();
}
private:
void MonitorResourceRequest(const net::test_server::HttpRequest& request) {
// |request.GetURL()| gives us the URL after it's already resolved to
// 127.0.0.1, so reconstruct the requested host via the Host header (which
// includes host+port).
GURL requested_url = request.GetURL();
auto it = request.headers.find("Host");
if (it != request.headers.end()) {
requested_url = GURL("http://" + it->second + request.relative_url);
}
base::AutoLock lock(lock_);
if (!saw_request_url_ && request_url_ == requested_url) {
saw_request_url_ = true;
request_content_ = request.content;
if (run_loop_) {
run_loop_->Quit();
}
}
}
base::Lock lock_;
GURL request_url_ GUARDED_BY(lock_);
std::string request_content_ GUARDED_BY(lock_);
bool saw_request_url_ GUARDED_BY(lock_) = false;
std::unique_ptr<base::RunLoop> run_loop_ GUARDED_BY(lock_);
base::test::ScopedFeatureList feature_list_;
};
// Ensure that after a main frame with a cross-site iframe is itself navigated
// cross-site, the unload event handlers (for unload, pagehide, or
// visibilitychange events) in the iframe can use navigator.sendBeacon() to do a
// termination ping. See https://crbug.com/852204, where this was broken with
// site isolation if the iframe was in its own process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
SubframeTerminationPing_SendBeacon) {
StartEmbeddedServer();
// See BackForwardCache::DisableForTestingReason for explanation.
DisableBackForwardCache(BackForwardCacheImpl::TEST_USES_UNLOAD_EVENT);
const std::string unload_event_names[] = {"unload", "pagehide",
"visibilitychange"};
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
GURL ping_url(embedded_test_server()->GetURL("b.com", "/empty.html"));
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
for (const std::string& unload_event_name : unload_event_names) {
for (const std::string& unload_event_target :
EventTargetsForEvent(unload_event_name)) {
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root =
static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
// Add a subframe unload handler to do a termination ping via sendBeacon.
AddUnloadEventHandler(
child_rfh, unload_event_name, unload_event_target,
base::StringPrintf("navigator.sendBeacon('%s', 'ping');",
ping_url.spec().c_str()));
ExtendSubframeUnloadTimeoutForTerminationPing(child_rfh);
// Navigate the main frame to c.com and wait for the ping.
StartMonitoringRequestsFor(ping_url);
EXPECT_TRUE(NavigateToURL(shell(), c_url));
// Test succeeds if this doesn't time out while waiting for |ping_url|.
WaitForMonitoredRequest();
EXPECT_EQ("ping", GetRequestContent());
}
}
}
// Ensure that after a main frame with a cross-site iframe is itself navigated
// cross-site, the pagehide handler in the iframe can use an image load to do a
// termination ping. See https://crbug.com/852204, where this was broken with
// site isolation if the iframe was in its own process.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
SubframeTerminationPing_Image) {
StartEmbeddedServer();
// See BackForwardCache::DisableForTestingReason for explanation.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
// Add a subframe pagehide handler to do a termination ping by loading an
// image.
GURL ping_url(embedded_test_server()->GetURL("b.com", "/blank.jpg"));
AddUnloadEventHandler(
child_rfh, "pagehide", "window",
base::StringPrintf("var img = document.createElement('img');"
"img.src = '%s';"
"document.body.appendChild(img);",
ping_url.spec().c_str()));
ExtendSubframeUnloadTimeoutForTerminationPing(child_rfh);
// Navigate the main frame to c.com and wait for the ping.
StartMonitoringRequestsFor(ping_url);
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), c_url));
// Test succeeds if this doesn't time out while waiting for |ping_url|.
WaitForMonitoredRequest();
}
// Ensure that when closing a window containing a page with a cross-site
// iframe, the iframe still runs its pagehide handler and can do a sendBeacon
// termination ping.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
SubframeTerminationPingWhenWindowCloses) {
StartEmbeddedServer();
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Open a popup window with a page containing a cross-site iframe.
GURL popup_url(embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b(c)"));
Shell* popup = OpenPopup(root, popup_url, "popup");
WebContentsImpl* popup_contents =
static_cast<WebContentsImpl*>(popup->web_contents());
EXPECT_TRUE(WaitForLoadStop(popup_contents));
EXPECT_EQ(popup_url, popup_contents->GetLastCommittedURL());
FrameTreeNode* popup_root = popup_contents->GetPrimaryFrameTree().root();
RenderFrameHostImpl* child_rfh =
popup_root->child_at(0)->current_frame_host();
// In the popup, add a subframe pagehide handler to do a termination ping via
// sendBeacon.
GURL ping_url(embedded_test_server()->GetURL("c.com", "/empty.html"));
AddUnloadEventHandler(
child_rfh, "pagehide", "window",
base::StringPrintf("navigator.sendBeacon('%s', 'ping');",
ping_url.spec().c_str()));
ExtendSubframeUnloadTimeoutForTerminationPing(child_rfh);
// Close the popup and wait for the ping.
StartMonitoringRequestsFor(ping_url);
popup->Close();
// Test succeeds if this doesn't time out while waiting for |ping_url|.
WaitForMonitoredRequest();
EXPECT_EQ("ping", GetRequestContent());
}
// Ensure that after a main frame with a cross-site iframe is navigated
// cross-site, and the iframe had a pagehide handler which never finishes,
// the iframe's process eventually exits.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
SubframeProcessGoesAwayAfterUnloadTimeout) {
StartEmbeddedServer();
// See BackForwardCache::DisableForTestingReason for explanation.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
// Add a pagehide handler which never finishes to b.com subframe.
AddUnloadEventHandler(child_rfh, "pagehide", "window", "while(1);");
// Navigate the main frame to c.com and wait for the subframe process to
// shut down. This should happen when the subframe unload timeout happens,
// roughly in one second. Note that depending on whether site isolation is
// enabled, the subframe process may or may not be the same as the old main
// frame process, but it should shut down regardless.
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
RenderProcessHostWatcher process_exit_observer(
child_rfh->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
EXPECT_TRUE(NavigateToURL(shell(), c_url));
process_exit_observer.Wait();
}
// Verify that when an OOPIF with a pagehide handler navigates cross-process,
// its pagehide handler is able to send a postMessage to the parent frame.
// See https://crbug.com/857274.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
PostMessageToParentWhenSubframeNavigates) {
StartEmbeddedServer();
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
FrameTreeNode* child = root->child_at(0);
// Add an onmessage listener in the main frame.
EXPECT_TRUE(ExecJs(root, R"(
function waitForMessage() {
return new Promise(resolve => {
window.addEventListener('message', function(e) {
resolve(e.data);
});
});
})"));
// Add a pagehide handler in the child frame to send a postMessage to the
// parent frame.
AddUnloadEventHandler(child->current_frame_host(), "pagehide", "window",
"parent.postMessage('foo', '*')");
child->current_frame_host()->DisableUnloadTimerForTesting();
// Navigate the subframe cross-site to c.com and wait for the message.
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
EXPECT_EQ("foo",
EvalJs(root, base::StringPrintf(
"var p = waitForMessage();"
"document.querySelector('iframe').src = '%s';"
"p;",
c_url.spec().c_str())));
// Now repeat the test with a remote-to-local navigation that brings the
// subframe back to a.com.
AddUnloadEventHandler(child->current_frame_host(), "pagehide", "window",
"parent.postMessage('bar', '*')");
child->current_frame_host()->DisableUnloadTimerForTesting();
GURL a_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
EXPECT_EQ("bar",
EvalJs(root, base::StringPrintf(
"var p = waitForMessage();"
"document.querySelector('iframe').src = '%s';"
"p;",
a_url.spec().c_str())));
}
// Ensure that when a pending delete RenderFrameHost's process dies, the
// current RenderFrameHost does not lose its child frames. See
// https://crbug.com/867274.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest,
PendingDeleteRFHProcessShutdownDoesNotRemoveSubframes) {
StartEmbeddedServer();
GURL first_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), first_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImpl* rfh = root->current_frame_host();
// Set up a pagehide handler which never finishes to force |rfh| to stay
// around in pending delete state and never receive the
// mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame.
EXPECT_TRUE(ExecJs(rfh, "window.onpagehide = function(e) { while(1); };\n"));
rfh->DisableUnloadTimerForTesting();
// Navigate to another page with two subframes.
RenderFrameDeletedObserver rfh_observer(rfh);
// Ensure that current document won't enter the BackForwardCache, so that it
// properly execute its unload handler. So, we disable back-forward cache for
// this test.
DisableBackForwardCache(BackForwardCacheImpl::TEST_USES_UNLOAD_EVENT);
GURL second_url(embedded_test_server()->GetURL(
"b.com", "/cross_site_iframe_factory.html?b(c,b)"));
EXPECT_TRUE(NavigateToURL(shell(), second_url));
// At this point, |rfh| should still be live and pending deletion.
EXPECT_FALSE(rfh_observer.deleted());
EXPECT_TRUE(rfh->IsPendingDeletion());
EXPECT_TRUE(rfh->IsRenderFrameLive());
// Meanwhile, the new page should have two subframes.
EXPECT_EQ(2U, root->child_count());
// Kill the pending delete RFH's process.
RenderProcessHostWatcher crash_observer(
rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
rfh->GetProcess()->Shutdown(0);
crash_observer.Wait();
// The process kill should simulate a
// mojo::AgentSchedulingGroupHost::DidUnloadRenderFrame and trigger
// destruction of the pending delete RFH.
rfh_observer.WaitUntilDeleted();
// Ensure that the process kill didn't incorrectly remove subframes from the
// new page.
ASSERT_EQ(2U, root->child_count());
EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
EXPECT_TRUE(root->child_at(1)->current_frame_host()->IsRenderFrameLive());
}
// RenderFrameHost should have correct sudden termination disabler
// state after the event listeners are registered and removed.
// Regression test for crbug.com/1341417.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerUnloadBrowserTest,
AddAndRemoveEventListenersAffectingSuddenTerminationDisablerState) {
StartEmbeddedServer();
const std::string sudden_termination_disabler_event_names[] = {
"unload", "beforeunload", "pagehide", "visibilitychange"};
// Initialize the RenderFrameHost.
GURL first_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), first_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
RenderFrameHostImpl* rfh = root->current_frame_host();
// Register a callback function so we can remove the event listener later.
EXPECT_TRUE(ExecJs(rfh, "var callback = (e) => {};\n"));
for (const std::string& event_name :
sudden_termination_disabler_event_names) {
for (const std::string& event_target : EventTargetsForEvent(event_name)) {
// The sudden termination disabler state is initially set to false.
EXPECT_FALSE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
// Now add an event listener for the event_name.
EXPECT_TRUE(ExecJs(
rfh, base::StringPrintf("%s.addEventListener('%s', callback);",
event_target.c_str(), event_name.c_str())));
// The sudden termination disabler state should be true now.
EXPECT_TRUE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
// Remove the registered event listener.
EXPECT_TRUE(ExecJs(
rfh, base::StringPrintf("%s.removeEventListener('%s', callback);",
event_target.c_str(), event_name.c_str())));
// The sudden termination disabler state should be false now.
EXPECT_FALSE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
// Add the event listener back for the event_name.
EXPECT_TRUE(ExecJs(
rfh, base::StringPrintf("%s.addEventListener('%s', callback);",
event_target.c_str(), event_name.c_str())));
// The sudden termination disabler state should be true again.
EXPECT_TRUE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
// Calling `document.open()` should trigger `RemoveAllEventListeners()`
// in both the document DOM node and the DOM window.
EXPECT_TRUE(ExecJs(rfh, "document.open();"));
// The sudden termination disabler state should be false now.
EXPECT_FALSE(rfh->GetSuddenTerminationDisablerState(
DisablerTypeForEvent(event_name)));
}
}
}
namespace {
// A helper to post a recurring check that a renderer process is foregrounded.
// The recurring check uses WeakPtr semantic and will die when this class goes
// out of scope.
class AssertForegroundHelper {
public:
AssertForegroundHelper() = default;
AssertForegroundHelper(const AssertForegroundHelper&) = delete;
AssertForegroundHelper& operator=(const AssertForegroundHelper&) = delete;
#if BUILDFLAG(IS_APPLE) && !BUILDFLAG(IS_IOS_TVOS)
// Asserts that |renderer_process| isn't backgrounded and reposts self to
// check again shortly. |renderer_process| must outlive this
// AssertForegroundHelper instance.
void AssertForegroundAndRepost(const base::Process& renderer_process) {
ASSERT_EQ(renderer_process.GetPriority(
BrowserChildProcessHost::GetPortProvider()),
base::Process::Priority::kUserBlocking);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AssertForegroundHelper::AssertForegroundAndRepost,
weak_ptr_factory_.GetWeakPtr(),
std::cref(renderer_process)),
base::Milliseconds(1));
}
#else // BUILDFLAG(IS_APPLE) && !BUILDFLAG(IS_IOS_TVOS)
// Same as above without the Mac specific base::PortProvider.
void AssertForegroundAndRepost(const base::Process& renderer_process) {
ASSERT_NE(renderer_process.GetPriority(),
base::Process::Priority::kBestEffort);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AssertForegroundHelper::AssertForegroundAndRepost,
weak_ptr_factory_.GetWeakPtr(),
std::cref(renderer_process)),
base::Milliseconds(1));
}
#endif // BUILDFLAG(IS_APPLE) && !BUILDFLAG(IS_IOS_TVOS)
private:
base::WeakPtrFactory<AssertForegroundHelper> weak_ptr_factory_{this};
};
} // namespace
// This is a regression test for https://crbug.com/560446. It ensures the
// newly launched process for cross-process navigation in the foreground
// WebContents isn't backgrounded prior to the navigation committing and a
// "visible" widget being added to the process. This test discards the spare
// RenderProcessHost if present, to ensure that it is not used in the
// cross-process navigation.
// TODO(crbug.com/40760155): Flaky on Mac.
#if BUILDFLAG(IS_APPLE)
#define MAYBE_ForegroundNavigationIsNeverBackgroundedWithoutSpareProcess \
DISABLED_ForegroundNavigationIsNeverBackgroundedWithoutSpareProcess
#else
#define MAYBE_ForegroundNavigationIsNeverBackgroundedWithoutSpareProcess \
ForegroundNavigationIsNeverBackgroundedWithoutSpareProcess
#endif
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
MAYBE_ForegroundNavigationIsNeverBackgroundedWithoutSpareProcess) {
StartEmbeddedServer();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Start off navigating to a.com and capture the process used to commit.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
RenderProcessHost* start_rph =
web_contents->GetPrimaryMainFrame()->GetProcess();
// Discard the spare RenderProcessHost to ensure a new RenderProcessHost
// is created and has the right prioritization.
auto& spare_manager = SpareRenderProcessHostManagerImpl::Get();
spare_manager.CleanupSparesForTesting();
EXPECT_TRUE(spare_manager.GetSpares().empty());
// Start a navigation to b.com to ensure a cross-process navigation is
// in progress and ensure the process for the speculative host is different.
GURL url(embedded_test_server()->GetURL("b.com", "/title2.html"));
content::TestNavigationManager navigation_manager(web_contents, url);
shell()->LoadURL(url);
navigation_manager.WaitForSpeculativeRenderFrameHostCreation();
RenderProcessHost* speculative_rph = web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host()
->GetProcess();
EXPECT_NE(start_rph, speculative_rph);
EXPECT_FALSE(speculative_rph->IsReady());
#if !BUILDFLAG(IS_ANDROID)
// TODO(gab, nasko): On Android IsProcessBackgrounded is currently giving
// incorrect value at this stage of the process lifetime. This should be
// fixed in follow up cleanup work. See https://crbug.com/560446.
EXPECT_NE(speculative_rph->GetPriority(),
base::Process::Priority::kBestEffort);
#endif
// Wait for the underlying OS process to have launched and be ready to
// receive IPCs.
RenderProcessHostWatcher process_observer(
speculative_rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY);
process_observer.Wait();
// Kick off an infinite check against self that the process used for
// navigation is never backgrounded. The WaitForNavigationFinished will wait
// inside a RunLoop() and hence perform this check regularly throughout the
// navigation.
const base::Process& process = speculative_rph->GetProcess();
EXPECT_TRUE(process.IsValid());
AssertForegroundHelper assert_foreground_helper;
assert_foreground_helper.AssertForegroundAndRepost(process);
// The process should be foreground priority before commit because it is
// pending, and foreground after commit because it has a visible widget.
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_NE(start_rph, web_contents->GetPrimaryMainFrame()->GetProcess());
EXPECT_EQ(speculative_rph, web_contents->GetPrimaryMainFrame()->GetProcess());
}
// Similar to the test above, but verifies the spare RenderProcessHost uses the
// right priority.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
ForegroundNavigationIsNeverBackgroundedWithSpareProcess) {
// This test applies only when spare RenderProcessHost is enabled and in use.
if (!RenderProcessHostImpl::IsSpareProcessKeptAtAllTimes()) {
return;
}
StartEmbeddedServer();
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Start off navigating to a.com and capture the process used to commit.
SpareRenderProcessHostStartedObserver spare_started_observer;
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
// The AndroidWarmUpSpareRendererWithTimeout feature will create a spare
// renderer after the navigation finishes or with a delay so we need to
// explicitly wait.
if (base::FeatureList::IsEnabled(
features::kAndroidWarmUpSpareRendererWithTimeout)) {
spare_started_observer.WaitForSpareRenderProcessStarted();
}
RenderProcessHost* start_rph =
web_contents->GetPrimaryMainFrame()->GetProcess();
// At this time, there should be at least one RenderProcessHost. Capture them
// for testing expectations later.
auto& spare_manager = SpareRenderProcessHostManagerImpl::Get();
EXPECT_THAT(
spare_manager.GetSpares(),
testing::Each(testing::Property(&RenderProcessHost::GetPriority,
base::Process::Priority::kBestEffort)));
std::vector<ChildProcessId> spare_rph_ids = spare_manager.GetSpareIds();
ASSERT_FALSE(spare_rph_ids.empty());
// Start a navigation to b.com to ensure a cross-process navigation is
// in progress and ensure the process for the speculative host is
// different, but matches the spare RenderProcessHost.
GURL url(embedded_test_server()->GetURL("b.com", "/title2.html"));
content::TestNavigationManager navigation_manager(web_contents, url);
shell()->LoadURL(url);
navigation_manager.WaitForSpeculativeRenderFrameHostCreation();
RenderProcessHost* speculative_rph = web_contents->GetPrimaryFrameTree()
.root()
->render_manager()
->speculative_frame_host()
->GetProcess();
EXPECT_NE(start_rph, speculative_rph);
// In this test case, a spare RenderProcessHost will be used, so verify it
// and ensure it is ready.
EXPECT_THAT(spare_rph_ids, testing::Contains(speculative_rph->GetID()));
// If LoadUrl finished before the task to call
// RenderProcessHostImpl::OnChannelConnected is run, wait for the task to be
// run.
if (!speculative_rph->IsReady()) {
RenderProcessHostWatcher ready_waiter(
speculative_rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY);
ready_waiter.Wait();
}
EXPECT_TRUE(speculative_rph->IsReady());
// The creation of the speculative RenderFrameHost should change the
// RenderProcessHost's copy of the priority of the spare process from
// background to foreground.
EXPECT_NE(speculative_rph->GetPriority(),
base::Process::Priority::kBestEffort);
// The OS process itself is updated on the process launcher thread, so it
// cannot be observed immediately here. Perform a thread hop to and back to
// allow for the priority change to occur before using the
// AssertForegroundHelper object to check the OS process priority.
{
base::RunLoop run_loop;
GetProcessLauncherTaskRunner()->PostTaskAndReply(
FROM_HERE, base::DoNothing(), run_loop.QuitWhenIdleClosure());
run_loop.Run();
}
// Kick off an infinite check against self that the process used for
// navigation is never backgrounded. The WaitForNavigationFinished will wait
// inside a RunLoop() and hence perform this check regularly throughout the
// navigation.
const base::Process& process = speculative_rph->GetProcess();
EXPECT_TRUE(process.IsValid());
AssertForegroundHelper assert_foreground_helper;
assert_foreground_helper.AssertForegroundAndRepost(process);
// The process should be foreground priority before commit because it is
// pending, and foreground after commit because it has a visible widget.
ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
EXPECT_NE(start_rph, web_contents->GetPrimaryMainFrame()->GetProcess());
EXPECT_EQ(speculative_rph, web_contents->GetPrimaryMainFrame()->GetProcess());
}
// When ProactivelySwapBrowsingInstance is enabled, the browser switch to a new
// BrowsingInstance on cross-site HTTP(S) main frame navigations, when there are
// no other windows in the BrowsingInstance.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
ProactivelySwapBrowsingInstance) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstance> a_site_instance =
web_contents->GetPrimaryMainFrame()->GetSiteInstance();
// Navigate to B. The navigation is document initiated. It swaps
// BrowsingInstance only if ProactivelySwapBrowsingInstance is enabled.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
EXPECT_TRUE(WaitForLoadStop(web_contents));
scoped_refptr<SiteInstance> b_site_instance =
web_contents->GetPrimaryMainFrame()->GetSiteInstance();
if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
} else {
EXPECT_TRUE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
}
}
// Tests specific to the "default process" mode (which creates strict
// SiteInstances that can share a default process per BrowsingInstance).
class RenderFrameHostManagerDefaultProcessTest
: public RenderFrameHostManagerTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
RenderFrameHostManagerTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDisableSiteIsolation);
command_line->RemoveSwitch(switches::kSitePerProcess);
}
};
// Ensure that the default process can be used for URLs that don't assign a site
// to the SiteInstance, when Site Isolation is not enabled.
// 1. Visit foo.com.
// 2. Start to navigate to a siteless URL.
// 3. When the commit is pending, start a navigation to bar.com in a popup.
// (Using a popup avoids a crash when replacting the speculative RFH, per
// https://crbug.com/838348.)
// All navigations should use the default process, and we should not crash.
// See https://crbug.com/977956.
// TODO(crbug.com/390571607, yangsharon): Enable this test when default
// SiteInstanceGroups is implemented.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerDefaultProcessTest,
DISABLED_NavigationRacesWithSitelessCommitInDefaultProcess) {
ASSERT_TRUE(embedded_test_server()->Start());
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title1.html"));
// Step 1: Visit foo.com in the default process.
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
RenderProcessHost* original_process =
web_contents->GetPrimaryMainFrame()->GetProcess();
EXPECT_EQ(original_process, web_contents->GetPrimaryMainFrame()
->GetSiteInstance()
->GetDefaultProcessForBrowsingInstance());
// This test expect a cross-site navigation to be same BrowsingInstance. With
// ProactivelySwapBrowsingInstance, it won't be the case. Opening a popup
// prevent the BrowsingInstance to change.
if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
GURL popup_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(OpenPopup(web_contents->GetPrimaryMainFrame(), popup_url, ""));
}
// Set up a URL for which ShouldAssignSiteForURL will return false. The
// corresponding SiteInstance's site will be left unassigned, and its process
// won't be locked. This requires adding the URL's scheme as an empty
// document scheme.
url::ScopedSchemeRegistryForTests scheme_registry;
url::AddEmptyDocumentScheme("siteless");
GURL siteless_url("siteless://test");
EXPECT_FALSE(SiteInstance::ShouldAssignSiteForURL(siteless_url));
// Set up the work to be done after the renderer is asked to commit
// |siteless_url|, but before the corresponding DidCommitProvisionalLoad IPC
// is processed. This will start a navigation to |bar_url| in a popup and
// wait for its response. We use a popup to avoid trampling the speculative
// RFH while it is committing (per https://crbug.com/838348).
auto did_commit_callback =
base::BindLambdaForTesting([&](RenderFrameHost* rfh) {
Shell* new_shell = OpenPopup(shell(), GURL(url::kAboutBlankURL), "foo");
EXPECT_TRUE(new_shell);
// Step 3: Navigate to bar.com in the same BrowsingInstance, while the
// commit to siteless_url is pending. This used to crash because it
// picked the default process, but IsSuitableHost said it was not ok due
// to the pending siteless URL commit (in https://crbug.com/977956).
TestNavigationManager bar_manager(new_shell->web_contents(), bar_url);
EXPECT_TRUE(
ExecJs(new_shell, base::StringPrintf("location.href = '%s'",
bar_url.spec().c_str())));
// Wait for response. This will cause |bar_manager| to spin up a
// nested message loop while we're blocked in the current message loop
// (within DidCommitNavigationInterceptor). Thus, it's important to
// allow nestable tasks in |bar_manager|'s message loop, so that it can
// process the response before we unblock the
// DidCommitNavigationInterceptor's message loop and finish processing
// the commit.
bar_manager.AllowNestableTasks();
EXPECT_TRUE(bar_manager.WaitForResponse());
bar_manager.ResumeNavigation();
// After returning here, the commit for |siteless_url| will be
// processed.
});
// Step 2: Visit siteless_url in the same BrowsingInstance, but wait before
// the commit IPC is processed. (See did_commit_callback above for step 3.)
CommitMessageDelayer commit_delayer(web_contents,
siteless_url /* deferred_url */,
std::move(did_commit_callback));
EXPECT_TRUE(ExecJs(shell(), base::StringPrintf("location.href = '%s'",
siteless_url.spec().c_str())));
// Wait for the DidCommit IPC for |siteless_url|, and before processing it,
// trigger a navigation to |foo_url| and wait for its response.
commit_delayer.Wait();
EXPECT_EQ(original_process,
web_contents->GetPrimaryMainFrame()->GetProcess());
}
// 1. Navigate to A1(B2, B3(B4), C5)
// 2. Crash process B
// 3. Reload B2, creating RFH B6.
//
// Along the way, check the RenderFrameProxies.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
CrashFrameReloadAndCheckProxy) {
// This test explicitly requires multiple processes to be used. It won't mean
// anything without SiteIsolation.
if (!AreAllSitesIsolatedForTesting()) {
return;
}
// 1. Navigate to A1(B2, B3(B4), C5).
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,b(b),c)"));
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* a1 = web_contents->GetPrimaryMainFrame();
RenderFrameHostImpl* b2 = a1->child_at(0)->current_frame_host();
RenderFrameHostImpl* b3 = a1->child_at(1)->current_frame_host();
RenderFrameHostImpl* b4 = b3->child_at(0)->current_frame_host();
RenderFrameHostImpl* c5 = a1->child_at(2)->current_frame_host();
RenderFrameDeletedObserver delete_a1(a1);
RenderFrameDeletedObserver delete_b2(b2);
RenderFrameDeletedObserver delete_b3(b3);
RenderFrameDeletedObserver delete_b4(b4);
RenderFrameDeletedObserver delete_c5(c5);
GURL b2_url = b2->GetLastCommittedURL();
int b2_routing_id = b2->GetRoutingID();
auto proxy_count = [](RenderFrameHostImpl* rfh) {
return rfh->browsing_context_state()->GetProxyCount();
};
// There are 3 processes, so every frame has 2 frame proxies.
EXPECT_EQ(2u, proxy_count(a1));
EXPECT_EQ(2u, proxy_count(b2));
EXPECT_EQ(2u, proxy_count(b3));
EXPECT_EQ(2u, proxy_count(b4));
EXPECT_EQ(2u, proxy_count(c5));
auto is_proxy_live = [](RenderFrameHostImpl* rfh,
scoped_refptr<SiteInstanceImpl> site_instance) {
return rfh->browsing_context_state()
->GetRenderFrameProxyHost(site_instance->group())
->is_render_frame_proxy_live();
};
// Store SiteInstance for later comparison.
scoped_refptr<SiteInstanceImpl> a_site_instance(a1->GetSiteInstance());
scoped_refptr<SiteInstanceImpl> b_site_instance(b2->GetSiteInstance());
scoped_refptr<SiteInstanceImpl> c_site_instance(c5->GetSiteInstance());
// Check that each of the site instances are in a different group, so proxies
// exist for the others.
EXPECT_NE(a_site_instance->group(), b_site_instance->group());
EXPECT_NE(a_site_instance->group(), c_site_instance->group());
EXPECT_NE(b_site_instance->group(), c_site_instance->group());
// Check the state of the proxies before the crash:
EXPECT_TRUE(is_proxy_live(a1, b_site_instance));
EXPECT_TRUE(is_proxy_live(a1, c_site_instance));
EXPECT_TRUE(is_proxy_live(b2, a_site_instance));
EXPECT_TRUE(is_proxy_live(b2, c_site_instance));
EXPECT_TRUE(is_proxy_live(b3, a_site_instance));
EXPECT_TRUE(is_proxy_live(b3, c_site_instance));
EXPECT_TRUE(is_proxy_live(c5, a_site_instance));
EXPECT_TRUE(is_proxy_live(c5, b_site_instance));
// 2. Crash process B.
RenderProcessHost* process = b2->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
// Only B4 is deleted. B2 and B3 are still there in a "crashed" state.
delete_b4.WaitUntilDeleted();
// B2, B3, B4 RenderFrame are gone.
EXPECT_FALSE(delete_a1.deleted());
EXPECT_TRUE(delete_b2.deleted());
EXPECT_TRUE(delete_b3.deleted());
EXPECT_TRUE(delete_b4.deleted());
EXPECT_FALSE(delete_c5.deleted());
// B2 and B3 RenderFrameHost are still there, but B4 is definitely gone.
ASSERT_EQ(3u, a1->child_count());
EXPECT_EQ(b2, a1->child_at(0)->current_frame_host());
EXPECT_EQ(b3, a1->child_at(1)->current_frame_host());
ASSERT_EQ(0u, b3->child_count());
EXPECT_FALSE(a1->must_be_replaced_for_crash());
EXPECT_TRUE(b2->must_be_replaced_for_crash());
EXPECT_TRUE(b3->must_be_replaced_for_crash());
EXPECT_FALSE(c5->must_be_replaced_for_crash());
EXPECT_EQ(2u, proxy_count(a1));
EXPECT_EQ(2u, proxy_count(b2));
EXPECT_EQ(2u, proxy_count(b3));
EXPECT_EQ(2u, proxy_count(c5));
// Check the state of the proxies after the crash:
EXPECT_FALSE(is_proxy_live(a1, b_site_instance));
EXPECT_TRUE(is_proxy_live(a1, c_site_instance));
EXPECT_TRUE(is_proxy_live(b2, a_site_instance));
EXPECT_TRUE(is_proxy_live(b2, c_site_instance));
EXPECT_TRUE(is_proxy_live(b3, a_site_instance));
EXPECT_TRUE(is_proxy_live(b3, c_site_instance));
EXPECT_TRUE(is_proxy_live(c5, a_site_instance));
EXPECT_FALSE(is_proxy_live(c5, b_site_instance));
// 3. Reload B2, B6 is created.
NavigateFrameToURL(b2->frame_tree_node(), b2_url);
// B2 has been replaced
EXPECT_NE(b2_routing_id,
a1->child_at(0)->current_frame_host()->GetRoutingID());
// B3 hasn't been replaced.
EXPECT_EQ(b3, a1->child_at(1)->current_frame_host());
RenderFrameHostImpl* b6 = a1->child_at(0)->current_frame_host();
EXPECT_TRUE(b3->must_be_replaced_for_crash());
EXPECT_FALSE(b6->must_be_replaced_for_crash());
EXPECT_EQ(a_site_instance, a1->GetSiteInstance());
EXPECT_EQ(b_site_instance, b6->GetSiteInstance());
EXPECT_EQ(c_site_instance, c5->GetSiteInstance());
EXPECT_EQ(2u, proxy_count(a1));
EXPECT_EQ(2u, proxy_count(b6));
EXPECT_EQ(2u, proxy_count(b3));
EXPECT_EQ(2u, proxy_count(c5));
// Check the state of the proxies after the reload.
EXPECT_TRUE(is_proxy_live(a1, b_site_instance));
EXPECT_TRUE(is_proxy_live(a1, c_site_instance));
EXPECT_TRUE(is_proxy_live(b6, a_site_instance));
EXPECT_TRUE(is_proxy_live(b6, c_site_instance));
EXPECT_TRUE(is_proxy_live(b3, a_site_instance));
EXPECT_TRUE(is_proxy_live(b3, c_site_instance));
EXPECT_TRUE(is_proxy_live(c5, a_site_instance));
EXPECT_TRUE(is_proxy_live(c5, b_site_instance));
}
// With just the right initial navigations using RendererDebugURLs, creating a
// new RenderFrameHost can fail. https://crbug.com/1006814
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigateFromRevivedRendererDebugURL) {
StartEmbeddedServer();
// This matches IsRendererDebugURL.
GURL debug_url("javascript:'hello'");
// Just needs to be any URL that would navigate successfully.
GURL other_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Go to the debug URL. This is a synchronous navigation.
shell()->LoadURL(debug_url);
ASSERT_EQ("hello", EvalJs(shell(), "document.body.innerText"));
// Crash the renderer.
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderFrameHostImpl* rfh = root->current_frame_host();
RenderProcessHost* process = rfh->GetProcess();
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
// Load the URL again. This will cause the RenderWidgetHost to be revived,
// pointing to a RenderWidget in a new process.
shell()->LoadURL(debug_url);
ASSERT_EQ("hello", EvalJs(shell(), "document.body.innerText"));
RenderProcessHost* new_process = root->current_frame_host()->GetProcess();
// Now try load another URL. It should cope smoothly with the fact that the
// RenderWidgetHost is already revived.
ASSERT_TRUE(NavigateToURL(web_contents, other_url));
// In https://crbug.com/1006814 with site-isolation disabled when creating new
// hosts for crashed frames, the process does not change. We check that here
// to make sure that we actually recreated the bug. With site-isolation
// enabled, the process should change.
if (!AreAllSitesIsolatedForTesting()) {
ASSERT_EQ(new_process, root->current_frame_host()->GetProcess());
} else {
ASSERT_NE(new_process, root->current_frame_host()->GetProcess());
}
}
// Helper class to run tests without site isolation.
class RenderFrameHostManagerNoSiteIsolationTest
: public RenderFrameHostManagerTest {
public:
RenderFrameHostManagerNoSiteIsolationTest() = default;
RenderFrameHostManagerNoSiteIsolationTest(
const RenderFrameHostManagerNoSiteIsolationTest&) = delete;
RenderFrameHostManagerNoSiteIsolationTest& operator=(
const RenderFrameHostManagerNoSiteIsolationTest&) = delete;
~RenderFrameHostManagerNoSiteIsolationTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(switches::kDisableSiteIsolation);
}
};
// Ensure that when a process that allows any site gets reused by new
// BrowsingInstances, ChildProcessSecurityPolicy gets notified about those new
// BrowsingInstances. Failure to do so will lead to a crash at commit time due
// to mismatched process locks. See https://crbug.com/1141877.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerNoSiteIsolationTest,
IncludeIsolationContextInProcessThatAllowsAnySite) {
StartEmbeddedServer();
// Ensure we have one renderer process that's reused for everything.
RenderProcessHost::SetMaxRendererProcessCount(1);
// The test starts out with an initial window with a blank SiteInstance.
// Create a new window in a new BrowsingInstance and another blank
// SiteInstance.
Shell* shell2 =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
GURL(), nullptr, gfx::Size());
SiteInstanceImpl* old_instance = static_cast<SiteInstanceImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
SiteInstanceImpl* new_instance = static_cast<SiteInstanceImpl*>(
shell2->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(old_instance->IsRelatedSiteInstance(new_instance));
// At this point, neither SiteInstance should have a site assigned.
EXPECT_FALSE(old_instance->HasSite());
EXPECT_FALSE(new_instance->HasSite());
// Both should use the same process.
EXPECT_EQ(old_instance->GetProcess(), new_instance->GetProcess());
// Make sure the BrowsingInstanceId is cleaned up immediately.
ChildProcessSecurityPolicyImpl::GetInstance()
->SetBrowsingInstanceCleanupDelayForTesting(0);
// Close the test's initial window. This should destroy the initial
// BrowsingInstance and remove it from ChildProcessSecurityPolicy.
shell()->Close();
// Navigate to a web URL in the second window. This shouldn't crash.
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
ASSERT_TRUE(NavigateToURL(shell2->web_contents(), url));
}
// With RenderDocument for subframes, removing a frame while it is executing
// its own unload handler caused a crash. https://crbug.com/1148793
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RemoveSubframeInPageHide_SameSite) {
// TODO(crbug.com/40731502): Remove this early return. This doesn't
// work for RenderDocumentLevel::kNonLocalRootSubframe or greater because
// cancelling the navigation when detaching the subtree tries to restore the
// replaced `blink::RemoteFrame` (which doesn't exist in the same-site
// RenderDocument case because the replaced object wasn't a
// `blink::RemoteFrame`, but instead a RenderFrame).
if (ShouldCreateNewRenderFrameHostOnSameSiteNavigation(
/*is_main_frame=*/false, /*is_local_root=*/false)) {
return;
}
AssertCanRemoveSubframeInPageHide(/*same_site=*/true);
}
// See RemoveSubframeInUnload_SameSite
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
RemoveSubframeInPageHide_CrossSite) {
// TODO(crbug.com/40731502): Remove this early return.
if (ShouldCreateNewRenderFrameHostOnSameSiteNavigation(
/*is_main_frame=*/false, /*is_local_root=*/false) &&
!AreAllSitesIsolatedForTesting()) {
return;
}
AssertCanRemoveSubframeInPageHide(/*same_site=*/false);
}
// This test demonstrates a similar issue to the previous two tests, but
// triggers it in a slightly different way. The previous two tests navigate a
// subframe and rely on some variant of RenderDocument being enabled to trigger
// the crash. If the navigation commits in a new RenderFrameHostImpl, the
// renderer does not correctly handle the case where running the unload handler
// while swapping in the new frame detaches the navigated frame.
//
// However, this bug actually precedes RenderDocument, as detaching a document
// for navigation at swap time must also detach the subtree. Given a frame tree
// A1(B(A2)) and a navigation from B->A3, committing A3 will unload B. However,
// unloading B will also detach A2, and A2's unload handler can detach B since
// it can script A1 and remove the frame owner element synchronously.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerUnloadBrowserTest, NestedUnload) {
// These tests require site isolation to trigger the (formerly problematic)
// delayed detach of the remote frame when swapping in the new local frame.
if (!AreAllSitesIsolatedForTesting()) {
return;
}
SetupCrossSiteRedirector(embedded_test_server());
StartEmbeddedServer();
EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
"a.com", "/nested-unload-0.html")));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site A -- proxies for B\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
FrameTreeVisualizer().DepictFrameTree(
web_contents->GetPrimaryFrameTree().root()));
// Navigate the subframe, triggering unload.
FrameTreeNode* subframe = web_contents->GetPrimaryMainFrame()->child_at(0);
RenderFrameDeletedObserver observer(
subframe->render_manager()->current_frame_host());
GURL other_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
// The navigation will remove the frame that was navigating. Various Navigate
// helpers run into problems with this because there is no successful commit
// nor is there a DidStopLoading (because the destination frame should not
// load at all). So instead we start the Navigation and just wait for the
// deletion.
ASSERT_TRUE(ExecJs(subframe, JsReplace("location = $1", other_url)));
observer.WaitUntilDeleted();
// The subframe has been removed.
EXPECT_EQ(0UL, web_contents->GetPrimaryMainFrame()->child_count());
// TODO(crbug.com/40142480): Remove this. Without this, the crash in
// the renderer in https://crbug.com/1148793 is usually not caught.
ASSERT_TRUE(ExecJs(shell(), ""));
}
// See RemoveSubframeInPageHide_SameSite
void RenderFrameHostManagerTest::AssertCanRemoveSubframeInPageHide(
bool same_site) {
StartEmbeddedServer();
// Create a page with a subframe.
GURL frame_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a)"));
ASSERT_TRUE(NavigateToURL(shell(), frame_url));
// Set up the subframe's pagehide handler to remove the subframe.
ASSERT_TRUE(ExecJs(shell(), R"(
const subframe = document.getElementById("child-0");
subframe.contentWindow.onpagehide = () => {
subframe.remove();
}
)"));
// Navigate the subframe, triggering pagehide.
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* subframe = web_contents->GetPrimaryMainFrame()->child_at(0);
RenderFrameDeletedObserver observer(
subframe->render_manager()->current_frame_host());
GURL other_url(embedded_test_server()->GetURL(same_site ? "a.com" : "b.com",
"/title1.html"));
// The navigation will remove the frame that was navigating. Various Navigate
// helpers run into problems with this because there is no successful commit
// nor is there a DidStopLoading (because the destination frame should not
// load at all). So instead we start the Navigation and just wait for the
// deletion.
ASSERT_TRUE(ExecJs(subframe, JsReplace("location = $1", other_url)));
observer.WaitUntilDeleted();
// The subframe has been removed.
EXPECT_EQ(0UL, web_contents->GetPrimaryMainFrame()->child_count());
// TODO(crbug.com/40142480): Remove this. Without this, the crash in
// the renderer in https://crbug.com/1148793 is usually not caught.
ASSERT_TRUE(ExecJs(shell(), ""));
}
// From https://crbug.com/1169844. Verify that crashing a cross-site subframe
// and navigating it to a new site does not cause the browser to crash.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerTest,
NavigateCrossSiteSubframeAfterCrash) {
StartEmbeddedServer();
// Ensure that all 3 pages are isolated from each other (even on
// Android, where site-per-process is not the default).
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{"a.com", "b.com", "c.com"});
// Create a page with a subframe and navigate it cross-site.
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
ASSERT_TRUE(NavigateToURL(shell(), url));
// Crash the subframe.
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* subframe = web_contents->GetPrimaryMainFrame()->child_at(0);
RenderFrameHostImpl* rfh = subframe->current_frame_host();
RenderProcessHost* process = rfh->GetProcess();
{
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
}
ASSERT_FALSE(rfh->IsRenderFrameLive());
subframe = web_contents->GetPrimaryMainFrame()->child_at(0);
// Navigate the subframe cross-site.
GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));
ASSERT_TRUE(NavigateFrameToURL(subframe, url_c));
ASSERT_TRUE(subframe->current_frame_host()->IsRenderFrameLive());
}
// From https://crbug.com/1503038.
// The RuntimeFeatureStateDocumentData should be re-created when the main frame
// recovers from a crash.
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerTest,
RuntimeFeatureStateDocumentDataShouldBeRecreatedAfterCrash) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("a.com", "/empty.html"));
ASSERT_TRUE(NavigateToURL(shell(), url));
// Crash the frame.
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* rfh = web_contents->GetPrimaryMainFrame();
RenderProcessHost* process = rfh->GetProcess();
{
RenderProcessHostWatcher crash_observer(
process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
}
ASSERT_FALSE(rfh->IsRenderFrameLive());
auto* root = web_contents->GetPrimaryFrameTree().root();
RenderFrameHostManager* manager = root->render_manager();
manager->InitializeMainRenderFrameForImmediateUse();
// Add a new iframe. As part of this iframe's creation
// RenderFrameHostImpl::SetOriginDependentStateOfNewFrame() will be called
// which will attempt to copy the parent frame's
// RuntimeFeatureStateDocumentData.
std::string script =
"var new_iframe = document.createElement('iframe');"
"document.documentElement.appendChild(new_iframe);";
// If the parent's RuntimeFeatureStateDocumentData exists then this will
// succeed, otherwise we'll hit a CHECK.
EXPECT_TRUE(ExecJs(shell(), script));
}
// Tests that disable kDeferSpeculativeRFHCreation. This is to ensure that
// GetFrameHostNavigation is called immediately on NavigationRequest creation
// instead of getting queued in a task, which possibly causes it not to be run
// if the network response is already received and the final RenderFrameHost
// has been picked by another GetFrameHostNavigation call.
class RenderFrameHostManagerDisableDeferSpeculativeRFHCreationTest
: public RenderFrameHostManagerTest {
public:
RenderFrameHostManagerDisableDeferSpeculativeRFHCreationTest() {
feature_list_.InitAndDisableFeature(features::kDeferSpeculativeRFHCreation);
}
~RenderFrameHostManagerDisableDeferSpeculativeRFHCreationTest() override =
default;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_P(
RenderFrameHostManagerDisableDeferSpeculativeRFHCreationTest,
WastedSpeculativeRFHMetrics) {
StartEmbeddedServer();
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL url_d(embedded_test_server()->GetURL("d.com", "/title1.html"));
GURL url_e(embedded_test_server()->GetURL("e.com", "/title1.html"));
GURL url_c_redirect_to_d(embedded_test_server()->GetURL(
"c.com", "/server-redirect?" + url_d.spec()));
GURL url_d_redirect_to_e(embedded_test_server()->GetURL(
"d.com", "/server-redirect?" + url_e.spec()));
{
SCOPED_TRACE("Case 1: From initial blank document to A");
// 1) Navigate to A, which will reuse the current RFH.
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURL(shell(), url_a));
// GetFrameHostForNavigation is called twice for the navigation above.
// The first call was when there's no associated RFH yet, and because the
// navigation reuses the current RFH (because it's the first navigation), on
// the second call we didn't waste any speculative RFHs.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowKeepCurrentRFH,
1)));
}
{
SCOPED_TRACE("Case 2: From A to B");
// 2) Navigate cross-site to B, which will use a new speculative RFH. Note
// that we navigate from the renderer to avoid doing a proactive
// BrowsingInstance swap which might cause renderer process/RenderFrameHost
// swaps despite Site Isolation being turned off, making the metrics a bit
// confusing to explain.
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_b));
// GetFrameHostForNavigation is called twice for the navigation above.
// The first call was when there's no associated RFH yet, then it will
// create a speculative RFH. Then on the second call we keep using the same
// speculative RFH. Note that if all of Site Isolation, BFCache,
// RenderDocument and default SiteInstanceGroup are turned off, we'll just
// reuse current RFH in both cases.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(
(AreStrictSiteInstancesEnabled() ||
CanSameSiteMainFrameNavigationsChangeRenderFrameHosts())
? WastedSpeculativeRFHCase::
kNotWasted_NowKeepSameSpeculativeRFH
: WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowKeepCurrentRFH,
1)));
}
{
SCOPED_TRACE("Case 3: From B to C and redirecting to D");
// 3) Navigate (initially) cross-site to C, but then redirect to another
// cross-site URL D. Note that we navigate from the renderer to avoid doing
// a proactive BrowsingInstance swap which might cause renderer process
// swaps despite Site Isolation being turned off, making the metrics a bit
// confusing to explain.
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_c_redirect_to_d, url_d));
// GetFrameHostForNavigation is called twice for the navigation above.
bool wasted_speculative_rfh = false;
if (AreStrictSiteInstancesEnabled() ||
CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// First call created speculative RFH. Second call can reuse the
// speculative RFH only if site isolation is turned off.
wasted_speculative_rfh = AreAllSitesIsolatedForTesting();
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(AreStrictSiteInstancesEnabled()
? WastedSpeculativeRFHCase::
kWasted_NowUseNewSpeculativeRFH
: WastedSpeculativeRFHCase::
kNotWasted_NowKeepSameSpeculativeRFH,
1)));
} else {
// No wasted speculative RFH.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowKeepCurrentRFH,
1)));
}
if (wasted_speculative_rfh) {
// The wasted speculative RFH created a new process for C.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH."
"WastedRFHLikelyCreatedNewProcess",
true, 1);
// The replacement speculative RFH created a new process for D.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.ReplacementRFHCreatedNewProcess",
true, 1);
// Process differs between the wasted speculative RFH and the newly picked
// RFH.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.ProcessDiffers", true, 1);
// No cross-origin isolation difference.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.CrossOriginIsolationDiffers",
false, 1);
}
}
{
SCOPED_TRACE("Case 4: From D to C and redirecting to D");
// 4) Navigate (initially) cross-site to C, but then redirect to a
// same-site-to-current-RFH URL D. Note that we navigate from the renderer
// to avoid doing a proactive BrowsingInstance swap which might cause
// renderer process swaps despite Site Isolation being turned off, making
// the metrics a bit confusing to explain.
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_c_redirect_to_d, url_d));
// GetFrameHostForNavigation is called twice for the navigation above.
bool wasted_speculative_rfh = false;
// The first call will create a new process if strict site isolation is
// enabled, or BFCache-induced proactive BrowsingInstance swap happened.
// TODO(https://crbug.com/376777350): Reconsider if we really should do
// process swap on BrowsingInstance swap when site isolation is turned off.
bool first_call_created_new_process =
(AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled());
if (AreStrictSiteInstancesEnabled() ||
CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// First call created speculative RFH because of strict SiteInstances,
// RenderDocument, or proactive swap for BFCache. This speculative RFH
// will never get used, see cases below.
if (ShouldCreateNewHostForAllFrames() && first_call_created_new_process) {
// The navigation needs to commit in a speculative RFH, but the
// previously created speculative RFH is now incompatible because it
// was created for C in another process.
wasted_speculative_rfh = true;
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(
WastedSpeculativeRFHCase::kNotWasted_WasUnassociated, 1),
base::Bucket(
WastedSpeculativeRFHCase::kWasted_NowUseNewSpeculativeRFH,
1)));
} else if (AreStrictSiteInstancesEnabled()) {
// If we are using default SiteInstanceGroups, the navigation started in
// a speculative RFH for C. After the redirect to D, it committed in
// either the current RFH for D (without RenderDocument), or a new
// speculative RFH for D (with RenderDocument). So, the wasted
// speculative RFH metric should indicate that there was a change from
// speculative to current RFH (without RenderDocument), or from one
// speculative RFH to another (with RenderDocument).
wasted_speculative_rfh = true;
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(
WastedSpeculativeRFHCase::kNotWasted_WasUnassociated, 1),
base::Bucket(
ShouldCreateNewHostForAllFrames()
? WastedSpeculativeRFHCase::
kWasted_NowUseNewSpeculativeRFH
: WastedSpeculativeRFHCase::kWasted_NowUseCurrentRFH,
1)));
} else if (ShouldCreateNewHostForAllFrames()) {
// The navigation needs to commit in a speculative RFH, and it can reuse
// the previously created speculative RFH.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(
WastedSpeculativeRFHCase::kNotWasted_WasUnassociated, 1),
base::Bucket(WastedSpeculativeRFHCase::
kNotWasted_NowKeepSameSpeculativeRFH,
1)));
} else {
// The navigation ends up reusing the current active RFH instead.
wasted_speculative_rfh = true;
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(
WastedSpeculativeRFHCase::kNotWasted_WasUnassociated, 1),
base::Bucket(WastedSpeculativeRFHCase::kWasted_NowUseCurrentRFH,
1)));
}
} else {
// No speculative RFH created.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowKeepCurrentRFH,
1)));
}
if (wasted_speculative_rfh) {
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH."
"WastedRFHLikelyCreatedNewProcess",
first_call_created_new_process, 1);
// The replacement RFH does not create a new process for D.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.ReplacementRFHCreatedNewProcess",
false, 1);
// Process differs between the wasted speculative RFH and the newly picked
// RFH if the wasted one created a new process. The new RFH must be in the
// current / D's process.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.ProcessDiffers",
first_call_created_new_process, 1);
// No cross-origin isolation difference.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.CrossOriginIsolationDiffers",
false, 1);
}
}
{
SCOPED_TRACE("Case 5: From D to D and redirecting to E");
// 5) Navigate (initially) same-site to D, but then redirect cross-site to
// E. Note that we navigate from the renderer to avoid doing a proactive
// BrowsingInstance swap which might cause renderer process swaps despite
// Site Isolation being turned off, making the metrics a bit confusing to
// explain.
base::HistogramTester histogram_tester;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), url_d_redirect_to_e, url_e));
// GetFrameHostForNavigation is called twice for the navigation above.
bool wasted_speculative_rfh = false;
if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
// First call created speculative RFH. Second call can reuse the
// speculative RFH only if site isolation is turned off and default
// SiteInstanceGroups are not enabled.
wasted_speculative_rfh = AreStrictSiteInstancesEnabled();
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(wasted_speculative_rfh
? WastedSpeculativeRFHCase::
kWasted_NowUseNewSpeculativeRFH
: WastedSpeculativeRFHCase::
kNotWasted_NowKeepSameSpeculativeRFH,
1)));
} else {
// No speculative RFH created for the initial call for D.
EXPECT_THAT(
histogram_tester.GetAllSamples(
"Navigation.All.WastedSpeculativeRFHCase"),
testing::ElementsAre(
base::Bucket(WastedSpeculativeRFHCase::kNotWasted_WasUnassociated,
1),
base::Bucket(
AreStrictSiteInstancesEnabled()
? WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowUseSpeculativeRFH
: WastedSpeculativeRFHCase::
kNotWasted_WasUsingCurrentRFH_NowKeepCurrentRFH,
1)));
}
if (wasted_speculative_rfh) {
// The wasted speculative RFH used the process for D.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH."
"WastedRFHLikelyCreatedNewProcess",
false, 1);
// The replacement speculative RFH created a new process for E, if site
// isolation is turned on, or BFCache is enabled.
//
// TODO(https://crbug.com/376777350): Investigate if we should suppress
// the process creation for BFCache when site isolation is turned off.
//
// TODO(https://crbug.com/419469455): These metrics do not behave properly
// with default SiteInstanceGroups. They indicate that in this case, a new
// process is created, whereas in fact it is not created but rather
// reused. As a result, ProcessDiffers and ReplacementRFHCreateNewProcess
// are both incorrect (true instead of false) with default
// SiteInstanceGroups, which should be fixed.
if (!ShouldUseDefaultSiteInstanceGroup()) {
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH."
"ReplacementRFHCreatedNewProcess",
AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled(), 1);
// Process differs between the wasted RFH and the newly chosen RFH if a
// new process is created for E.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.ProcessDiffers",
AreAllSitesIsolatedForTesting() || IsBackForwardCacheEnabled(), 1);
}
// No cross-origin isolation difference.
histogram_tester.ExpectUniqueSample(
"Navigation.All.WastedSpeculativeRFH.CrossOriginIsolationDiffers",
false, 1);
}
}
}
// Tests that enable clearing window.name on cross-site
// cross-BrowsingInstance navigations.
class RenderFrameHostManagerClearWindowNameTest
: public RenderFrameHostManagerTest {
public:
RenderFrameHostManagerClearWindowNameTest() {
feature_list_.InitAndEnableFeature(
features::kClearCrossSiteCrossBrowsingContextGroupWindowName);
}
~RenderFrameHostManagerClearWindowNameTest() override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
// Verify that cross-site main frame navigation that swaps BrowsingInstances
// clears window.name.
IN_PROC_BROWSER_TEST_P(RenderFrameHostManagerClearWindowNameTest,
ClearWindowNameCrossSite) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to a.com/title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// Set window.name.
EXPECT_TRUE(content::ExecJs(web_contents, "window.name='foo'"));
auto* frame_a = web_contents->GetPrimaryMainFrame();
EXPECT_EQ("foo", frame_a->GetFrameName());
scoped_refptr<SiteInstance> site_instance_a = frame_a->GetSiteInstance();
// Renderer-initiated navigate to b.com/title2.html.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_b));
auto* frame_b = web_contents->GetPrimaryMainFrame();
scoped_refptr<SiteInstance> site_instance_b = frame_b->GetSiteInstance();
// Whether renderer-initiated top-level cross-site navigates swap
// BrowsingInstances is based on whether ProactivelySwapBrowsingInstances or
// BackForwardCache is enabled.
if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
EXPECT_FALSE(site_instance_a->IsRelatedSiteInstance(site_instance_b.get()));
// The BrowsingInstance got swapped, window.name is cleared.
EXPECT_EQ("", frame_b->GetFrameName());
} else {
EXPECT_TRUE(site_instance_a->IsRelatedSiteInstance(site_instance_b.get()));
// Window.name is not cleared.
EXPECT_EQ("foo", frame_b->GetFrameName());
}
// Navigate to c.com/title1.html. The navigation is cross-site, top-level and
// swaps BrowsingInstances, thus should clear window.name.
EXPECT_TRUE(NavigateToURL(shell(), url_c));
auto* frame_c = web_contents->GetPrimaryMainFrame();
// Check that b.com/title1.html and c.com/title1.html are in different
// BrowsingInstances.
scoped_refptr<SiteInstance> site_instance_c = frame_c->GetSiteInstance();
EXPECT_FALSE(site_instance_b->IsRelatedSiteInstance(site_instance_c.get()));
// Window.name should be cleared.
EXPECT_EQ("", frame_c->GetFrameName());
}
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerUnloadBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerSpoofingTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RFHMProcessPerTabTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerDefaultProcessTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerNoSiteIsolationTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(
All,
RenderFrameHostManagerDisableDeferSpeculativeRFHCreationTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
RenderFrameHostManagerClearWindowNameTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
} // namespace content