Browse Source

feat: enable code cache for custom protocols (#40709)

* feat: enable code cache for custom protocols

Co-authored-by: Cheng Zhao <[email protected]>

* chore: update feat_allow_code_cache_in_custom_schemes.patch

* chore: e patches all

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Cheng Zhao <[email protected]>
Co-authored-by: Charles Kerr <[email protected]>
trop[bot] 1 year ago
parent
commit
14c6651e35

+ 3 - 2
docs/api/protocol.md

@@ -61,8 +61,9 @@ The `protocol` module has the following methods:
 module gets emitted and can be called only once.
 
 Registers the `scheme` as standard, secure, bypasses content security policy for
-resources, allows registering ServiceWorker, supports fetch API, and streaming
-video/audio. Specify a privilege with the value of `true` to enable the capability.
+resources, allows registering ServiceWorker, supports fetch API, streaming
+video/audio, and V8 code cache. Specify a privilege with the value of `true` to
+enable the capability.
 
 An example of registering a privileged scheme, that bypasses Content Security
 Policy:

+ 4 - 0
docs/api/session.md

@@ -1356,6 +1356,10 @@ registered.
 Sets the directory to store the generated JS [code cache](https://v8.dev/blog/code-caching-for-devs) for this session. The directory is not required to be created by the user before this call, the runtime will create if it does not exist otherwise will use the existing directory. If directory cannot be created, then code cache will not be used and all operations related to code cache will fail silently inside the runtime. By default, the directory will be `Code Cache` under the
 respective user data folder.
 
+Note that by default code cache is only enabled for http(s) URLs, to enable code
+cache for custom protocols, `codeCache: true` and `standard: true` must be
+specified when registering the protocol.
+
 #### `ses.clearCodeCaches(options)`
 
 * `options` Object

+ 2 - 0
docs/api/structures/custom-scheme.md

@@ -9,3 +9,5 @@
   * `supportFetchAPI` boolean (optional) - Default false.
   * `corsEnabled` boolean (optional) - Default false.
   * `stream` boolean (optional) - Default false.
+  * `codeCache` boolean (optional) - Enable V8 code cache for the scheme, only
+    works when `standard` is also set to true. Default false.

+ 1 - 0
patches/chromium/.patches

@@ -140,3 +140,4 @@ crash_gpu_process_and_clear_shader_cache_when_skia_reports.patch
 revert_same_party_cookie_attribute_removal.patch
 fix_restore_original_resize_performance_on_macos.patch
 fix_font_flooding_in_dev_tools.patch
+feat_allow_code_cache_in_custom_schemes.patch

+ 391 - 0
patches/chromium/feat_allow_code_cache_in_custom_schemes.patch

@@ -0,0 +1,391 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Cheng Zhao <[email protected]>
+Date: Thu, 26 Oct 2023 11:07:47 +0900
+Subject: Enable V8 code cache for custom schemes
+
+Add a new category in ContentClient::AddAdditionalSchemes which allows
+embedders to make custom schemes allow V8 code cache.
+
+Chromium CL: https://chromium-review.googlesource.com/c/chromium/src/+/5019665
+
+diff --git a/content/browser/code_cache/generated_code_cache.cc b/content/browser/code_cache/generated_code_cache.cc
+index 65ac47b199b3f6d37fe78495eeb3d3598c4add8d..5230bf2cf1387ca73b34e0be2e0cffecd46a3653 100644
+--- a/content/browser/code_cache/generated_code_cache.cc
++++ b/content/browser/code_cache/generated_code_cache.cc
+@@ -6,6 +6,7 @@
+ 
+ #include <iostream>
+ 
++#include "base/containers/contains.h"
+ #include "base/feature_list.h"
+ #include "base/functional/bind.h"
+ #include "base/functional/callback_helpers.h"
+@@ -24,6 +25,7 @@
+ #include "net/base/url_util.h"
+ #include "net/http/http_cache.h"
+ #include "url/gurl.h"
++#include "url/url_util.h"
+ 
+ using storage::BigIOBuffer;
+ 
+@@ -46,24 +48,37 @@ void CheckValidKeys(const GURL& resource_url,
+                     GeneratedCodeCache::CodeCacheType cache_type) {
+   // If the resource url is invalid don't cache the code.
+   DCHECK(resource_url.is_valid());
++  bool resource_url_allows_code_cache =
++      base::Contains(url::GetCodeCacheSchemes(), resource_url.scheme());
+   bool resource_url_is_chrome_or_chrome_untrusted =
+       resource_url.SchemeIs(content::kChromeUIScheme) ||
+       resource_url.SchemeIs(content::kChromeUIUntrustedScheme);
+   DCHECK(resource_url.SchemeIsHTTPOrHTTPS() ||
++         resource_url_allows_code_cache ||
+          resource_url_is_chrome_or_chrome_untrusted);
+ 
+-  // |origin_lock| should be either empty or should have
+-  // Http/Https/chrome/chrome-untrusted schemes and it should not be a URL with
+-  // opaque origin. Empty origin_locks are allowed when the renderer is not
+-  // locked to an origin.
++  // |origin_lock| should be either empty or should have code cache allowed
++  // schemes (http/https/chrome/chrome-untrusted or other custom schemes added
++  // by url::AddCodeCacheScheme), and it should not be a URL with opaque
++  // origin. Empty origin_locks are allowed when the renderer is not locked to
++  // an origin.
++  bool origin_lock_allows_code_cache =
++      base::Contains(url::GetCodeCacheSchemes(), origin_lock.scheme());
+   bool origin_lock_is_chrome_or_chrome_untrusted =
+       origin_lock.SchemeIs(content::kChromeUIScheme) ||
+       origin_lock.SchemeIs(content::kChromeUIUntrustedScheme);
+   DCHECK(origin_lock.is_empty() ||
+          ((origin_lock.SchemeIsHTTPOrHTTPS() ||
++           origin_lock_allows_code_cache ||
+            origin_lock_is_chrome_or_chrome_untrusted) &&
+           !url::Origin::Create(origin_lock).opaque()));
+ 
++  // The custom schemes share the cache type with http(s).
++  if (origin_lock_allows_code_cache || resource_url_allows_code_cache) {
++    DCHECK(cache_type == GeneratedCodeCache::kJavaScript ||
++           cache_type == GeneratedCodeCache::kWebAssembly);
++  }
++
+   // The chrome and chrome-untrusted schemes are only used with the WebUI
+   // code cache type.
+   DCHECK_EQ(origin_lock_is_chrome_or_chrome_untrusted,
+diff --git a/content/browser/code_cache/generated_code_cache.h b/content/browser/code_cache/generated_code_cache.h
+index f5c5ff2c89489257003dfe3284ee9de9f517c99b..fdd2e2483171c4d43963590200817dac27d22cf9 100644
+--- a/content/browser/code_cache/generated_code_cache.h
++++ b/content/browser/code_cache/generated_code_cache.h
+@@ -52,12 +52,14 @@ class CONTENT_EXPORT GeneratedCodeCache {
+   // Cache type. Used for collecting statistics for JS and Wasm in separate
+   // buckets.
+   enum CodeCacheType {
+-    // JavaScript from http(s) pages.
++    // JavaScript from pages of http(s) schemes or custom schemes registered by
++    // url::AddCodeCacheScheme.
+     kJavaScript,
+ 
+-    // WebAssembly from http(s) pages. This cache allows more total size and
+-    // more size per item than the JavaScript cache, since some
+-    // WebAssembly programs are very large.
++    // WebAssembly from pages of http(s) schemes or custom schemes registered by
++    // url::AddCodeCacheScheme. This cache allows more total size and more size
++    // per item than the JavaScript cache, since some WebAssembly programs are
++    // very large.
+     kWebAssembly,
+ 
+     // JavaScript from chrome and chrome-untrusted pages. The resource URLs are
+diff --git a/content/browser/code_cache/generated_code_cache_browsertest.cc b/content/browser/code_cache/generated_code_cache_browsertest.cc
+index c06245a4cf0fff1292927482032af5c02ddcbc67..d14ced275db4901e4fe9920b51ed1ce1360ed2d1 100644
+--- a/content/browser/code_cache/generated_code_cache_browsertest.cc
++++ b/content/browser/code_cache/generated_code_cache_browsertest.cc
+@@ -5,17 +5,28 @@
+ #include "base/test/metrics/histogram_tester.h"
+ #include "base/test/scoped_feature_list.h"
+ #include "base/test/test_future.h"
++#include "base/test/test_future.h"
++#include "components/services/storage/storage_service_impl.h"
+ #include "content/browser/code_cache/generated_code_cache.h"
++#include "content/browser/code_cache/generated_code_cache_context.h"
++#include "content/browser/storage_partition_impl.h"
++#include "content/common/url_schemes.h"
++#include "content/public/browser/browser_thread.h"
+ #include "content/public/test/browser_test.h"
+ #include "content/public/test/content_browser_test.h"
+ #include "content/public/test/content_browser_test_utils.h"
++#include "content/public/test/test_browser_context.h"
+ #include "content/shell/browser/shell.h"
++#include "content/test/test_content_client.h"
+ #include "net/dns/mock_host_resolver.h"
++#include "url/url_util.h"
+ 
+ namespace content {
+ 
+ namespace {
+ 
++const std::string kCodeCacheScheme = "test-code-cache";
++
+ bool SupportsSharedWorker() {
+ #if BUILDFLAG(IS_ANDROID)
+   // SharedWorkers are not enabled on Android. https://crbug.com/154571
+@@ -379,4 +390,80 @@ IN_PROC_BROWSER_TEST_P(CodeCacheBrowserTest,
+   }
+ }
+ 
++class CodeCacheInCustomSchemeBrowserTest : public ContentBrowserTest {
++ public:
++  CodeCacheInCustomSchemeBrowserTest() {
++    SetContentClient(&test_content_client_);
++    ReRegisterContentSchemesForTests();
++  }
++
++ private:
++  class CustomSchemeContentClient : public TestContentClient {
++   public:
++    void AddAdditionalSchemes(Schemes* schemes) override {
++      schemes->standard_schemes.push_back(kCodeCacheScheme);
++      schemes->code_cache_schemes.push_back(kCodeCacheScheme);
++    }
++  };
++
++  CustomSchemeContentClient test_content_client_;
++  url::ScopedSchemeRegistryForTests scheme_registry_;
++};
++
++IN_PROC_BROWSER_TEST_F(CodeCacheInCustomSchemeBrowserTest,
++                       AllowedCustomSchemeCanGenerateCodeCache) {
++  // Create browser context and get code cache context.
++  base::ScopedAllowBlockingForTesting allow_blocking;
++  TestBrowserContext browser_context;
++  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
++      browser_context.GetDefaultStoragePartition());
++  scoped_refptr<GeneratedCodeCacheContext> context =
++      partition->GetGeneratedCodeCacheContext();
++  EXPECT_NE(context, nullptr);
++
++  GURL url(kCodeCacheScheme + "://host4/script.js");
++  GURL origin(kCodeCacheScheme + "://host1:1/");
++  ASSERT_TRUE(url.is_valid());
++  ASSERT_TRUE(origin.is_valid());
++  std::string data("SomeData");
++
++  // Add a code cache entry for the custom scheme.
++  base::test::TestFuture<void> add_entry_future;
++  GeneratedCodeCacheContext::RunOrPostTask(
++      context.get(), FROM_HERE,
++      base::BindOnce([](scoped_refptr<GeneratedCodeCacheContext> context,
++                        const GURL& url,
++                        const GURL& origin,
++                        const std::string& data,
++                        base::OnceClosure callback) {
++        context->generated_js_code_cache()->WriteEntry(
++            url, origin, net::NetworkIsolationKey(),
++            base::Time::Now(), std::vector<uint8_t>(data.begin(), data.end()));
++        content::GetUIThreadTaskRunner({})->PostTask(
++            FROM_HERE, std::move(callback));
++      }, context, url, origin, data, add_entry_future.GetCallback()));
++  ASSERT_TRUE(add_entry_future.Wait());
++
++  // Get the code cache entry.
++  base::test::TestFuture<std::string> get_entry_future;
++  GeneratedCodeCacheContext::RunOrPostTask(
++      context.get(), FROM_HERE,
++      base::BindOnce([](scoped_refptr<GeneratedCodeCacheContext> context,
++                        const GURL& url,
++                        const GURL& origin,
++                        base::OnceCallback<void(std::string)> callback) {
++        context->generated_js_code_cache()->FetchEntry(
++            url, origin, net::NetworkIsolationKey(),
++            base::BindOnce([](base::OnceCallback<void(std::string)> callback,
++                              const base::Time& response_time,
++                              mojo_base::BigBuffer buffer) {
++              std::string data(buffer.data(), buffer.data() + buffer.size());
++              content::GetUIThreadTaskRunner({})->PostTask(
++                  FROM_HERE, base::BindOnce(std::move(callback), data));
++            }, std::move(callback)));
++      }, context, url, origin, get_entry_future.GetCallback()));
++  ASSERT_TRUE(get_entry_future.Wait());
++  ASSERT_EQ(data, get_entry_future.Get<0>());
++}
++
+ }  // namespace content
+diff --git a/content/browser/renderer_host/code_cache_host_impl.cc b/content/browser/renderer_host/code_cache_host_impl.cc
+index 6b9e5065dc570b506c4c2606d536319d98684e12..9d1f337b9c9890b6b7afda40bf2f829ff2a25bfd 100644
+--- a/content/browser/renderer_host/code_cache_host_impl.cc
++++ b/content/browser/renderer_host/code_cache_host_impl.cc
+@@ -6,6 +6,7 @@
+ 
+ #include <utility>
+ 
++#include "base/containers/contains.h"
+ #include "base/functional/bind.h"
+ #include "base/functional/callback_helpers.h"
+ #include "base/metrics/histogram_functions.h"
+@@ -28,6 +29,7 @@
+ #include "third_party/blink/public/common/cache_storage/cache_storage_utils.h"
+ #include "url/gurl.h"
+ #include "url/origin.h"
++#include "url/url_util.h"
+ 
+ using blink::mojom::CacheStorageError;
+ 
+@@ -40,6 +42,11 @@ enum class Operation {
+   kWrite,
+ };
+ 
++bool ProcessLockURLIsCodeCacheScheme(const ProcessLock& process_lock) {
++  return base::Contains(url::GetCodeCacheSchemes(),
++                        process_lock.lock_url().scheme());
++}
++
+ bool CheckSecurityForAccessingCodeCacheData(const GURL& resource_url,
+                                             int render_process_id,
+                                             Operation operation) {
+@@ -47,11 +54,12 @@ bool CheckSecurityForAccessingCodeCacheData(const GURL& resource_url,
+       ChildProcessSecurityPolicyImpl::GetInstance()->GetProcessLock(
+           render_process_id);
+ 
+-  // Code caching is only allowed for http(s) and chrome/chrome-untrusted
+-  // scripts. Furthermore, there is no way for http(s) pages to load chrome or
+-  // chrome-untrusted scripts, so any http(s) page attempting to store data
+-  // about a chrome or chrome-untrusted script would be an indication of
+-  // suspicious activity.
++  // Code caching is only allowed for scripts from open-web (http/https and
++  // custom schemes registered with url::AddCodeCacheScheme) and
++  // chrome/chrome-untrusted schemes. Furthermore, there is no way for
++  // open-web pages to load chrome or chrome-untrusted scripts, so any
++  // open-web page attempting to store data about a chrome or
++  // chrome-untrusted script would be an indication of suspicious activity.
+   if (resource_url.SchemeIs(content::kChromeUIScheme) ||
+       resource_url.SchemeIs(content::kChromeUIUntrustedScheme)) {
+     if (!process_lock.is_locked_to_site()) {
+@@ -60,9 +68,10 @@ bool CheckSecurityForAccessingCodeCacheData(const GURL& resource_url,
+       return false;
+     }
+     if (process_lock.matches_scheme(url::kHttpScheme) ||
+-        process_lock.matches_scheme(url::kHttpsScheme)) {
++        process_lock.matches_scheme(url::kHttpsScheme) ||
++        ProcessLockURLIsCodeCacheScheme(process_lock)) {
+       if (operation == Operation::kWrite) {
+-        mojo::ReportBadMessage("HTTP(S) pages cannot cache WebUI code");
++        mojo::ReportBadMessage("Open-web pages cannot cache WebUI code");
+       }
+       return false;
+     }
+@@ -72,7 +81,16 @@ bool CheckSecurityForAccessingCodeCacheData(const GURL& resource_url,
+     return process_lock.matches_scheme(content::kChromeUIScheme) ||
+            process_lock.matches_scheme(content::kChromeUIUntrustedScheme);
+   }
+-  if (resource_url.SchemeIsHTTPOrHTTPS()) {
++  if (base::Contains(url::GetCodeCacheSchemes(), resource_url.scheme()) &&
++      (process_lock.matches_scheme(url::kHttpScheme) ||
++       process_lock.matches_scheme(url::kHttpsScheme))) {
++    // While custom schemes registered with url::AddCodeCacheScheme are
++    // considered as open-web pages, we still do not trust http(s) pages
++    // loading resources from custom schemes.
++    return false;
++  }
++  if (resource_url.SchemeIsHTTPOrHTTPS() ||
++      base::Contains(url::GetCodeCacheSchemes(), resource_url.scheme())) {
+     if (process_lock.matches_scheme(content::kChromeUIScheme) ||
+         process_lock.matches_scheme(content::kChromeUIUntrustedScheme)) {
+       // It is possible for WebUI pages to include open-web content, but such
+@@ -136,15 +154,17 @@ absl::optional<GURL> GetSecondaryKeyForCodeCache(const GURL& resource_url,
+     return absl::nullopt;
+ 
+   // Case 3: process_lock_url is used to enfore site-isolation in code caches.
+-  // Http/https/chrome schemes are safe to be used as a secondary key. Other
+-  // schemes could be enabled if they are known to be safe and if it is
+-  // required to cache code from those origins.
++  // Code cache enabled schemes (http/https/chrome/chrome-untrusted and custom
++  // schemes registered with url::AddCodeCacheScheme) are safe to be used as a
++  // secondary key. Other schemes could be enabled if they are known to be safe
++  // and if it is required to cache code from those origins.
+   //
+   // file:// URLs will have a "file:" process lock and would thus share a
+   // cache across all file:// URLs. That would likely be ok for security, but
+   // since this case is not performance sensitive we will keep things simple and
+-  // limit the cache to http/https/chrome/chrome-untrusted processes.
+-  if (process_lock.matches_scheme(url::kHttpScheme) ||
++  // limit the cache to processes of code cache enabled schemes.
++  if (ProcessLockURLIsCodeCacheScheme(process_lock) ||
++      process_lock.matches_scheme(url::kHttpScheme) ||
+       process_lock.matches_scheme(url::kHttpsScheme) ||
+       process_lock.matches_scheme(content::kChromeUIScheme) ||
+       process_lock.matches_scheme(content::kChromeUIUntrustedScheme)) {
+diff --git a/content/common/url_schemes.cc b/content/common/url_schemes.cc
+index ce9644d33fe83379127b01bf9a2b1c4badc3bc7c..532b14e013b9b65ba390f09e8bb8934bb278a0d8 100644
+--- a/content/common/url_schemes.cc
++++ b/content/common/url_schemes.cc
+@@ -98,6 +98,9 @@ void RegisterContentSchemes(bool should_lock_registry) {
+   for (auto& scheme : schemes.empty_document_schemes)
+     url::AddEmptyDocumentScheme(scheme.c_str());
+ 
++  for (auto& scheme : schemes.code_cache_schemes)
++    url::AddCodeCacheScheme(scheme.c_str());
++
+ #if BUILDFLAG(IS_ANDROID)
+   if (schemes.allow_non_standard_schemes_in_origins)
+     url::EnableNonStandardSchemesForAndroidWebView();
+diff --git a/content/public/common/content_client.h b/content/public/common/content_client.h
+index 26bf98bc78b3a02dedf0b46b7c44d135507cb2d3..095582e753fe7b5012eaab7ade16a317aea20277 100644
+--- a/content/public/common/content_client.h
++++ b/content/public/common/content_client.h
+@@ -139,6 +139,9 @@ class CONTENT_EXPORT ContentClient {
+     // Registers a URL scheme as strictly empty documents, allowing them to
+     // commit synchronously.
+     std::vector<std::string> empty_document_schemes;
++    // Registers a URL scheme whose js and wasm scripts have V8 code cache
++    // enabled.
++    std::vector<std::string> code_cache_schemes;
+     // Registers a URL scheme as extension scheme.
+     std::vector<std::string> extension_schemes;
+     // Registers a URL scheme with a predefined default custom handler.
+diff --git a/url/url_util.cc b/url/url_util.cc
+index 9258cfcfada47aafe6ba20c648187947fec72372..a1834e543d27d46265af0c2133acac79b6c840e2 100644
+--- a/url/url_util.cc
++++ b/url/url_util.cc
+@@ -114,6 +114,9 @@ struct SchemeRegistry {
+       kAboutScheme,
+   };
+ 
++  // Embedder schemes that have V8 code cache enabled in js and wasm scripts.
++  std::vector<std::string> code_cache_schemes = {};
++
+   // Schemes with a predefined default custom handler.
+   std::vector<SchemeWithHandler> predefined_handler_schemes;
+ 
+@@ -659,6 +662,15 @@ const std::vector<std::string>& GetEmptyDocumentSchemes() {
+   return GetSchemeRegistry().empty_document_schemes;
+ }
+ 
++void AddCodeCacheScheme(const char* new_scheme) {
++  DoAddScheme(new_scheme,
++              &GetSchemeRegistryWithoutLocking()->code_cache_schemes);
++}
++
++const std::vector<std::string>& GetCodeCacheSchemes() {
++  return GetSchemeRegistry().code_cache_schemes;
++}
++
+ void AddPredefinedHandlerScheme(const char* new_scheme, const char* handler) {
+   DoAddSchemeWithHandler(
+       new_scheme, handler,
+diff --git a/url/url_util.h b/url/url_util.h
+index 8c94c7a4f6d5f653d326199e5b43452f969911d4..40dcdf9d680f9d345c09426da48b37f288234244 100644
+--- a/url/url_util.h
++++ b/url/url_util.h
+@@ -115,6 +115,15 @@ COMPONENT_EXPORT(URL) const std::vector<std::string>& GetCSPBypassingSchemes();
+ COMPONENT_EXPORT(URL) void AddEmptyDocumentScheme(const char* new_scheme);
+ COMPONENT_EXPORT(URL) const std::vector<std::string>& GetEmptyDocumentSchemes();
+ 
++// Adds an application-defined scheme to the list of schemes that have V8 code
++// cache enabled for the js and wasm scripts.
++// The WebUI schemes (chrome/chrome-untrusted) do not belong to this list, as
++// they are treated as a separate cache type for security purpose.
++// The http(s) schemes do not belong to this list neither, they always have V8
++// code cache enabled and can not load scripts from schemes in this list.
++COMPONENT_EXPORT(URL) void AddCodeCacheScheme(const char* new_scheme);
++COMPONENT_EXPORT(URL) const std::vector<std::string>& GetCodeCacheSchemes();
++
+ // Adds a scheme with a predefined default handler.
+ //
+ // This pair of strings must be normalized protocol handler parameters as

+ 27 - 1
shell/browser/api/electron_api_protocol.cc

@@ -32,6 +32,9 @@ std::vector<std::string> g_standard_schemes;
 // List of registered custom streaming schemes.
 std::vector<std::string> g_streaming_schemes;
 
+// Schemes that support V8 code cache.
+std::vector<std::string> g_code_cache_schemes;
+
 struct SchemeOptions {
   bool standard = false;
   bool secure = false;
@@ -40,6 +43,7 @@ struct SchemeOptions {
   bool supportFetchAPI = false;
   bool corsEnabled = false;
   bool stream = false;
+  bool codeCache = false;
 };
 
 struct CustomScheme {
@@ -71,6 +75,7 @@ struct Converter<CustomScheme> {
       opt.Get("supportFetchAPI", &(out->options.supportFetchAPI));
       opt.Get("corsEnabled", &(out->options.corsEnabled));
       opt.Get("stream", &(out->options.stream));
+      opt.Get("codeCache", &(out->options.codeCache));
     }
     return true;
   }
@@ -82,10 +87,14 @@ namespace electron::api {
 
 gin::WrapperInfo Protocol::kWrapperInfo = {gin::kEmbedderNativeGin};
 
-std::vector<std::string> GetStandardSchemes() {
+const std::vector<std::string>& GetStandardSchemes() {
   return g_standard_schemes;
 }
 
+const std::vector<std::string>& GetCodeCacheSchemes() {
+  return g_code_cache_schemes;
+}
+
 void AddServiceWorkerScheme(const std::string& scheme) {
   // There is no API to add service worker scheme, but there is an API to
   // return const reference to the schemes vector.
@@ -104,6 +113,15 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
     return;
   }
 
+  for (const auto& custom_scheme : custom_schemes) {
+    if (custom_scheme.options.codeCache && !custom_scheme.options.standard) {
+      thrower.ThrowError(
+          "Code cache can only be enabled when the custom scheme is registered "
+          "as standard scheme.");
+      return;
+    }
+  }
+
   std::vector<std::string> secure_schemes, cspbypassing_schemes, fetch_schemes,
       service_worker_schemes, cors_schemes;
   for (const auto& custom_scheme : custom_schemes) {
@@ -137,10 +155,16 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
     if (custom_scheme.options.stream) {
       g_streaming_schemes.push_back(custom_scheme.scheme);
     }
+    if (custom_scheme.options.codeCache) {
+      g_code_cache_schemes.push_back(custom_scheme.scheme);
+      url::AddCodeCacheScheme(custom_scheme.scheme.c_str());
+    }
   }
 
   const auto AppendSchemesToCmdLine = [](const char* switch_name,
                                          std::vector<std::string> schemes) {
+    if (schemes.empty())
+      return;
     // Add the schemes to command line switches, so child processes can also
     // register them.
     base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
@@ -158,6 +182,8 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
                          g_standard_schemes);
   AppendSchemesToCmdLine(electron::switches::kStreamingSchemes,
                          g_streaming_schemes);
+  AppendSchemesToCmdLine(electron::switches::kCodeCacheSchemes,
+                         g_code_cache_schemes);
 }
 
 namespace {

+ 2 - 1
shell/browser/api/electron_api_protocol.h

@@ -22,7 +22,8 @@ class ProtocolRegistry;
 
 namespace api {
 
-std::vector<std::string> GetStandardSchemes();
+const std::vector<std::string>& GetStandardSchemes();
+const std::vector<std::string>& GetCodeCacheSchemes();
 
 void AddServiceWorkerScheme(const std::string& scheme);
 

+ 3 - 2
shell/browser/electron_browser_client.cc

@@ -534,7 +534,8 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
         switches::kStandardSchemes,      switches::kEnableSandbox,
         switches::kSecureSchemes,        switches::kBypassCSPSchemes,
         switches::kCORSSchemes,          switches::kFetchSchemes,
-        switches::kServiceWorkerSchemes, switches::kStreamingSchemes};
+        switches::kServiceWorkerSchemes, switches::kStreamingSchemes,
+        switches::kCodeCacheSchemes};
     command_line->CopySwitchesFrom(*base::CommandLine::ForCurrentProcess(),
                                    kCommonSwitchNames);
     if (process_type == ::switches::kUtilityProcess ||
@@ -702,7 +703,7 @@ ElectronBrowserClient::CreateWindowForVideoPictureInPicture(
 
 void ElectronBrowserClient::GetAdditionalAllowedSchemesForFileSystem(
     std::vector<std::string>* additional_schemes) {
-  auto schemes_list = api::GetStandardSchemes();
+  const auto& schemes_list = api::GetStandardSchemes();
   if (!schemes_list.empty())
     additional_schemes->insert(additional_schemes->end(), schemes_list.begin(),
                                schemes_list.end());

+ 3 - 0
shell/common/options_switches.cc

@@ -227,6 +227,9 @@ const char kCORSSchemes[] = "cors-schemes";
 // Register schemes as streaming responses.
 const char kStreamingSchemes[] = "streaming-schemes";
 
+// Register schemes as supporting V8 code cache.
+const char kCodeCacheSchemes[] = "code-cache-schemes";
+
 // The browser process app model ID
 const char kAppUserModelId[] = "app-user-model-id";
 

+ 1 - 0
shell/common/options_switches.h

@@ -113,6 +113,7 @@ extern const char kBypassCSPSchemes[];
 extern const char kFetchSchemes[];
 extern const char kCORSSchemes[];
 extern const char kStreamingSchemes[];
+extern const char kCodeCacheSchemes[];
 extern const char kAppUserModelId[];
 extern const char kAppPath[];
 

+ 7 - 0
shell/renderer/renderer_client_base.cc

@@ -275,6 +275,13 @@ void RendererClientBase::RenderThreadStarted() {
     blink::SchemeRegistry::RegisterURLSchemeAsBypassingContentSecurityPolicy(
         WTF::String::FromUTF8(scheme.data(), scheme.length()));
 
+  std::vector<std::string> code_cache_schemes_list =
+      ParseSchemesCLISwitch(command_line, switches::kCodeCacheSchemes);
+  for (const auto& scheme : code_cache_schemes_list) {
+    blink::WebSecurityPolicy::RegisterURLSchemeAsCodeCacheWithHashing(
+        blink::WebString::FromASCII(scheme));
+  }
+
   // Allow file scheme to handle service worker by default.
   // FIXME(zcbenz): Can this be moved elsewhere?
   blink::WebSecurityPolicy::RegisterURLSchemeAsAllowingServiceWorkers("file");

+ 27 - 0
spec/api-protocol-spec.ts

@@ -1090,6 +1090,33 @@ describe('protocol module', () => {
     }
   });
 
+  describe('protocol.registerSchemesAsPrivileged codeCache', function () {
+    const temp = require('temp').track();
+    const appPath = path.join(fixturesPath, 'apps', 'refresh-page');
+
+    let w: BrowserWindow;
+    let codeCachePath: string;
+    beforeEach(async () => {
+      w = new BrowserWindow({ show: false });
+      codeCachePath = temp.path();
+    });
+
+    afterEach(async () => {
+      await closeWindow(w);
+      w = null as unknown as BrowserWindow;
+    });
+
+    it('code cache in custom protocol is disabled by default', async () => {
+      ChildProcess.spawnSync(process.execPath, [appPath, 'false', codeCachePath]);
+      expect(fs.readdirSync(path.join(codeCachePath, 'js')).length).to.equal(2);
+    });
+
+    it('codeCache:true enables codeCache in custom protocol', async () => {
+      ChildProcess.spawnSync(process.execPath, [appPath, 'true', codeCachePath]);
+      expect(fs.readdirSync(path.join(codeCachePath, 'js')).length).to.above(2);
+    });
+  });
+
   describe('handle', () => {
     afterEach(closeAllWindows);
 

+ 9 - 0
spec/fixtures/apps/refresh-page/main.html

@@ -0,0 +1,9 @@
+<html>
+<body>
+  <!-- Use mocha which has a large enough js file -->
+  <script src="mocha.js"></script>
+  <script>
+    mocha.setup('bdd');
+  </script>
+</body>
+</html>

+ 38 - 0
spec/fixtures/apps/refresh-page/main.js

@@ -0,0 +1,38 @@
+const path = require('node:path');
+const { once } = require('node:events');
+const { pathToFileURL } = require('node:url');
+const { BrowserWindow, app, protocol, net, session } = require('electron');
+
+if (process.argv.length < 4) {
+  console.error('Must pass allow_code_cache code_cache_dir');
+  process.exit(1);
+}
+
+protocol.registerSchemesAsPrivileged([
+  {
+    scheme: 'atom',
+    privileges: {
+      standard: true,
+      codeCache: process.argv[2] === 'true'
+    }
+  }
+]);
+
+app.once('ready', async () => {
+  const codeCachePath = process.argv[3];
+  session.defaultSession.setCodeCachePath(codeCachePath);
+
+  protocol.handle('atom', (request) => {
+    let { pathname } = new URL(request.url);
+    if (pathname === '/mocha.js') { pathname = path.resolve(__dirname, '../../../node_modules/mocha/mocha.js'); } else { pathname = path.join(__dirname, pathname); }
+    return net.fetch(pathToFileURL(pathname).toString());
+  });
+
+  const win = new BrowserWindow({ show: false });
+  win.loadURL('atom://host/main.html');
+  await once(win.webContents, 'did-finish-load');
+  // Reload to generate code cache.
+  win.reload();
+  await once(win.webContents, 'did-finish-load');
+  app.exit();
+});

+ 4 - 0
spec/fixtures/apps/refresh-page/package.json

@@ -0,0 +1,4 @@
+{
+  "name": "electron-test-refresh-page",
+  "main": "main.js"
+}