Browse Source

feat: preload scripts for service workers

Samuel Maddock 5 months ago
parent
commit
e420260647

+ 11 - 0
BUILD.gn

@@ -224,11 +224,21 @@ webpack_build("electron_utility_bundle") {
   out_file = "$target_gen_dir/js2c/utility_init.js"
 }
 
+webpack_build("electron_preload_realm_bundle") {
+  deps = [ ":build_electron_definitions" ]
+
+  inputs = auto_filenames.preload_realm_bundle_deps
+
+  config_file = "//electron/build/webpack/webpack.config.preload_realm.js"
+  out_file = "$target_gen_dir/js2c/preload_realm_bundle.js"
+}
+
 action("electron_js2c") {
   deps = [
     ":electron_browser_bundle",
     ":electron_isolated_renderer_bundle",
     ":electron_node_bundle",
+    ":electron_preload_realm_bundle",
     ":electron_renderer_bundle",
     ":electron_sandboxed_renderer_bundle",
     ":electron_utility_bundle",
@@ -240,6 +250,7 @@ action("electron_js2c") {
     "$target_gen_dir/js2c/browser_init.js",
     "$target_gen_dir/js2c/isolated_bundle.js",
     "$target_gen_dir/js2c/node_init.js",
+    "$target_gen_dir/js2c/preload_realm_bundle.js",
     "$target_gen_dir/js2c/renderer_init.js",
     "$target_gen_dir/js2c/sandbox_bundle.js",
     "$target_gen_dir/js2c/utility_init.js",

+ 6 - 0
build/webpack/webpack.config.preload_realm.js

@@ -0,0 +1,6 @@
+module.exports = require('./webpack.config.base')({
+  target: 'preload_realm',
+  alwaysHasNode: false,
+  wrapInitWithProfilingTimeout: true,
+  wrapInitWithTryCatch: true
+});

+ 1 - 0
docs/api/process.md

@@ -114,6 +114,7 @@ A `string` representing the current process's type, can be:
 
 * `browser` - The main process
 * `renderer` - A renderer process
+* `service-worker` - In a service worker
 * `worker` - In a web worker
 * `utility` - In a node process launched as a service
 

+ 22 - 0
filenames.auto.gni

@@ -184,6 +184,7 @@ auto_filenames = {
     "lib/sandboxed_renderer/api/exports/electron.ts",
     "lib/sandboxed_renderer/api/module-list.ts",
     "lib/sandboxed_renderer/init.ts",
+    "lib/sandboxed_renderer/preload.ts",
     "package.json",
     "tsconfig.electron.json",
     "tsconfig.json",
@@ -374,4 +375,25 @@ auto_filenames = {
     "typings/internal-ambient.d.ts",
     "typings/internal-electron.d.ts",
   ]
+
+  preload_realm_bundle_deps = [
+    "lib/common/api/native-image.ts",
+    "lib/common/define-properties.ts",
+    "lib/common/ipc-messages.ts",
+    "lib/common/webpack-globals-provider.ts",
+    "lib/preload_realm/api/exports/electron.ts",
+    "lib/preload_realm/api/module-list.ts",
+    "lib/preload_realm/init.ts",
+    "lib/renderer/api/context-bridge.ts",
+    "lib/renderer/api/ipc-renderer.ts",
+    "lib/renderer/ipc-native-setup.ts",
+    "lib/renderer/ipc-renderer-internal-utils.ts",
+    "lib/renderer/ipc-renderer-internal.ts",
+    "lib/sandboxed_renderer/preload.ts",
+    "package.json",
+    "tsconfig.electron.json",
+    "tsconfig.json",
+    "typings/internal-ambient.d.ts",
+    "typings/internal-electron.d.ts",
+  ]
 }

+ 4 - 0
filenames.gni

@@ -717,6 +717,10 @@ filenames = {
     "shell/renderer/electron_renderer_client.h",
     "shell/renderer/electron_sandboxed_renderer_client.cc",
     "shell/renderer/electron_sandboxed_renderer_client.h",
+    "shell/renderer/preload_realm_context.cc",
+    "shell/renderer/preload_realm_context.h",
+    "shell/renderer/preload_utils.cc",
+    "shell/renderer/preload_utils.h",
     "shell/renderer/renderer_client_base.cc",
     "shell/renderer/renderer_client_base.h",
     "shell/renderer/web_worker_observer.cc",

+ 18 - 0
lib/preload_realm/.eslintrc.json

@@ -0,0 +1,18 @@
+{
+  "rules": {
+    "no-restricted-imports": [
+      "error",
+      {
+        "paths": [
+          "electron",
+          "electron/main"
+        ],
+        "patterns": [
+          "./*",
+          "../*",
+          "@electron/internal/browser/*"
+        ]
+      }
+    ]
+  }
+}

+ 6 - 0
lib/preload_realm/api/exports/electron.ts

@@ -0,0 +1,6 @@
+import { defineProperties } from '@electron/internal/common/define-properties';
+import { moduleList } from '@electron/internal/preload_realm/api/module-list';
+
+module.exports = {};
+
+defineProperties(module.exports, moduleList);

+ 14 - 0
lib/preload_realm/api/module-list.ts

@@ -0,0 +1,14 @@
+export const moduleList: ElectronInternal.ModuleEntry[] = [
+  {
+    name: 'contextBridge',
+    loader: () => require('@electron/internal/renderer/api/context-bridge')
+  },
+  {
+    name: 'ipcRenderer',
+    loader: () => require('@electron/internal/renderer/api/ipc-renderer')
+  },
+  {
+    name: 'nativeImage',
+    loader: () => require('@electron/internal/common/api/native-image')
+  }
+];

+ 73 - 0
lib/preload_realm/init.ts

@@ -0,0 +1,73 @@
+import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
+import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
+import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
+
+import * as events from 'events';
+
+declare const binding: {
+  get: (name: string) => any;
+  process: NodeJS.Process;
+  createPreloadScript: (src: string) => Function
+};
+
+const { EventEmitter } = events;
+
+process._linkedBinding = binding.get;
+
+const v8Util = process._linkedBinding('electron_common_v8_util');
+// Expose Buffer shim as a hidden value. This is used by C++ code to
+// deserialize Buffer instances sent from browser process.
+v8Util.setHiddenValue(global, 'Buffer', Buffer);
+// The process object created by webpack is not an event emitter, fix it so
+// the API is more compatible with non-sandboxed renderers.
+for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) {
+  if (Object.hasOwn(process, prop)) {
+    delete process[prop];
+  }
+}
+Object.setPrototypeOf(process, EventEmitter.prototype);
+
+const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
+
+const {
+  preloadScripts,
+  process: processProps
+} = ipcRendererUtils.invokeSync<{
+  preloadScripts: ElectronInternal.PreloadScript[];
+  process: NodeJS.Process;
+}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
+
+const electron = require('electron');
+
+const loadedModules = new Map<string, any>([
+  ['electron', electron],
+  ['electron/common', electron],
+  ['events', events],
+  ['node:events', events]
+]);
+
+const loadableModules = new Map<string, Function>([
+  ['url', () => require('url')],
+  ['node:url', () => require('url')]
+]);
+
+const preloadProcess = createPreloadProcessObject();
+
+Object.assign(preloadProcess, binding.process);
+Object.assign(preloadProcess, processProps);
+
+Object.assign(process, binding.process);
+Object.assign(process, processProps);
+
+require('@electron/internal/renderer/ipc-native-setup');
+
+executeSandboxedPreloadScripts({
+  loadedModules: loadedModules,
+  loadableModules: loadableModules,
+  process: preloadProcess,
+  createPreloadScript: binding.createPreloadScript,
+  exposeGlobals: {
+    Buffer: Buffer,
+    global: global
+  }
+}, preloadScripts);

+ 16 - 78
lib/sandboxed_renderer/init.ts

@@ -1,6 +1,6 @@
 import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
-import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal';
 import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
+import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
 
 import * as events from 'events';
 import { setImmediate, clearImmediate } from 'timers';
@@ -28,18 +28,13 @@ for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)
 }
 Object.setPrototypeOf(process, EventEmitter.prototype);
 
-const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
 const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
 
 const {
   preloadScripts,
   process: processProps
 } = ipcRendererUtils.invokeSync<{
-  preloadScripts: {
-    preloadPath: string;
-    preloadSrc: string | null;
-    preloadError: null | Error;
-  }[];
+  preloadScripts: ElectronInternal.PreloadScript[];
   process: NodeJS.Process;
 }>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
 
@@ -60,8 +55,7 @@ const loadableModules = new Map<string, Function>([
   ['node:url', () => require('url')]
 ]);
 
-// Pass different process object to the preload script.
-const preloadProcess: NodeJS.Process = new EventEmitter() as any;
+const preloadProcess = createPreloadProcessObject();
 
 // InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this
 v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
@@ -69,80 +63,24 @@ v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
   (preloadProcess as events.EventEmitter).emit(event);
 });
 
