blob: 58aeef9518a23eb591c9ecc7f6d2baaf5538c0bb [file]
// Copyright 2025 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/code_cache_host_impl.h"
#include <stdint.h>
#include <array>
#include <ranges>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "content/browser/code_cache/generated_code_cache_context.h"
#include "content/browser/process_lock.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/url_info.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/common/child_process_id.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "crypto/hash.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "net/base/features.h"
#include "net/http/http_cache.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/origin.h"
namespace content {
// Parametrized on whether strict isolation is activated or not.
class CodeCacheHostImplTest : public testing::Test,
public testing::WithParamInterface<bool> {
public:
CodeCacheHostImplTest() {
SiteIsolationPolicy::DisableFlagCachingForTesting();
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (IsSitePerProcessOrStricter()) {
command_line->AppendSwitch(switches::kSitePerProcess);
} else {
command_line->RemoveSwitch(switches::kSitePerProcess);
command_line->AppendSwitch(switches::kDisableSiteIsolation);
}
feature_list_.InitWithFeatures(
{blink::features::kUsePersistentCacheForCodeCache,
net::features::kSplitCacheByNetworkIsolationKey,
blink::features::kInlineScriptCache},
{});
CHECK(temp_dir_.CreateUniqueTempDir());
generated_code_cache_context_ =
base::MakeRefCounted<GeneratedCodeCacheContext>();
generated_code_cache_context_->Initialize(temp_dir_.GetPath(), 0);
}
~CodeCacheHostImplTest() override {
generated_code_cache_context_->Shutdown();
ChildProcessSecurityPolicyImpl* p =
ChildProcessSecurityPolicyImpl::GetInstance();
for (auto renderer_id : added_renderers_) {
p->Remove(renderer_id);
}
}
void TearDown() override {
// The GeneratedCodeCache should not be exercised at all for the queries
// done in this test since they are expected to be served from
// PersistentCache.
histogram_tester.ExpectTotalCount("SiteIsolatedCodeCache.JS.Behaviour", 0);
}
static bool IsSitePerProcessOrStricter() { return GetParam(); }
void SetupRendererWithLock(ChildProcessId process_id,
const UrlInfo& url_info) {
ChildProcessSecurityPolicyImpl* p =
ChildProcessSecurityPolicyImpl::GetInstance();
p->AddForTesting(process_id, &browser_context_);
scoped_refptr<SiteInstanceImpl> site_instance =
SiteInstanceImpl::CreateForUrlInfo(
&browser_context_, url_info,
/*is_guest=*/false,
/*is_fenced=*/false,
/*is_fixed_storage_partition=*/false);
ChildProcessSecurityPolicyImpl::GetInstance()->LockProcess(
site_instance->GetIsolationContext(), process_id, false,
ProcessLock::FromSiteInfo(site_instance->GetSiteInfo()));
added_renderers_.push_back(process_id);
}
void SetupRendererWithLock(ChildProcessId process_id, const GURL& url) {
SetupRendererWithLock(process_id, UrlInfo::CreateForTesting(url));
}
protected:
base::test::ScopedFeatureList feature_list_;
BrowserTaskEnvironment task_environment_;
base::HistogramTester histogram_tester;
std::vector<ChildProcessId> added_renderers_;
TestBrowserContext browser_context_;
base::ScopedTempDir temp_dir_;
scoped_refptr<GeneratedCodeCacheContext> generated_code_cache_context_;
};
// PersistentCache is not supported on Fuchsia.
#if !BUILDFLAG(IS_FUCHSIA)
// Tests that back-to-back contexts operating in the same directory don't
// conflict with one another.
TEST_P(CodeCacheHostImplTest, PersistentCacheRecreationSequencing) {
base::FilePath cache_path =
temp_dir_.GetPath().AppendASCII("recreation_test");
const std::string data_str = "some data";
const GURL url("http://test.com/script.js");
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
// Create a context and put some data into it.
auto context_a = base::MakeRefCounted<GeneratedCodeCacheContext>();
context_a->Initialize(cache_path, 0);
GeneratedCodeCacheContext::RunOrPostTask(
context_a, FROM_HERE, base::BindLambdaForTesting([=]() {
auto host = CodeCacheHostImpl::Create(
ChildProcessId(1), context_a, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, url, base::Time::Now(),
mojo_base::BigBuffer(base::as_byte_span(data_str)));
}));
// Shut down the context, which posts a task to destroy the cache
// asynchronously.
context_a->Shutdown();
context_a.reset();
// Create a new context using the same path and use it.
auto context_b = base::MakeRefCounted<GeneratedCodeCacheContext>();
context_b->Initialize(cache_path, 0);
base::test::TestFuture<void> future;
GeneratedCodeCacheContext::RunOrPostTask(
context_b, FROM_HERE,
base::BindLambdaForTesting([context_b, &url, &nik,
quit = base::BindPostTaskToCurrentDefault(
future.GetCallback())]() mutable {
auto host = CodeCacheHostImpl::Create(
ChildProcessId(2), context_b, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->FetchCachedCode(blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[](base::OnceClosure quit, base::Time time,
mojo_base::BigBuffer buf) {
// The data may or may not have been cached;
// but for sure the process didn't crash.
if (IsSitePerProcessOrStricter()) {
EXPECT_EQ(buf.size(), 0U);
} else {
EXPECT_NE(buf.size(), 0U);
}
std::move(quit).Run();
},
std::move(quit)));
}));
EXPECT_TRUE(future.Wait());
context_b->Shutdown();
}
// Validates that improper site isolation setup (no valid process lock in
// renderer) leads to no use of the cache if full site isolation is activated.
// Under !IsSitePerProcessOrStricter() it instead means that the cache is usable
// and that entries are grouped under the same context for unlocked processes.
TEST_P(CodeCacheHostImplTest, PersistentCacheNoCachingWhenNoProperIsolation) {
const base::Time response_time = base::Time::Now();
const std::string data_str = "some data";
const mojo_base::BigBuffer data(base::as_byte_span(data_str));
const GURL url("http://example.com/script.js");
// The lack of a SetupRendererWithLock call for this process ID means
// `GetSecondaryKeyForCodeCache` will return an empty GURL, which causes this
// renderer to use the shared context key.
const ChildProcessId process_id(12);
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
// No way to confirm right away but storing should fail when
// !IsSitePerProcessOrStricter().
host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, url, response_time,
data.Clone());
if (IsSitePerProcessOrStricter()) {
// No data was stored so fetching fails.
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[&](base::OnceClosure quit_closure,
base::Time response_time, mojo_base::BigBuffer data) {
// Ensure data is empty.
EXPECT_EQ(response_time, base::Time());
EXPECT_EQ(data.byte_span(),
mojo_base::BigBuffer().byte_span());
std::move(quit_closure).Run();
},
quit_closure));
} else {
// Fetching of stored data succeeds.
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[&](base::Time expected_response_time,
const std::string& expected_data,
base::OnceClosure quit_closure,
base::Time response_time, mojo_base::BigBuffer data) {
EXPECT_EQ(expected_response_time, response_time);
EXPECT_EQ(expected_data,
std::string(
reinterpret_cast<const char*>(data.data()),
data.size()));
std::move(quit_closure).Run();
},
response_time, data_str, quit_closure));
}
}));
runloop.Run();
}
}
TEST_P(CodeCacheHostImplTest,
PersistentCacheLockedAndUnlockedProcessesShareNoData) {
const base::Time response_time = base::Time::Now();
const std::string data_str = "some data";
const mojo_base::BigBuffer data(base::as_byte_span(data_str));
const base::Time other_response_time = response_time + base::Minutes(1);
const std::string other_data_str = "some other data";
const mojo_base::BigBuffer other_data(base::as_byte_span(other_data_str));
const GURL url("http://example.com/script.js");
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId locked_process_id(12);
SetupRendererWithLock(locked_process_id, url);
// The lack of a SetupRendererWithLock call for this process ID means
// `GetSecondaryKeyForCodeCache` will return an empty GURL, which causes this
// renderer to use the shared context key.
const ChildProcessId unlocked_process_id(24);
// Locked process stores data.
{
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
locked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, url, response_time,
data.Clone());
}));
}
// Unlocked process.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
unlocked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
// Unlocked process cannot see the data stored by the locked process,
// even with the same NIK.
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[&](base::OnceClosure quit_closure, base::Time response_time,
mojo_base::BigBuffer data) {
// Ensure data is empty.
EXPECT_EQ(response_time, base::Time());
EXPECT_EQ(data.byte_span(),
mojo_base::BigBuffer().byte_span());
std::move(quit_closure).Run();
},
quit_closure));
// Store some other data to attempt to retrieve it from the locked
// process.
host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, url,
other_response_time, other_data.Clone());
}));
runloop.Run();
}
// Locked process cannot access data stored from shared context.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
locked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
// Locked process cannot see the data stored by the unlocked process
// and sees its own copy, even with the same NIK.
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[&](base::Time expected_response_time,
const std::string& expected_data,
base::OnceClosure quit_closure, base::Time response_time,
mojo_base::BigBuffer data) {
EXPECT_EQ(expected_response_time, response_time);
EXPECT_EQ(
expected_data,
std::string(reinterpret_cast<const char*>(data.data()),
data.size()));
std::move(quit_closure).Run();
},
response_time, data_str, quit_closure));
}));
runloop.Run();
}
}
// This test is parameterized to run with and without strict site isolation
// enabled. With a locked process, caching should succeed regardless of the
// isolation mode.
TEST_P(CodeCacheHostImplTest, PersistentCacheWriteAndReadFullIsolationSetup) {
const base::Time response_time = base::Time::Now();
const std::string data_str = "some data";
const mojo_base::BigBuffer data(base::as_byte_span(data_str));
const GURL original_resource_url("http://example.com/script.js");
// Storing and retrieving for from the same isolation context works.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GURL url = original_resource_url;
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId process_id(12);
SetupRendererWithLock(process_id, url);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, url, response_time,
data.Clone());
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, url,
base::BindOnce(
[&](base::Time expected_response_time,
const std::string& expected_data,
base::OnceClosure quit_closure, base::Time response_time,
mojo_base::BigBuffer data) {
EXPECT_EQ(expected_response_time, response_time);
EXPECT_EQ(
expected_data,
std::string(reinterpret_cast<const char*>(data.data()),
data.size()));
std::move(quit_closure).Run();
},
response_time, data_str, quit_closure));
}));
runloop.Run();
}
// Attempting to retrieve code for `original_resource_url` from a different
// isolation context does not work and instead returns default values.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GURL url("http://other.com/script.js");
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId process_id(24);
SetupRendererWithLock(process_id, url);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, original_resource_url,
base::BindOnce(
[&](base::OnceClosure quit_closure, base::Time response_time,
mojo_base::BigBuffer data) {
// Ensure data is empty.
EXPECT_EQ(response_time, base::Time());
EXPECT_EQ(data.byte_span(),
mojo_base::BigBuffer().byte_span());
std::move(quit_closure).Run();
},
quit_closure));
}));
runloop.Run();
}
}
TEST_P(CodeCacheHostImplTest, GetPendingBackend) {
base::RunLoop run_loop;
const GURL url("http://example.com/script.js");
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId process_id(12);
SetupRendererWithLock(process_id, url);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
for (auto cache_type : {blink::mojom::CodeCacheType::kJavascript,
blink::mojom::CodeCacheType::kWebAssembly}) {
base::test::TestFuture<
std::optional<persistent_cache::PendingBackend>>
future;
host->GetPendingBackend(cache_type, future.GetCallback());
EXPECT_TRUE(future.Get().has_value());
}
run_loop.Quit();
}));
run_loop.Run();
}
TEST_P(CodeCacheHostImplTest, SourceKeyedCacheWriteAndRead) {
const std::string data_str = "some data";
const mojo_base::BigBuffer data(base::as_byte_span(data_str));
const GURL url("http://example.com/script.js");
const std::string script_source = "console.log(42)";
const std::array<uint8_t, 32> source_hash =
crypto::hash::Sha256(script_source);
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId process_id(12);
SetupRendererWithLock(process_id, url);
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->DidGenerateSourceKeyedCacheableMetadata(
std::vector(std::from_range, source_hash), data.Clone());
host->FetchSourceKeyedCachedCodeForTesting(
source_hash,
base::BindOnce(
[](base::RepeatingClosure quit_closure,
std::string expected_data, mojo_base::BigBuffer data) {
EXPECT_EQ(
expected_data,
std::string(reinterpret_cast<const char*>(data.data()),
data.size()));
quit_closure.Run();
},
quit_closure, data_str));
}));
runloop.Run();
}
// Isolation check: a different site shouldn't have access.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GURL other_url("http://other.example/script.js");
net::NetworkIsolationKey other_nik(net::SchemefulSite{other_url},
net::SchemefulSite{other_url});
const ChildProcessId other_process_id(24);
SetupRendererWithLock(other_process_id, other_url);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
other_process_id, generated_code_cache_context_, other_nik,
blink::StorageKey::CreateFirstParty(
url::Origin::Create(other_url)));
host->FetchSourceKeyedCachedCodeForTesting(
source_hash, base::BindOnce(
[](base::RepeatingClosure quit_closure,
mojo_base::BigBuffer data) {
EXPECT_EQ(data.size(), 0U);
quit_closure.Run();
},
quit_closure));
}));
runloop.Run();
}
}
TEST_P(CodeCacheHostImplTest,
SourceKeyedCacheLockedAndUnlockedProcessesShareNoData) {
const std::string data_str = "some data";
const mojo_base::BigBuffer data(base::as_byte_span(data_str));
const std::string other_data_str = "some other data";
const mojo_base::BigBuffer other_data(base::as_byte_span(other_data_str));
const GURL url("http://example.com/script.js");
const url::Origin origin = url::Origin::Create(url);
const std::string script_source = "console.log(42)";
const std::array<uint8_t, 32> source_hash =
crypto::hash::Sha256(script_source);
const std::vector<uint8_t> source_hash_vec(std::from_range, source_hash);
net::NetworkIsolationKey nik(net::SchemefulSite{url},
net::SchemefulSite{url});
const ChildProcessId locked_process_id(12);
SetupRendererWithLock(locked_process_id, url);
const ChildProcessId unlocked_process_id(24);
// Locked process stores data.
{
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
locked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(origin));
host->DidGenerateSourceKeyedCacheableMetadata(source_hash_vec,
data.Clone());
}));
}
// Unlocked process.
{
base::RunLoop runloop;
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
base::test::TestFuture<mojo_base::BigBuffer> fetch_future;
auto host = CodeCacheHostImpl::Create(
unlocked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
// Unlocked process cannot read cache from the locked process.
host->FetchSourceKeyedCachedCodeForTesting(
source_hash, fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get().size(), 0U);
fetch_future.Clear();
// Unlocked process cannot store any data because the site has already
// been locked to a process.
host->DidGenerateSourceKeyedCacheableMetadata(source_hash_vec,
other_data.Clone());
host->FetchSourceKeyedCachedCodeForTesting(
source_hash, fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get().size(), 0U);
runloop.Quit();
}));
runloop.Run();
}
// Locked process cannot access data stored from shared context.
{
base::RunLoop runloop;
auto quit_closure = runloop.QuitClosure();
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto host = CodeCacheHostImpl::Create(
locked_process_id, generated_code_cache_context_, nik,
blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
host->FetchSourceKeyedCachedCodeForTesting(
source_hash,
base::BindOnce(
[](base::RepeatingClosure quit_closure,
std::string expected_data, mojo_base::BigBuffer data) {
EXPECT_EQ(
expected_data,
std::string(reinterpret_cast<const char*>(data.data()),
data.size()));
quit_closure.Run();
},
quit_closure, data_str));
}));
runloop.Run();
}
}
// Tests that a WebUI page does not see a resource cached by an open web site.
TEST_P(CodeCacheHostImplTest, WebUiObliviousToOpenWeb) {
base::RunLoop run_loop;
// The URL of a resource loaded by both a site on the one web and a WebUI
// page.
const GURL resource_url("https://best.web.site.com/script.js");
// State for a site on the open web that loads the above resource.
const ChildProcessId kOpenWebProcessId(12);
const GURL open_web_site("https://best.web.site.com/");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
// State for a WebUI page that also loads the above resource.
const ChildProcessId kWebUiProcessId(13);
const GURL web_ui_site(
base::StrCat({kChromeUIScheme, "://some.chrome.page"}));
SetupRendererWithLock(kWebUiProcessId, web_ui_site);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
// Create the open web's cache and put the resource into it.
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
open_web_host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::Time::Now(),
mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
// Create a WebUI page's cache and make sure the resource is absent.
auto web_ui_host = CodeCacheHostImpl::Create(
kWebUiProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{web_ui_site},
net::SchemefulSite{web_ui_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(web_ui_site)));
web_ui_host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::BindLambdaForTesting([&](base::Time found_response_time,
mojo_base::BigBuffer found_data) {
EXPECT_EQ(found_response_time, base::Time());
EXPECT_EQ(found_data.size(), 0U);
run_loop.Quit();
}));
}));
run_loop.Run();
}
// Tests that a page on the open web can't access resources cached for a WebUI
// page.
TEST_P(CodeCacheHostImplTest, OpenWebObliviousToWebUi) {
base::RunLoop run_loop;
// The URL of a resource loaded by a WebUI page.
const GURL resource_url(
base::StrCat({kChromeUIScheme, "://settings/settings.js"}));
// State for a WebUI page that loads the above resource.
const ChildProcessId kWebUiProcessId(12);
const GURL web_ui_site(base::StrCat({kChromeUIScheme, "://settings"}));
SetupRendererWithLock(kWebUiProcessId, web_ui_site);
// State for a site on the open web that wants to modify the cached data for
// the above resource.
const ChildProcessId kOpenWebProcessId(13);
const GURL open_web_site("https://somewhere.com");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
// Create the WebUI page's cache and put the resource into it.
auto web_ui_host = CodeCacheHostImpl::Create(
kWebUiProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{web_ui_site},
net::SchemefulSite{web_ui_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(web_ui_site)));
const base::Time web_ui_resource_time = base::Time::Now();
const auto resource_data = base::byte_span_from_cstring("hi");
web_ui_host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, resource_url,
web_ui_resource_time, mojo_base::BigBuffer(resource_data));
// Create the open web site's cache and make sure the resource is
// absent.
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
base::test::TestFuture<base::Time, mojo_base::BigBuffer> fetch_future;
open_web_host->FetchCachedCode(blink::mojom::CodeCacheType::kJavascript,
resource_url,
fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get<0>(), base::Time());
EXPECT_EQ(fetch_future.Get<1>().size(), 0U);
fetch_future.Clear();
{
mojo::FakeMessageDispatchContext dispatch_context;
// Insert into the open web site's cache in an attempt to corrupt the
// cached data for the WebUI page.
open_web_host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::Time::Now(),
mojo_base::BigBuffer(base::byte_span_from_cstring("nyuknyuk")));
}
// Verify that the WebUI page sees its own cached data if WebUI caching
// is enabled or nothing otherwise.
web_ui_host->FetchCachedCode(blink::mojom::CodeCacheType::kJavascript,
resource_url, fetch_future.GetCallback());
if (base::FeatureList::IsEnabled(features::kWebUICodeCache)) {
EXPECT_EQ(fetch_future.Get<0>(), web_ui_resource_time);
EXPECT_EQ(base::span(fetch_future.Get<1>()), resource_data);
} else {
EXPECT_EQ(fetch_future.Get<0>(), base::Time());
EXPECT_EQ(base::span(fetch_future.Get<1>()).size(), 0U);
}
run_loop.Quit();
}));
run_loop.Run();
}
// Tests that a PDF page does not see a resource cached by an open web site
// on the same origin. Validates that process separation is properly maintained
// in the V8 cache.
TEST_P(CodeCacheHostImplTest, PdfObliviousToOpenWeb) {
base::RunLoop run_loop;
// The URL of a resource loaded by both a site on the open web and a PDF
// page.
const GURL resource_url("https://victim.example.com/script.js");
// State for a site on the open web that loads the above resource.
const ChildProcessId kOpenWebProcessId(12);
const GURL open_web_site("https://victim.example.com/");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
// State for a PDF page that also loads the above resource on the same site.
// Note that this requires setting is_pdf to true in the starting UrlInfo.
const ChildProcessId kPdfProcessId(13);
UrlInfo url_info(
UrlInfoInit(open_web_site)
.WithEmbedderIsolationInfo(EmbedderIsolationInfo::CreateForPdf()));
SetupRendererWithLock(kPdfProcessId, url_info);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
// Create the open web's cache and put the resource into it.
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
open_web_host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::Time::Now(),
mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
// Create a PDF page's cache and make sure the resource is absent.
// It should NOT receive the contents of the HTML's V8 cache!
auto pdf_host = CodeCacheHostImpl::Create(
kPdfProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
pdf_host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::BindLambdaForTesting([&](base::Time found_response_time,
mojo_base::BigBuffer found_data) {
EXPECT_EQ(found_response_time, base::Time());
EXPECT_EQ(found_data.size(), 0U);
run_loop.Quit();
}));
}));
run_loop.Run();
}
// Tests that an origin-restricted sandboxed iframe does not see a resource
// cached by an open web site on the same origin.
TEST_P(CodeCacheHostImplTest, SandboxedIframeObliviousToOpenWeb) {
base::RunLoop run_loop;
// The URL of a resource loaded by both a site on the open web and a
// sandboxed iframe.
const GURL resource_url("https://victim.example.com/script.js");
// State for a site on the open web that loads the above resource.
const ChildProcessId kOpenWebProcessId(12);
const GURL open_web_site("https://victim.example.com/");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
// State for a sandboxed iframe that also loads the above resource on the
// same site. Note that this requires setting is_sandboxed to true in the
// starting UrlInfo.
const ChildProcessId kSandboxedProcessId(13);
UrlInfo url_info(UrlInfoInit(open_web_site).WithSandbox(true));
SetupRendererWithLock(kSandboxedProcessId, url_info);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
// Create the open web's cache and put the resource into it.
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
open_web_host->DidGenerateCacheableMetadata(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::Time::Now(),
mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
// Create a sandboxed iframe's cache and make sure the resource is
// absent. It should NOT receive the contents of the HTML's V8 cache!
auto sandboxed_host = CodeCacheHostImpl::Create(
kSandboxedProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
sandboxed_host->FetchCachedCode(
blink::mojom::CodeCacheType::kJavascript, resource_url,
base::BindLambdaForTesting([&](base::Time found_response_time,
mojo_base::BigBuffer found_data) {
EXPECT_EQ(found_response_time, base::Time());
EXPECT_EQ(found_data.size(), 0U);
run_loop.Quit();
}));
}));
run_loop.Run();
}
TEST_P(CodeCacheHostImplTest, SourceKeyedCacheWebUiObliviousToOpenWeb) {
base::RunLoop run_loop;
const std::string script_source = "console.log(42)";
const std::array<uint8_t, 32> source_hash =
crypto::hash::Sha256(script_source);
static constexpr ChildProcessId kOpenWebProcessId(12);
const GURL open_web_site("https://external.example/");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
static constexpr ChildProcessId kWebUiProcessId(13);
const GURL web_ui_site(
base::StrCat({kChromeUIScheme, "://some.chrome.page"}));
SetupRendererWithLock(kWebUiProcessId, web_ui_site);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
open_web_host->DidGenerateSourceKeyedCacheableMetadata(
std::vector(std::from_range, source_hash),
mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
auto web_ui_host = CodeCacheHostImpl::Create(
kWebUiProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{web_ui_site},
net::SchemefulSite{web_ui_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(web_ui_site)));
base::test::TestFuture<mojo_base::BigBuffer> fetch_future;
web_ui_host->FetchSourceKeyedCachedCodeForTesting(
source_hash, fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get().size(), 0U);
run_loop.Quit();
}));
run_loop.Run();
}
TEST_P(CodeCacheHostImplTest, OpenWebObliviousToSourceKeyedWebUi) {
base::RunLoop run_loop;
const std::string script_source = "console.log(42)";
const std::array<uint8_t, 32> source_hash =
crypto::hash::Sha256(script_source);
static constexpr ChildProcessId kWebUiProcessId(12);
const GURL web_ui_site(base::StrCat({kChromeUIScheme, "://settings"}));
SetupRendererWithLock(kWebUiProcessId, web_ui_site);
static constexpr ChildProcessId kOpenWebProcessId(13);
const GURL open_web_site("https://external.example");
SetupRendererWithLock(kOpenWebProcessId, open_web_site);
GeneratedCodeCacheContext::RunOrPostTask(
generated_code_cache_context_.get(), FROM_HERE,
base::BindLambdaForTesting([&]() {
base::test::TestFuture<mojo_base::BigBuffer> fetch_future;
auto web_ui_host = CodeCacheHostImpl::Create(
kWebUiProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{web_ui_site},
net::SchemefulSite{web_ui_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(web_ui_site)));
web_ui_host->DidGenerateSourceKeyedCacheableMetadata(
std::vector(std::from_range, source_hash),
mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
// WebUI sites cannot store any data.
web_ui_host->FetchSourceKeyedCachedCodeForTesting(
source_hash, fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get().size(), 0U);
fetch_future.Clear();
auto open_web_host = CodeCacheHostImpl::Create(
kOpenWebProcessId, generated_code_cache_context_,
net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
net::SchemefulSite{open_web_site}),
blink::StorageKey::CreateFirstParty(
url::Origin::Create(open_web_site)));
open_web_host->FetchSourceKeyedCachedCodeForTesting(
source_hash, fetch_future.GetCallback());
EXPECT_EQ(fetch_future.Get().size(), 0U);
run_loop.Quit();
}));
run_loop.Run();
}
INSTANTIATE_TEST_SUITE_P(All,
CodeCacheHostImplTest,
testing::Values(true, false));
#endif // !BUILDFLAG(IS_FUCHSIA)
} // namespace content