| // 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 |