-Object.assign(preloadProcess, binding.process);
-Object.assign(preloadProcess, processProps);
-
 Object.assign(process, binding.process);
 Object.assign(process, processProps);
 
-process.getProcessMemoryInfo = preloadProcess.getProcessMemoryInfo = () => {
-  return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
-};
-
-Object.defineProperty(preloadProcess, 'noDeprecation', {
-  get () {
-    return process.noDeprecation;
-  },
-  set (value) {
-    process.noDeprecation = value;
-  }
-});
-
-// This is the `require` function that will be visible to the preload script
-function preloadRequire (module: string) {
-  if (loadedModules.has(module)) {
-    return loadedModules.get(module);
-  }
-  if (loadableModules.has(module)) {
-    const loadedModule = loadableModules.get(module)!();
-    loadedModules.set(module, loadedModule);
-    return loadedModule;
-  }
-  throw new Error(`module not found: ${module}`);
-}
-
-// Process command line arguments.
-const { hasSwitch } = process._linkedBinding('electron_common_command_line');
-
-// Similar to nodes --expose-internals flag, this exposes _linkedBinding so
-// that tests can call it to get access to some test only bindings
-if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
-  preloadProcess._linkedBinding = process._linkedBinding;
-}
+Object.assign(preloadProcess, binding.process);
+Object.assign(preloadProcess, processProps);
 
 // Common renderer initialization
 require('@electron/internal/renderer/common-init');
 
-// Wrap the script into a function executed in global scope. It won't have
-// access to the current scope, so we'll expose a few objects as arguments:
-//
-// - `require`: The `preloadRequire` function
-// - `process`: The `preloadProcess` object
-// - `Buffer`: Shim of `Buffer` implementation
-// - `global`: The window object, which is aliased to `global` by webpack.
-function runPreloadScript (preloadSrc: string) {
-  const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) {
-  ${preloadSrc}
-  })`;
-
-  // eval in window scope
-  const preloadFn = binding.createPreloadScript(preloadWrapperSrc);
-  const exports = {};
-
-  preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate, exports, { exports });
-}
-
-for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) {
-  try {
-    if (preloadSrc) {
-      runPreloadScript(preloadSrc);
-    } else if (preloadError) {
-      throw preloadError;
-    }
-  } catch (error) {
-    console.error(`Unable to load preload script: ${preloadPath}`);
-    console.error(error);
-
-    ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadPath, error);
+executeSandboxedPreloadScripts({
+  loadedModules: loadedModules,
+  loadableModules: loadableModules,
+  process: preloadProcess,
+  createPreloadScript: binding.createPreloadScript,
+  exposeGlobals: {
+    Buffer: Buffer,
+    global: global,
+    setImmediate: setImmediate,
+    clearImmediate: clearImmediate
   }
-}
+}, preloadScripts);

+ 107 - 0
lib/sandboxed_renderer/preload.ts

@@ -0,0 +1,107 @@
+import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
+import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal';
+
+import { EventEmitter } from 'events';
+
+// Delay loading for `process._linkedBinding` to be set.
+const getIpcRendererLazy = () => require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
+
+interface PreloadContext {
+  loadedModules: Map<string, any>;
+  loadableModules: Map<string, any>;
+
+  /** Process object to pass into preloads. */
+  process: NodeJS.Process;
+
+  createPreloadScript: (src: string) => Function
+
+  /** Globals to be exposed to preload context. */
+  exposeGlobals: any;
+}
+
+export function createPreloadProcessObject (): NodeJS.Process {
+  const preloadProcess: NodeJS.Process = new EventEmitter() as any;
+
+  preloadProcess.getProcessMemoryInfo = () => {
+    const { ipcRendererInternal } = getIpcRendererLazy();
+    return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
+  };
+
+  Object.defineProperty(preloadProcess, 'noDeprecation', {
+    get () {
+      return process.noDeprecation;
+    },
+    set (value) {
+      process.noDeprecation = value;
+    }
+  });
+
+  const { hasSwitch } = process._linkedBinding('electron_common_command_line');
+
+  // Similar to nodes --expose-internals flag, this exposes _linkedBinding so
+  // that tests can call it to get access to some test only bindings
+  if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
+    preloadProcess._linkedBinding = process._linkedBinding;
+  }
+
+  return preloadProcess;
+}
+
+// This is the `require` function that will be visible to the preload script
+function preloadRequire (context: PreloadContext, module: string) {
+  if (context.loadedModules.has(module)) {
+    return context.loadedModules.get(module);
+  }
+  if (context.loadableModules.has(module)) {
+    const loadedModule = context.loadableModules.get(module)!();
+    context.loadedModules.set(module, loadedModule);
+    return loadedModule;
+  }
+  throw new Error(`module not found: ${module}`);
+}
+
+// Wrap the script into a function executed in global scope. It won't have
+// access to the current scope, so we'll expose a few objects as arguments:
+//
+// - `require`: The `preloadRequire` function
+// - `process`: The `preloadProcess` object
+// - `Buffer`: Shim of `Buffer` implementation
+// - `global`: The window object, which is aliased to `global` by webpack.
+function runPreloadScript (context: PreloadContext, preloadSrc: string) {
+  const globalVariables = [];
+  const fnParameters = [];
+  for (const [key, value] of Object.entries(context.exposeGlobals)) {
+    globalVariables.push(key);
+    fnParameters.push(value);
+  }
+  const preloadWrapperSrc = `(function(require, process, exports, module, ${globalVariables.join(', ')}) {
+  ${preloadSrc}
+  })`;
+
+  // eval in window scope
+  const preloadFn = context.createPreloadScript(preloadWrapperSrc);
+  const exports = {};
+
+  preloadFn(preloadRequire.bind(null, context), context.process, exports, { exports }, ...fnParameters);
+}
+
+/**
+ * Execute preload scripts within a sandboxed process.
+ */
+export function executeSandboxedPreloadScripts (context: PreloadContext, preloadScripts: ElectronInternal.PreloadScript[]) {
+  for (const { filePath, contents, error } of preloadScripts) {
+    try {
+      if (contents) {
+        runPreloadScript(context, contents);
+      } else if (error) {
+        throw error;
+      }
+    } catch (error) {
+      console.error(`Unable to load preload script: ${filePath}`);
+      console.error(error);
+
+      const { ipcRendererInternal } = getIpcRendererLazy();
+      ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, filePath, error);
+    }
+  }
+}

+ 4 - 0
script/gen-filenames.ts

@@ -44,6 +44,10 @@ const main = async () => {
     {
       name: 'utility_bundle_deps',
       config: 'webpack.config.utility.js'
+    },
+    {
+      name: 'preload_realm_bundle_deps',
+      config: 'webpack.config.preload_realm.js'
     }
   ];
 

+ 12 - 0
shell/browser/electron_browser_client.cc

@@ -578,6 +578,18 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
         web_preferences->AppendCommandLineSwitches(
             command_line, IsRendererSubFrame(process_id));
     }
+
+    // Service worker processes should only run preloads if one has been
+    // registered prior to startup.
+    auto* render_process_host = content::RenderProcessHost::FromID(process_id);
+    if (render_process_host) {
+      auto* browser_context = render_process_host->GetBrowserContext();
+      auto* session_prefs =
+          SessionPreferences::FromBrowserContext(browser_context);
+      if (session_prefs->HasServiceWorkerPreloadScript()) {
+        command_line->AppendSwitch(switches::kServiceWorkerPreload);
+      }
+    }
   }
 }
 

+ 9 - 0
shell/browser/session_preferences.cc

@@ -30,4 +30,13 @@ SessionPreferences* SessionPreferences::FromBrowserContext(
   return static_cast<SessionPreferences*>(context->GetUserData(&kLocatorKey));
 }
 
+bool SessionPreferences::HasServiceWorkerPreloadScript() {
+  const auto& preloads = preload_scripts();
+  auto it = std::find_if(
+      preloads.begin(), preloads.end(), [](const PreloadScript& script) {
+        return script.script_type == PreloadScript::ScriptType::kServiceWorker;
+      });
+  return it != preloads.end();
+}
+
 }  // namespace electron

+ 2 - 0
shell/browser/session_preferences.h

@@ -28,6 +28,8 @@ class SessionPreferences : public base::SupportsUserData::Data {
 
   std::vector<PreloadScript>& preload_scripts() { return preload_scripts_; }
 
+  bool HasServiceWorkerPreloadScript();
+
  private:
   SessionPreferences();
 

+ 4 - 1
shell/common/gin_helper/callback.cc

@@ -33,7 +33,10 @@ struct TranslatorHolder {
 };
 
 // Cached JavaScript version of |CallTranslator|.
-v8::Persistent<v8::FunctionTemplate> g_call_translator;
+// v8::Persistent handles are bound to a specific v8::Isolate. Require
+// initializing per-thread to avoid using the wrong isolate from service
+// worker preload scripts.
+thread_local v8::Persistent<v8::FunctionTemplate> g_call_translator;
 
 void CallTranslator(v8::Local<v8::External> external,
                     v8::Local<v8::Object> state,

+ 4 - 0
shell/common/options_switches.h

@@ -288,6 +288,10 @@ inline constexpr base::cstring_view kEnableAuthNegotiatePort =
 // If set, NTLM v2 is disabled for POSIX platforms.
 inline constexpr base::cstring_view kDisableNTLMv2 = "disable-ntlm-v2";
 
+// Indicates that preloads for service workers are registered.
+inline constexpr base::cstring_view kServiceWorkerPreload =
+    "service-worker-preload";
+
 }  // namespace switches
 
 }  // namespace electron

+ 33 - 65
shell/renderer/electron_sandboxed_renderer_client.cc

@@ -11,18 +11,18 @@
 #include "base/base_paths.h"
 #include "base/command_line.h"
 #include "base/containers/contains.h"
-#include "base/process/process_handle.h"
 #include "base/process/process_metrics.h"
 #include "content/public/renderer/render_frame.h"
 #include "shell/common/api/electron_bindings.h"
 #include "shell/common/application_info.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/gin_helper/microtasks_scope.h"
-#include "shell/common/node_bindings.h"
 #include "shell/common/node_includes.h"
 #include "shell/common/node_util.h"
 #include "shell/common/options_switches.h"
 #include "shell/renderer/electron_render_frame_observer.h"
+#include "shell/renderer/preload_realm_context.h"
+#include "shell/renderer/preload_utils.h"
 #include "third_party/blink/public/common/web_preferences/web_preferences.h"
 #include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
 #include "third_party/blink/public/web/blink.h"
@@ -34,66 +34,6 @@ namespace electron {
 namespace {
 
 constexpr std::string_view kEmitProcessEventKey = "emit-process-event";
-constexpr std::string_view kBindingCacheKey = "native-binding-cache";
-
-v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
-  auto context = isolate->GetCurrentContext();
-  gin_helper::Dictionary global(isolate, context->Global());
-  v8::Local<v8::Value> cache;
-
-  if (!global.GetHidden(kBindingCacheKey, &cache)) {
-    cache = v8::Object::New(isolate);
-    global.SetHidden(kBindingCacheKey, cache);
-  }
-
-  return cache->ToObject(context).ToLocalChecked();
-}
-
-// adapted from node.cc
-v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
-                                v8::Local<v8::String> key,
-                                gin_helper::Arguments* margs) {
-  v8::Local<v8::Object> exports;
-  std::string binding_key = gin::V8ToString(isolate, key);
-  gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
-
-  if (cache.Get(binding_key, &exports)) {
-    return exports;
-  }
-
-  auto* mod = node::binding::get_linked_module(binding_key.c_str());
-
-  if (!mod) {
-    char errmsg[1024];
-    snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
-             binding_key.c_str());
-    margs->ThrowError(errmsg);
-    return exports;
-  }
-
-  exports = v8::Object::New(isolate);
-  DCHECK_EQ(mod->nm_register_func, nullptr);
-  DCHECK_NE(mod->nm_context_register_func, nullptr);
-  mod->nm_context_register_func(exports, v8::Null(isolate),
-                                isolate->GetCurrentContext(), mod->nm_priv);
-  cache.Set(binding_key, exports);
-  return exports;
-}
-
-v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
-                                         v8::Local<v8::String> source) {
-  auto context = isolate->GetCurrentContext();
-  auto maybe_script = v8::Script::Compile(context, source);
-  v8::Local<v8::Script> script;
-  if (!maybe_script.ToLocal(&script))
-    return {};
-  return script->Run(context).ToLocalChecked();
-}
-
-double Uptime() {
-  return (base::Time::Now() - base::Process::Current().CreationTime())
-      .InSecondsF();
-}
 
 void InvokeEmitProcessEvent(v8::Local<v8::Context> context,
                             const std::string& event_name) {
@@ -132,8 +72,8 @@ void ElectronSandboxedRendererClient::InitializeBindings(
     content::RenderFrame* render_frame) {
   auto* isolate = context->GetIsolate();
   gin_helper::Dictionary b(isolate, binding);
-  b.SetMethod("get", GetBinding);
-  b.SetMethod("createPreloadScript", CreatePreloadScript);
+  b.SetMethod("get", preload_utils::GetBinding);
+  b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
 
   auto process = gin_helper::Dictionary::CreateEmpty(isolate);
   b.Set("process", process);
@@ -141,7 +81,7 @@ void ElectronSandboxedRendererClient::InitializeBindings(
   ElectronBindings::BindProcess(isolate, &process, metrics_.get());
   BindProcess(isolate, &process, render_frame);
 
-  process.SetMethod("uptime", Uptime);
+  process.SetMethod("uptime", preload_utils::Uptime);
   process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
   process.SetReadOnly("pid", base::GetCurrentProcId());
   process.SetReadOnly("sandboxed", true);
@@ -231,4 +171,32 @@ void ElectronSandboxedRendererClient::EmitProcessEvent(
   InvokeEmitProcessEvent(context, event_name);
 }
 
+void ElectronSandboxedRendererClient::WillEvaluateServiceWorkerOnWorkerThread(
+    blink::WebServiceWorkerContextProxy* context_proxy,
+    v8::Local<v8::Context> v8_context,
+    int64_t service_worker_version_id,
+    const GURL& service_worker_scope,
+    const GURL& script_url,
+    const blink::ServiceWorkerToken& service_worker_token) {
+  RendererClientBase::WillEvaluateServiceWorkerOnWorkerThread(
+      context_proxy, v8_context, service_worker_version_id,
+      service_worker_scope, script_url, service_worker_token);
+
+  auto* command_line = base::CommandLine::ForCurrentProcess();
+  if (command_line->HasSwitch(switches::kServiceWorkerPreload)) {
+    preload_realm::OnCreatePreloadableV8Context(v8_context,
+                                                service_worker_data);
+  }
+}
+
+void ElectronSandboxedRendererClient::
+    WillDestroyServiceWorkerContextOnWorkerThread(
+        v8::Local<v8::Context> context,
+        int64_t service_worker_version_id,
+        const GURL& service_worker_scope,
+        const GURL& script_url) {
+  RendererClientBase::WillDestroyServiceWorkerContextOnWorkerThread(
+      context, service_worker_version_id, service_worker_scope, script_url);
+}
+
 }  // namespace electron

+ 12 - 0
shell/renderer/electron_sandboxed_renderer_client.h

@@ -42,6 +42,18 @@ class ElectronSandboxedRendererClient : public RendererClientBase {
   void RenderFrameCreated(content::RenderFrame*) override;
   void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override;
   void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override;
+  void WillEvaluateServiceWorkerOnWorkerThread(
+      blink::WebServiceWorkerContextProxy* context_proxy,
+      v8::Local<v8::Context> v8_context,
+      int64_t service_worker_version_id,
+      const GURL& service_worker_scope,
+      const GURL& script_url,
+      const blink::ServiceWorkerToken& service_worker_token) override;
+  void WillDestroyServiceWorkerContextOnWorkerThread(
+      v8::Local<v8::Context> context,
+      int64_t service_worker_version_id,
+      const GURL& service_worker_scope,
+      const GURL& script_url) override;
 
  private:
   void EmitProcessEvent(content::RenderFrame* render_frame,

+ 295 - 0
shell/renderer/preload_realm_context.cc

@@ -0,0 +1,295 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/renderer/preload_realm_context.h"
+
+#include "base/command_line.h"
+#include "base/process/process.h"
+#include "base/process/process_metrics.h"
+#include "shell/common/api/electron_bindings.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/node_includes.h"
+#include "shell/common/node_util.h"
+#include "shell/renderer/preload_utils.h"
+#include "shell/renderer/service_worker_data.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_controller.h"  // nogncheck
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"  // nogncheck
+#include "third_party/blink/renderer/core/inspector/worker_thread_debugger.h"  // nogncheck
+#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h"  // nogncheck
+#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h"  // nogncheck
+#include "third_party/blink/renderer/platform/bindings/script_state.h"  // nogncheck
+#include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h"  // nogncheck
+#include "third_party/blink/renderer/platform/bindings/v8_per_context_data.h"  // nogncheck
+#include "third_party/blink/renderer/platform/context_lifecycle_observer.h"  // nogncheck
+#include "v8/include/v8-context.h"
+
+namespace electron::preload_realm {
+
+namespace {
+
+static constexpr int kElectronContextEmbedderDataIndex =
+    static_cast<int>(gin::kPerContextDataStartIndex) +
+    static_cast<int>(gin::kEmbedderElectron);
+
+// This is a helper class to make the initiator ExecutionContext the owner
+// of a ShadowRealmGlobalScope and its ScriptState. When the initiator
+// ExecutionContext is destroyed, the ShadowRealmGlobalScope is destroyed,
+// too.
+class PreloadRealmLifetimeController
+    : public blink::GarbageCollected<PreloadRealmLifetimeController>,
+      public blink::ContextLifecycleObserver {
+ public:
+  explicit PreloadRealmLifetimeController(
+      blink::ExecutionContext* initiator_execution_context,
+      blink::ScriptState* initiator_script_state,
+      blink::ShadowRealmGlobalScope* shadow_realm_global_scope,
+      blink::ScriptState* shadow_realm_script_state,
+      electron::ServiceWorkerData* service_worker_data)
+      : initiator_script_state_(initiator_script_state),
+        is_initiator_worker_or_worklet_(
+            initiator_execution_context->IsWorkerOrWorkletGlobalScope()),
+        shadow_realm_global_scope_(shadow_realm_global_scope),
+        shadow_realm_script_state_(shadow_realm_script_state),
+        service_worker_data_(service_worker_data) {
+    // Align lifetime of this controller to that of the initiator's context.
+    self_ = this;
+
+    SetContextLifecycleNotifier(initiator_execution_context);
+    RegisterDebugger(initiator_execution_context);
+
+    initiator_context()->SetAlignedPointerInEmbedderData(
+        kElectronContextEmbedderDataIndex, static_cast<void*>(this));
+    realm_context()->SetAlignedPointerInEmbedderData(
+        kElectronContextEmbedderDataIndex, static_cast<void*>(this));
+
+    metrics_ = base::ProcessMetrics::CreateCurrentProcessMetrics();
+    RunInitScript();
+  }
+
+  static PreloadRealmLifetimeController* From(v8::Local<v8::Context> context) {
+    if (context->GetNumberOfEmbedderDataFields() <=
+        kElectronContextEmbedderDataIndex) {
+      return nullptr;
+    }
+    auto* controller = static_cast<PreloadRealmLifetimeController*>(
+        context->GetAlignedPointerFromEmbedderData(
+            kElectronContextEmbedderDataIndex));
+    CHECK(controller);
+    return controller;
+  }
+
+  void Trace(blink::Visitor* visitor) const override {
+    visitor->Trace(initiator_script_state_);
+    visitor->Trace(shadow_realm_global_scope_);
+    visitor->Trace(shadow_realm_script_state_);
+    ContextLifecycleObserver::Trace(visitor);
+  }
+
+  v8::MaybeLocal<v8::Context> GetContext() {
+    return shadow_realm_script_state_->ContextIsValid()
+               ? shadow_realm_script_state_->GetContext()
+               : v8::MaybeLocal<v8::Context>();
+  }
+
+  v8::MaybeLocal<v8::Context> GetInitiatorContext() {
+    return initiator_script_state_->ContextIsValid()
+               ? initiator_script_state_->GetContext()
+               : v8::MaybeLocal<v8::Context>();
+  }
+
+  electron::ServiceWorkerData* service_worker_data() {
+    return service_worker_data_;
+  }
+
+ protected:
+  void ContextDestroyed() override {
+    v8::HandleScope handle_scope(realm_isolate());
+    realm_context()->SetAlignedPointerInEmbedderData(
+        kElectronContextEmbedderDataIndex, nullptr);
+
+    // See ShadowRealmGlobalScope::ContextDestroyed
+    shadow_realm_script_state_->DisposePerContextData();
+    if (is_initiator_worker_or_worklet_) {
+      shadow_realm_script_state_->DissociateContext();
+    }
+    shadow_realm_script_state_.Clear();
+    shadow_realm_global_scope_->NotifyContextDestroyed();
+    shadow_realm_global_scope_.Clear();
+
+    self_.Clear();
+  }
+
+ private:
+  v8::Isolate* realm_isolate() {
+    return shadow_realm_script_state_->GetIsolate();
+  }
+  v8::Local<v8::Context> realm_context() {
+    return shadow_realm_script_state_->GetContext();
+  }
+  v8::Local<v8::Context> initiator_context() {
+    return initiator_script_state_->GetContext();
+  }
+
+  void RegisterDebugger(blink::ExecutionContext* initiator_execution_context) {
+    v8::Isolate* isolate = realm_isolate();
+    v8::Local<v8::Context> context = realm_context();
+
+    blink::WorkerThreadDebugger* debugger =
+        blink::WorkerThreadDebugger::From(isolate);
+    ;
+    const auto* worker_context =
+        To<blink::WorkerOrWorkletGlobalScope>(initiator_execution_context);
+
+    // Override path to make preload realm easier to find in debugger.
+    blink::KURL url_for_debugger(worker_context->Url());
+    url_for_debugger.SetPath("electron-preload-realm");
+
+    debugger->ContextCreated(worker_context->GetThread(), url_for_debugger,
+                             context);
+  }
+
+  void RunInitScript() {
+    v8::Isolate* isolate = realm_isolate();
+    v8::Local<v8::Context> context = realm_context();
+
+    v8::Context::Scope context_scope(context);
+    v8::MicrotasksScope microtasks_scope(
+        isolate, context->GetMicrotaskQueue(),
+        v8::MicrotasksScope::kDoNotRunMicrotasks);
+
+    v8::Local<v8::Object> binding = v8::Object::New(isolate);
+
+    gin_helper::Dictionary b(isolate, binding);
+    b.SetMethod("get", preload_utils::GetBinding);
+    b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
+
+    gin_helper::Dictionary process = gin::Dictionary::CreateEmpty(isolate);
+    b.Set("process", process);
+
+    ElectronBindings::BindProcess(isolate, &process, metrics_.get());
+
+    process.SetMethod("uptime", preload_utils::Uptime);
+    process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
+    process.SetReadOnly("pid", base::GetCurrentProcId());
+    process.SetReadOnly("sandboxed", true);
+    process.SetReadOnly("type", "service-worker");
+    process.SetReadOnly("contextIsolated", true);
+
+    std::vector<v8::Local<v8::String>> preload_realm_bundle_params = {
+        node::FIXED_ONE_BYTE_STRING(isolate, "binding")};
+
+    std::vector<v8::Local<v8::Value>> preload_realm_bundle_args = {binding};
+
+    util::CompileAndCall(context, "electron/js2c/preload_realm_bundle",
+                         &preload_realm_bundle_params,
+                         &preload_realm_bundle_args);
+  }
+
+  const blink::WeakMember<blink::ScriptState> initiator_script_state_;
+  bool is_initiator_worker_or_worklet_;
+  blink::Member<blink::ShadowRealmGlobalScope> shadow_realm_global_scope_;
+  blink::Member<blink::ScriptState> shadow_realm_script_state_;
+
+  std::unique_ptr<base::ProcessMetrics> metrics_;
+  raw_ptr<ServiceWorkerData> service_worker_data_;
+
+  blink::Persistent<PreloadRealmLifetimeController> self_;
+};
+
+}  // namespace
+
+v8::MaybeLocal<v8::Context> GetInitiatorContext(
+    v8::Local<v8::Context> context) {
+  DCHECK(!context.IsEmpty());
+  blink::ExecutionContext* execution_context =
+      blink::ExecutionContext::From(context);
+  if (!execution_context->IsShadowRealmGlobalScope())
+    return v8::MaybeLocal<v8::Context>();
+  auto* controller = PreloadRealmLifetimeController::From(context);
+  if (controller)
+    return controller->GetInitiatorContext();
+  return v8::MaybeLocal<v8::Context>();
+}
+
+v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
+    v8::Local<v8::Context> context) {
+  DCHECK(!context.IsEmpty());
+  blink::ExecutionContext* execution_context =
+      blink::ExecutionContext::From(context);
+  if (!execution_context->IsServiceWorkerGlobalScope())
+    return v8::MaybeLocal<v8::Context>();
+  auto* controller = PreloadRealmLifetimeController::From(context);
+  if (controller)
+    return controller->GetContext();
+  return v8::MaybeLocal<v8::Context>();
+}
+
+electron::ServiceWorkerData* GetServiceWorkerData(
+    v8::Local<v8::Context> context) {
+  auto* controller = PreloadRealmLifetimeController::From(context);
+  return controller ? controller->service_worker_data() : nullptr;
+}
+
+void OnCreatePreloadableV8Context(
+    v8::Local<v8::Context> initiator_context,
+    electron::ServiceWorkerData* service_worker_data) {
+  v8::Isolate* isolate = initiator_context->GetIsolate();
+  blink::ScriptState* initiator_script_state =
+      blink::ScriptState::MaybeFrom(isolate, initiator_context);
+  DCHECK(initiator_script_state);
+  blink::ExecutionContext* initiator_execution_context =
+      blink::ExecutionContext::From(initiator_context);
+  DCHECK(initiator_execution_context);
+  blink::DOMWrapperWorld* world = blink::DOMWrapperWorld::Create(
+      isolate, blink::DOMWrapperWorld::WorldType::kShadowRealm);
+  CHECK(world);  // Not yet run out of the world id.
+
+  // Create a new ShadowRealmGlobalScope.
+  blink::ShadowRealmGlobalScope* shadow_realm_global_scope =
+      blink::MakeGarbageCollected<blink::ShadowRealmGlobalScope>(
+          initiator_execution_context);
+  const blink::WrapperTypeInfo* wrapper_type_info =
+      shadow_realm_global_scope->GetWrapperTypeInfo();
+
+  // Create a new v8::Context.
+  // Initialize V8 extensions before creating the context.
+  v8::ExtensionConfiguration extension_configuration =
+      blink::ScriptController::ExtensionsFor(shadow_realm_global_scope);
+
+  v8::Local<v8::ObjectTemplate> global_template =
+      wrapper_type_info->GetV8ClassTemplate(isolate, *world)
+          .As<v8::FunctionTemplate>()
+          ->InstanceTemplate();
+  v8::Local<v8::Object> global_proxy;  // Will request a new global proxy.
+  v8::Local<v8::Context> context =
+      v8::Context::New(isolate, &extension_configuration, global_template,
+                       global_proxy, v8::DeserializeInternalFieldsCallback(),
+                       initiator_execution_context->GetMicrotaskQueue());
+  context->UseDefaultSecurityToken();
+
+  // Associate the Blink object with the v8::Context.
+  blink::ScriptState* script_state =
+      blink::ScriptState::Create(context, world, shadow_realm_global_scope);
+
+  // Associate the Blink object with the v8::Objects.
+  global_proxy = context->Global();
+  blink::V8DOMWrapper::SetNativeInfo(isolate, global_proxy,
+                                     shadow_realm_global_scope);
+  v8::Local<v8::Object> global_object =
+      global_proxy->GetPrototype().As<v8::Object>();
+  blink::V8DOMWrapper::SetNativeInfo(isolate, global_object,
+                                     shadow_realm_global_scope);
+
+  // Install context-dependent properties.
+  std::ignore =
+      script_state->PerContextData()->ConstructorForType(wrapper_type_info);
+
+  // Make the initiator execution context the owner of the
+  // ShadowRealmGlobalScope and the ScriptState.
+  blink::MakeGarbageCollected<PreloadRealmLifetimeController>(
+      initiator_execution_context, initiator_script_state,
+      shadow_realm_global_scope, script_state, service_worker_data);
+}
+
+}  // namespace electron::preload_realm

+ 34 - 0
shell/renderer/preload_realm_context.h

@@ -0,0 +1,34 @@
+// Copyright (c) 2025 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_
+#define ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_
+
+#include "v8/include/v8-forward.h"
+
+namespace electron {
+class ServiceWorkerData;
+}
+
+namespace electron::preload_realm {
+
+// Get initiator context given the preload context.
+v8::MaybeLocal<v8::Context> GetInitiatorContext(v8::Local<v8::Context> context);
+
+// Get the preload context given the initiator context.
+v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
+    v8::Local<v8::Context> context);
+
+// Get service worker data given the preload realm context.
+electron::ServiceWorkerData* GetServiceWorkerData(
+    v8::Local<v8::Context> context);
+
+// Create
+void OnCreatePreloadableV8Context(
+    v8::Local<v8::Context> initiator_context,
+    electron::ServiceWorkerData* service_worker_data);
+
+}  // namespace electron::preload_realm
+
+#endif  // ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_

+ 80 - 0
shell/renderer/preload_utils.cc

@@ -0,0 +1,80 @@
+// Copyright (c) 2016 GitHub, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/renderer/preload_utils.h"
+
+#include "base/process/process.h"
+#include "shell/common/gin_helper/arguments.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/node_includes.h"
+#include "v8/include/v8-context.h"
+
+namespace electron::preload_utils {
+
+namespace {
+
+constexpr std::string_view kBindingCacheKey = "native-binding-cache";
+
+v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
+  auto context = isolate->GetCurrentContext();
+  gin_helper::Dictionary global(isolate, context->Global());
+  v8::Local<v8::Value> cache;
+
+  if (!global.GetHidden(kBindingCacheKey, &cache)) {
+    cache = v8::Object::New(isolate);
+    global.SetHidden(kBindingCacheKey, cache);
+  }
+
+  return cache->ToObject(context).ToLocalChecked();
+}
+
+}  // namespace
+
+// adapted from node.cc
+v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
+                                v8::Local<v8::String> key,
+                                gin_helper::Arguments* margs) {
+  v8::Local<v8::Object> exports;
+  std::string binding_key = gin::V8ToString(isolate, key);
+  gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
+
+  if (cache.Get(binding_key, &exports)) {
+    return exports;
+  }
+
+  auto* mod = node::binding::get_linked_module(binding_key.c_str());
+
+  if (!mod) {
+    char errmsg[1024];
+    snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
+             binding_key.c_str());
+    margs->ThrowError(errmsg);
+    return exports;
+  }
+
+  exports = v8::Object::New(isolate);
+  DCHECK_EQ(mod->nm_register_func, nullptr);
+  DCHECK_NE(mod->nm_context_register_func, nullptr);
+  mod->nm_context_register_func(exports, v8::Null(isolate),
+                                isolate->GetCurrentContext(), mod->nm_priv);
+  cache.Set(binding_key, exports);
+  return exports;
+}
+
+v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
+                                         v8::Local<v8::String> source) {
+  auto context = isolate->GetCurrentContext();
+  auto maybe_script = v8::Script::Compile(context, source);
+  v8::Local<v8::Script> script;
+  if (!maybe_script.ToLocal(&script))
+    return {};
+  return script->Run(context).ToLocalChecked();
+}
+
+double Uptime() {
+  return (base::Time::Now() - base::Process::Current().CreationTime())
+      .InSecondsF();
+}
+
+}  // namespace electron::preload_utils

+ 27 - 0
shell/renderer/preload_utils.h

@@ -0,0 +1,27 @@
+// Copyright (c) 2016 GitHub, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
+#define ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
+
+#include "v8/include/v8-forward.h"
+
+namespace gin_helper {
+class Arguments;
+}
+
+namespace electron::preload_utils {
+
+v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
+                                v8::Local<v8::String> key,
+                                gin_helper::Arguments* margs);
+
+v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
+                                         v8::Local<v8::String> source);
+
+double Uptime();
+
+}  // namespace electron::preload_utils
+
+#endif  // ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_