refactor: generate PreloadRealmGlobalScope IDL bindings in //electron (#55165)

* refactor: use PreloadRealmGlobalScope for service worker preloads

Service worker preload scripts ran in Blink's ShadowRealmGlobalScope.
The ShadowRealm proposal has stalled, so Chromium may remove that class.

A new Chromium patch adds PreloadRealmGlobalScope, a global scope that
exposes the same interfaces without depending on any ShadowRealm code,
and Electron now creates the preload context with it.

Uncaught errors and unhandled rejections in a service worker preload
script are now reported through the worker's console. Before, they hit
NOTREACHED() in ShadowRealmGlobalScope.

Calling getEventListeners() from the DevTools console in the preload
context no longer crashes the renderer.

* refactor: generate PreloadRealmGlobalScope IDL bindings in //electron

* ci: normalize include paths in Windows gen headers for clang-tidy

The Windows clang-tidy job runs in a Linux container using generated
headers downloaded from out_gen_artifacts_win_x64. On Windows, Python
code generators such as inspector_protocol/code_generator.py use
os.path.join() and emit backslashes in #include directives, causing
Linux Siso scandeps and clang-tidy on RBE workers to fail with:

gen/third_party/blink/renderer/core/inspector/protocol/forward.h:15:10: error: 'third_party/inspector_protocol\crdtp/error_support.h' file not found [clang-diagnostic-error]
   15 | #include "third_party/inspector_protocol\crdtp/error_support.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* fixup! normalize include paths in Windows gen headers for clang-tidy

* fix: resolve root worker context for ShadowRealms nested in preload realms

When a ShadowRealm is created inside a service worker preload realm
(e.g. with --harmony-shadow-realm), its initiator ExecutionContext is
PreloadRealmGlobalScope rather than a WorkerOrWorkletGlobalScope or
ShadowRealmGlobalScope. Previously, GetRootInitiatorExecutionContext()
only walked initiators via To<ShadowRealmGlobalScope>(), which either
stopped at PreloadRealmGlobalScope (failing the subsequent
To<WorkerOrWorkletGlobalScope>() cast on unhandled rejections or console
messages) or performed an invalid downcast to ShadowRealmGlobalScope.

Delegate GetRootInitiatorExecutionContext() to
initiator_execution_context_->GetRootExecutionContext() so nested
ShadowRealms walk through PreloadRealmGlobalScope to the underlying
ServiceWorkerGlobalScope.

* fix: use Supplement<ExecutionContext> for PreloadRealmLifetimeController

Replace V8 embedder data slot storage in PreloadRealmLifetimeController with
blink::Supplement<ExecutionContext> so it cannot collide with NodeBindings
storing node::IsolateData* in kElectronContextEmbedderDataIndex
or require a persistent self root.

* build: track blink pydeps

* chore: update patches

---------

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
This commit is contained in:
RoboandSamuel Maddock authored and GitHub committed 2026-10-26 08:26:03 -07:00
1 parent 8c691a4ffb
commit a4b0cc65a5
12 files changed
+810 -741

No files matched your search

+99 -1
View File
@@ -477,6 +477,102 @@ action("electron_generate_node_defines") {
args = [ rebase_path(target_gen_dir) ] + rebase_path(inputs)
}
action("electron_generate_preload_realm_bindings") {
script = "build/generate_preload_realm_bindings.py"
core_idl_files = [
"//third_party/blink/renderer/core/dom/abort_controller.idl",
"//third_party/blink/renderer/core/dom/abort_signal.idl",
"//third_party/blink/renderer/core/dom/dom_exception.idl",
"//third_party/blink/renderer/core/dom/events/custom_event.idl",
"//third_party/blink/renderer/core/dom/events/event.idl",
"//third_party/blink/renderer/core/dom/events/event_target.idl",
"//third_party/blink/renderer/core/dom/observable.idl",
"//third_party/blink/renderer/core/dom/quota_exceeded_error.idl",
"//third_party/blink/renderer/core/dom/subscriber.idl",
"//third_party/blink/renderer/core/dom/void_function.idl",
"//third_party/blink/renderer/core/events/error_event.idl",
"//third_party/blink/renderer/core/events/promise_rejection_event.idl",
"//third_party/blink/renderer/core/frame/universal_global_scope.idl",
"//third_party/blink/renderer/core/messaging/structured_serialize_options.idl",
"//third_party/blink/renderer/core/streams/byte_length_queuing_strategy.idl",
"//third_party/blink/renderer/core/streams/count_queuing_strategy.idl",
"//third_party/blink/renderer/core/streams/readable_byte_stream_controller.idl",
"//third_party/blink/renderer/core/streams/readable_stream.idl",
"//third_party/blink/renderer/core/streams/readable_stream_byob_reader.idl",
"//third_party/blink/renderer/core/streams/readable_stream_byob_request.idl",
"//third_party/blink/renderer/core/streams/readable_stream_default_controller.idl",
"//third_party/blink/renderer/core/streams/readable_stream_default_reader.idl",
"//third_party/blink/renderer/core/streams/transform_stream.idl",
"//third_party/blink/renderer/core/streams/transform_stream_default_controller.idl",
"//third_party/blink/renderer/core/streams/writable_stream.idl",
"//third_party/blink/renderer/core/streams/writable_stream_default_controller.idl",
"//third_party/blink/renderer/core/streams/writable_stream_default_writer.idl",
"//third_party/blink/renderer/core/url/url.idl",
"//third_party/blink/renderer/core/url/url_search_params.idl",
"//third_party/blink/renderer/core/url_pattern/url_pattern.idl",
]
modules_idl_files = [
"//third_party/blink/renderer/modules/compression/compression_stream.idl",
"//third_party/blink/renderer/modules/compression/decompression_stream.idl",
"//third_party/blink/renderer/modules/encoding/text_decoder.idl",
"//third_party/blink/renderer/modules/encoding/text_decoder_stream.idl",
"//third_party/blink/renderer/modules/encoding/text_encoder.idl",
"//third_party/blink/renderer/modules/encoding/text_encoder_stream.idl",
]
idl_file = "shell/renderer/preload_realm_global_scope.idl"
runtime_enabled_features =
"//third_party/blink/renderer/platform/runtime_enabled_features.json5"
wrapper_type_info_header =
"//third_party/blink/renderer/platform/bindings/wrapper_type_info.h"
inputs = [
idl_file,
runtime_enabled_features,
wrapper_type_info_header,
] + core_idl_files + modules_idl_files
# Read Blink's own .pydeps files directly instead of using action_with_pydeps
# (which expects a checked-in build/generate_preload_realm_bindings.pydeps)
# so we automatically pick up upstream Blink bindings script changes on rolls
# without manually maintaining a duplicate .pydeps file in //electron.
_blink_scripts_dir = "//third_party/blink/renderer/bindings/scripts"
_pydeps_files = [
"$_blink_scripts_dir/collect_idl_files.pydeps",
"$_blink_scripts_dir/generate_bindings.pydeps",
]
_pydeps_lines = []
foreach(_pydeps_file, _pydeps_files) {
_pydeps_lines += read_file(_pydeps_file, "list lines")
}
_pydeps_entries = filter_exclude(_pydeps_lines, [ "#*" ])
inputs +=
rebase_path(_pydeps_entries, ".", _blink_scripts_dir) + _pydeps_files
outputs = [
"$target_gen_dir/preload_realm/v8_preload_realm_global_scope.cc",
"$target_gen_dir/preload_realm/v8_preload_realm_global_scope.h",
]
args =
[
"--root-src-dir",
rebase_path("//", root_build_dir),
"--root-gen-dir",
rebase_path(root_gen_dir, root_build_dir),
"--output-reldir",
rebase_path("$target_gen_dir/preload_realm", root_gen_dir),
"--idl-file",
rebase_path(idl_file, root_build_dir),
"--runtime-enabled-features",
rebase_path(runtime_enabled_features, root_build_dir),
"--wrapper-type-info-header",
rebase_path(wrapper_type_info_header, root_build_dir),
"--core-idl-files",
] + rebase_path(core_idl_files, root_build_dir) +
[ "--modules-idl-files" ] + rebase_path(modules_idl_files, root_build_dir)
}
source_set("electron_lib") {
configs += [
"//v8:external_startup_data",
@@ -491,6 +587,7 @@ source_set("electron_lib") {
deps = [
":electron_fuses",
":electron_generate_node_defines",
":electron_generate_preload_realm_bindings",
":electron_js2c",
":electron_version_header",
":resources",
@@ -633,7 +730,8 @@ source_set("electron_lib") {
deps += [ "//electron/build/config:generate_mas_config" ]
sources = filenames.lib_sources
sources = filenames.lib_sources +
get_target_outputs(":electron_generate_preload_realm_bindings")
if (is_win) {
sources += filenames.lib_sources_win
}
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Generates Blink V8 bindings for PreloadRealmGlobalScope."""
import argparse
import os
import sys
import tempfile
def setup_blink_paths(root_src_dir):
module_dirs = (
os.path.join(root_src_dir, 'third_party', 'blink', 'renderer', 'bindings',
'scripts'),
os.path.join(root_src_dir, 'third_party', 'blink', 'renderer', 'build',
'scripts'),
os.path.join(root_src_dir, 'third_party', 'mako', 'mako'),
os.path.join(root_src_dir, 'tools'),
)
for module_dir in reversed(module_dirs):
if module_dir not in sys.path:
sys.path.insert(1, module_dir)
# Interfaces that should be [Exposed=*] per spec (crbug.com/41480387 and
# crbug.com/40282760) and are exposed on PreloadRealmGlobalScope in addition
# to constructs already marked [Exposed=*].
PRELOAD_REALM_EXPOSED_INTERFACES = frozenset([
'AbortController',
'AbortSignal',
'CompressionStream',
'CustomEvent',
'DOMException',
'DecompressionStream',
'ErrorEvent',
'Event',
'EventTarget',
'Observable',
'PromiseRejectionEvent',
'Subscriber',
'TextDecoder',
'TextDecoderStream',
'TextEncoder',
'TextEncoderStream',
'URL',
'URLPattern',
'URLSearchParams',
])
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--root-src-dir', required=True)
parser.add_argument('--root-gen-dir', required=True)
parser.add_argument('--output-reldir', required=True)
parser.add_argument('--idl-file', required=True)
parser.add_argument('--runtime-enabled-features', required=True)
parser.add_argument('--wrapper-type-info-header', required=True)
parser.add_argument('--core-idl-files', nargs='+', required=True)
parser.add_argument('--modules-idl-files', nargs='+', required=True)
options = parser.parse_args()
import re
with open(options.wrapper_type_info_header, encoding='utf-8') as f:
wti_content = f.read()
start_match = re.search(
r'static_assert\(kScriptWrappableStartTag\s*==\s*(\d+)\)', wti_content)
last_match = re.search(
r'kLastGeneratedScriptWrappableTag\s*=\s*(\d+)', wti_content)
assert start_match and last_match, (
'Could not parse ScriptWrappable tag bounds from wrapper_type_info.h')
start_tag = int(start_match.group(1))
last_generated_tag = int(last_match.group(1))
assert last_generated_tag - start_tag >= 1600, (
f'ScriptWrappable tag range [{start_tag}, {last_generated_tag}) is too '
'narrow; verify kLastGeneratedScriptWrappableTag - 1 does not collide.')
setup_blink_paths(options.root_src_dir)
import bind_gen
from bind_gen import codegen_expr
from bind_gen import codegen_utils
from bind_gen.interface import generate_class_like
from idl_parser import idl_lexer
from idl_parser import idl_parser
import web_idl
from web_idl.idl_compiler import IdlCompiler
from web_idl.idl_type import IdlTypeFactory
from web_idl.ir_builder import load_and_register_idl_definitions
from web_idl.ir_map import IRMap
from web_idl.reference import RefByIdFactory
lexer = idl_lexer.IDLLexer()
idl_file_parser = idl_parser.IDLParser(lexer)
def strip_non_global_defs(ast_node):
# Only Interface, InterfaceMixin (for UniversalGlobalScope), Includes,
# Dictionary (for StructuredSerializeOptions), and Callback (for
# VoidFunction) are needed to compile PreloadRealmGlobalScope. Stripping
# members and auxiliary definitions from the exposed interface files avoids
# pulling in their transitive argument/attribute types and mixins.
children = getattr(ast_node, '_children', None)
if not children:
return ast_node
kept = []
for child in children:
cls = child.GetClass()
name = child.GetName()
if cls == 'Interface':
if not child.GetProperty('MIXIN') and name != 'PreloadRealmGlobalScope':
child._children = [
c for c in (child._children or [])
if c.GetClass() == 'ExtAttributes'
]
kept.append(child)
elif cls == 'Includes' and name == 'PreloadRealmGlobalScope':
kept.append(child)
elif cls == 'Dictionary' and name == 'StructuredSerializeOptions':
kept.append(child)
elif cls == 'Callback' and name == 'VoidFunction':
kept.append(child)
ast_node._children = kept
return ast_node
core_ast = web_idl.AstGroup(
component=web_idl.Component('core'),
for_testing=False,
)
for filepath in options.core_idl_files:
core_ast.add_ast_node(
strip_non_global_defs(idl_parser.ParseFile(idl_file_parser, filepath)))
modules_ast = web_idl.AstGroup(
component=web_idl.Component('modules'),
for_testing=False,
)
for filepath in list(options.modules_idl_files) + [options.idl_file]:
modules_ast.add_ast_node(
strip_non_global_defs(idl_parser.ParseFile(idl_file_parser, filepath)))
with tempfile.NamedTemporaryFile(suffix='.pickle', delete=False) as tmp_core:
tmp_core_path = tmp_core.name
with tempfile.NamedTemporaryFile(
suffix='.pickle', delete=False) as tmp_modules:
tmp_modules_path = tmp_modules.name
with tempfile.NamedTemporaryFile(suffix='.pickle', delete=False) as tmp_db:
tmp_db_path = tmp_db.name
try:
core_ast.write_to_file(tmp_core_path)
modules_ast.write_to_file(tmp_modules_path)
web_idl.init(
runtime_enabled_features_paths=[options.runtime_enabled_features])
ir_map = IRMap()
ref_to_idl_def_factory = RefByIdFactory()
idl_type_factory = IdlTypeFactory()
load_and_register_idl_definitions(
filepaths=[tmp_core_path, tmp_modules_path],
register_ir=ir_map.register,
create_ref_to_idl_def=ref_to_idl_def_factory.create,
idl_type_factory=idl_type_factory,
)
for ir in ir_map.irs_of_kind(IRMap.IR.Kind.INTERFACE):
if ir.identifier in PRELOAD_REALM_EXPOSED_INTERFACES:
ir.exposure.add_global_name_and_feature('PreloadRealm')
errors = []
compiler = IdlCompiler(
ir_map=ir_map,
ref_to_idl_def_factory=ref_to_idl_def_factory,
idl_type_factory=idl_type_factory,
report_error=errors.append,
)
database = compiler.build_database()
if errors:
sys.exit('\n'.join(errors))
preload_iface = database.find(web_idl.Identifier('PreloadRealmGlobalScope'))
preload_iface._code_generator_info._blink_headers = (
'electron/shell/renderer/preload_realm_global_scope.h',)
# Use `blink::kLastGeneratedScriptWrappableTag - 1` so the tag automatically
# tracks Blink's generated ScriptWrappable tag upper bound in
# wrapper_type_info.h.
preload_tag_expr = 'blink::kLastGeneratedScriptWrappableTag - 1'
preload_iface._tag = preload_tag_expr
preload_iface._max_subclass_tag = preload_tag_expr
database.write_to_file(tmp_db_path)
# 3. Configure bind_gen to emit single-component bindings into output_reldir
# without MODULES_EXPORT DLL macro annotations.
orig_expr_from_exposure = codegen_expr.expr_from_exposure
def patched_expr_from_exposure(exposure, **kwargs):
if any(e.global_name == 'PreloadRealm'
for e in exposure.global_names_and_features) and not kwargs.get(
'global_names'):
return codegen_expr._Expr('true')
return orig_expr_from_exposure(exposure, **kwargs)
codegen_expr.expr_from_exposure = patched_expr_from_exposure
bind_gen.interface.expr_from_exposure = patched_expr_from_exposure
codegen_utils.component_export = lambda component, for_testing: ''
bind_gen.interface.component_export = codegen_utils.component_export
codegen_utils.component_export_header = lambda component, for_testing: None
bind_gen.interface.component_export_header = (
codegen_utils.component_export_header)
from bind_gen.union_name_mapper import UnionNameMapper
UnionNameMapper.init = classmethod(
lambda cls, filename, database: cls.init_with_config_text('', database))
component_reldirs = {
web_idl.Component('core'):
'third_party/blink/renderer/bindings/core/v8',
web_idl.Component('modules'):
'third_party/blink/renderer/bindings/modules/v8',
}
bind_gen.init(
web_idl_database_path=tmp_db_path,
root_src_dir=options.root_src_dir,
root_gen_dir=options.root_gen_dir,
component_reldirs=component_reldirs,
enable_style_format=False,
enable_code_generation_tracing=False,
)
# Route PreloadRealmGlobalScope's own output files to Electron's gen dir
# while keeping core/modules paths intact for #include references to Blink
# interfaces.
from bind_gen.path_manager import PathManager
orig_path_manager_init = PathManager.__init__
def patched_path_manager_init(self, idl_definition):
orig_path_manager_init(self, idl_definition)
if getattr(idl_definition, 'identifier',
None) == 'PreloadRealmGlobalScope':
norm_out = os.path.normpath(options.output_reldir).replace('\\', '/')
self._api_dir = norm_out
self._impl_dir = norm_out
PathManager.__init__ = patched_path_manager_init
db = bind_gen.package_initializer.package_initializer().web_idl_database()
generate_class_like(db.find(web_idl.Identifier('PreloadRealmGlobalScope')))
finally:
for path in (tmp_core_path, tmp_modules_path, tmp_db_path):
if os.path.exists(path):
os.remove(path)
if __name__ == '__main__':
main()
+2
View File
@@ -829,6 +829,8 @@ filenames = {
"shell/renderer/preload_environment.h",
"shell/renderer/preload_realm_context.cc",
"shell/renderer/preload_realm_context.h",
"shell/renderer/preload_realm_global_scope.cc",
"shell/renderer/preload_realm_global_scope.h",
"shell/renderer/preload_utils.cc",
"shell/renderer/preload_utils.h",
"shell/renderer/renderer_client_base.cc",
+2 -1
View File
@@ -139,4 +139,5 @@ fix_plumb_the_requesting_frame_through_pointer_lock_requests.patch
feat_let_the_host_of_a_remote_nswindow_set_more_of_its_behavior.patch
device_make_devicemonitormessagewindow_a_nodestructor.patch
fix_handle_a_null_pending_call_in_dbus_objectproxy.patch
feat_add_preloadrealmglobalscope.patch
feat_update_executioncontext_api.patch
use_forward_slashes_for_the_default_crdtp_include_dir_in_templates.patch
@@ -1,686 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Maddock <samuel.maddock@gmail.com>
Date: Tue, 20 Oct 2026 18:54:12 +0000
Subject: feat: add PreloadRealmGlobalScope
Electron runs service worker preload scripts in a context of their own on
the worker's thread. That context used to be a Blink ShadowRealmGlobalScope,
which exists for the TC39 ShadowRealm proposal. The proposal has stalled, so
Chromium may remove its implementation.
PreloadRealmGlobalScope is a global scope for that context which does not
depend on any ShadowRealm code. It is exposed the same interfaces as a
ShadowRealm, which are the ones the specs mark [Exposed=*], and defers to
the context that created it for everything else. Unlike a ShadowRealm, it
reports uncaught errors and console messages through that context instead
of treating them as unreachable.
Electron creates the context itself, in
shell/renderer/preload_realm_context.cc. This patch is specific to Electron
and is not intended to be upstreamed.
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc b/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
index 41a5db4e019084d47763a43bd548b29faa070660..3e62033228307ef2474c91e250db08cd72934ad6 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
@@ -78,6 +78,7 @@
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/inspector/main_thread_debugger.h"
#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h"
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h"
#include "third_party/blink/renderer/core/timing/time_clamper.h"
@@ -448,6 +449,11 @@ static void PromiseRejectHandlerInWorker(v8::PromiseRejectMessage data) {
ExecutionContext* execution_context = ExecutionContext::From(script_state);
if (!execution_context)
return;
+ // An Electron preload realm has no script controller of its own.
+ if (auto* preload_realm =
+ DynamicTo<PreloadRealmGlobalScope>(execution_context)) {
+ execution_context = preload_realm->GetInitiatorExecutionContext();
+ }
ExecutionContext* root_worker_context =
execution_context->IsShadowRealmGlobalScope()
diff --git a/third_party/blink/renderer/bindings/generated_in_core.gni b/third_party/blink/renderer/bindings/generated_in_core.gni
index 788f261f96ef65df2bc7fef0fa2a793da22e44ca..7b8efe1bc88c18e3ccf6e9a663c6670f46e8bea6 100644
--- a/third_party/blink/renderer/bindings/generated_in_core.gni
+++ b/third_party/blink/renderer/bindings/generated_in_core.gni
@@ -1530,6 +1530,8 @@ generated_interface_sources_in_core = [
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_preference_manager.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_preference_object.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_preference_object.h",
+ "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_preload_realm_global_scope.cc",
+ "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_preload_realm_global_scope.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_progress_event.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_progress_event.h",
"$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_promise_rejection_event.cc",
diff --git a/third_party/blink/renderer/bindings/generated_in_modules.gni b/third_party/blink/renderer/bindings/generated_in_modules.gni
index b56e9a96ebd3d598e53848fa955d02ac45628900..4f4d8a5ec938f7b4ccd336b64221d62017fb83b4 100644
--- a/third_party/blink/renderer/bindings/generated_in_modules.gni
+++ b/third_party/blink/renderer/bindings/generated_in_modules.gni
@@ -2793,6 +2793,8 @@ generated_interface_sources_in_modules = [
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_plugin.h",
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_plugin_array.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_plugin_array.h",
+ "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_preload_realm_global_scope.cc",
+ "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_preload_realm_global_scope.h",
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_presentation.cc",
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_presentation.h",
"$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_presentation_availability.cc",
diff --git a/third_party/blink/renderer/bindings/idl_in_core.gni b/third_party/blink/renderer/bindings/idl_in_core.gni
index f8d8e7180904bdf3a330483b8e6e7c3bf5dde439..9f495478b3a5164db9f113041a8e4e38ffb1d0fd 100644
--- a/third_party/blink/renderer/bindings/idl_in_core.gni
+++ b/third_party/blink/renderer/bindings/idl_in_core.gni
@@ -828,6 +828,7 @@ static_idl_files_in_core = [
"//third_party/blink/renderer/core/preferences/preference_manager.idl",
"//third_party/blink/renderer/core/preferences/preference_object.idl",
"//third_party/blink/renderer/core/preferences/navigator_preferences.idl",
+ "//third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.idl",
"//third_party/blink/renderer/core/url/origin.idl",
"//third_party/blink/renderer/core/url/url.idl",
"//third_party/blink/renderer/core/url/url_search_params.idl",
diff --git a/third_party/blink/renderer/bindings/scripts/bind_gen/codegen_expr.py b/third_party/blink/renderer/bindings/scripts/bind_gen/codegen_expr.py
index aafb09af75d6adb990bf7b71f3ea5d4abe5da2dd..e7a7c0ec1e80b69c8cda9c474861fc770294c8a2 100644
--- a/third_party/blink/renderer/bindings/scripts/bind_gen/codegen_expr.py
+++ b/third_party/blink/renderer/bindings/scripts/bind_gen/codegen_expr.py
@@ -287,6 +287,7 @@ def expr_from_exposure(
"DedicatedWorker": "IsDedicatedWorkerGlobalScope",
"LayoutWorklet": "IsLayoutWorkletGlobalScope",
"PaintWorklet": "IsPaintWorkletGlobalScope",
+ "PreloadRealm": "IsPreloadRealmGlobalScope",
"ServiceWorker": "IsServiceWorkerGlobalScope",
"ShadowRealm": "IsShadowRealmGlobalScope",
"SharedWorker": "IsSharedWorkerGlobalScope",
diff --git a/third_party/blink/renderer/core/BUILD.gn b/third_party/blink/renderer/core/BUILD.gn
index 12942929d97d2bfff86fc43c506253934fd8e68e..a56872b923bd6944a9821ee455654b18daed26f1 100644
--- a/third_party/blink/renderer/core/BUILD.gn
+++ b/third_party/blink/renderer/core/BUILD.gn
@@ -65,6 +65,7 @@ import("//third_party/blink/renderer/core/page/build.gni")
import("//third_party/blink/renderer/core/paint/build.gni")
import("//third_party/blink/renderer/core/permissions_policy/build.gni")
import("//third_party/blink/renderer/core/preferences/build.gni")
+import("//third_party/blink/renderer/core/preload_realm/build.gni")
import("//third_party/blink/renderer/core/resize_observer/build.gni")
import("//third_party/blink/renderer/core/route_matching/build.gni")
import("//third_party/blink/renderer/core/sanitizer/build.gni")
@@ -361,6 +362,7 @@ component("core") {
sources +=
rebase_path(blink_core_sources_xmlhttprequest, "", "xmlhttprequest")
sources += rebase_path(blink_core_sources_preferences, "", "preferences")
+ sources += rebase_path(blink_core_sources_preload_realm, "", "preload_realm")
configs -= core_config_remove
configs += core_config_add
diff --git a/third_party/blink/renderer/core/dom/abort_controller.idl b/third_party/blink/renderer/core/dom/abort_controller.idl
index 563b583bb9d64b350e3fdb0969901e8c674dc47c..427efba591af98680cfe59fa3f2089085d431599 100644
--- a/third_party/blink/renderer/core/dom/abort_controller.idl
+++ b/third_party/blink/renderer/core/dom/abort_controller.idl
@@ -5,7 +5,7 @@
// https://dom.spec.whatwg.org/#interface-abortcontroller
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface AbortController {
[CallWith=ScriptState, Measure] constructor();
[SameObject] readonly attribute AbortSignal signal;
diff --git a/third_party/blink/renderer/core/dom/abort_signal.idl b/third_party/blink/renderer/core/dom/abort_signal.idl
index d4ed5c6639d848a71f40d866cf53516873e05292..99620265ccb9a98ea8513f75b55ecdff4b9c596f 100644
--- a/third_party/blink/renderer/core/dom/abort_signal.idl
+++ b/third_party/blink/renderer/core/dom/abort_signal.idl
@@ -5,7 +5,7 @@
// https://dom.spec.whatwg.org/#interface-AbortSignal
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface AbortSignal : EventTarget {
[
CallWith=ScriptState,
diff --git a/third_party/blink/renderer/core/dom/dom_exception.idl b/third_party/blink/renderer/core/dom/dom_exception.idl
index 6e829626ffac53dd947b599ea374016ff73a8557..60a2a94c51167952aa0e7159f61c2ffd1e638770 100644
--- a/third_party/blink/renderer/core/dom/dom_exception.idl
+++ b/third_party/blink/renderer/core/dom/dom_exception.idl
@@ -31,7 +31,7 @@
// https://webidl.spec.whatwg.org/#es-DOMException
[
- Exposed=(Window,Worker,ShadowRealm), // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm), // TODO(crbug.com/41480387): This should be Exposed=*
Serializable
] interface DOMException {
constructor(optional DOMString message = "", optional DOMString name = "Error");
diff --git a/third_party/blink/renderer/core/dom/events/custom_event.idl b/third_party/blink/renderer/core/dom/events/custom_event.idl
index 200c6b8f30b122fcbbd5c76fecdd83350c81a9b7..6c002b630bda97a3571a5b16121bb620773e91b8 100644
--- a/third_party/blink/renderer/core/dom/events/custom_event.idl
+++ b/third_party/blink/renderer/core/dom/events/custom_event.idl
@@ -26,7 +26,7 @@
// https://dom.spec.whatwg.org/#interface-customevent
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface CustomEvent : Event {
[CallWith=ScriptState] constructor(DOMString type, optional CustomEventInit eventInitDict = {});
[CallWith=ScriptState] readonly attribute any detail;
diff --git a/third_party/blink/renderer/core/dom/events/event.idl b/third_party/blink/renderer/core/dom/events/event.idl
index 11e459da0f5d2b05d32ca8d483ed784473beed69..6cc8da25f79ca2aad3c52b79e17fd7b9a2e6280a 100644
--- a/third_party/blink/renderer/core/dom/events/event.idl
+++ b/third_party/blink/renderer/core/dom/events/event.idl
@@ -21,7 +21,7 @@
// https://dom.spec.whatwg.org/#interface-event
[
- Exposed=(Window,Worker,AudioWorklet,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,AudioWorklet,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface Event {
constructor(DOMString type, optional EventInit eventInitDict = {});
readonly attribute DOMString type;
diff --git a/third_party/blink/renderer/core/dom/events/event_target.idl b/third_party/blink/renderer/core/dom/events/event_target.idl
index 24e54a9c02041b53c79657e8143f4f063f9cd161..803d937fabf31619dea6caa58b5e16c51fda0920 100644
--- a/third_party/blink/renderer/core/dom/events/event_target.idl
+++ b/third_party/blink/renderer/core/dom/events/event_target.idl
@@ -26,7 +26,7 @@ dictionary ObservableEventListenerOptions {
};
[
- Exposed=(Window,Worker,AudioWorklet,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,AudioWorklet,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface EventTarget {
[CallWith=ScriptState] constructor();
undefined addEventListener(DOMString type, EventListener? listener, optional (AddEventListenerOptions or boolean) options);
diff --git a/third_party/blink/renderer/core/dom/observable.idl b/third_party/blink/renderer/core/dom/observable.idl
index 79410b5072c589f34cf180b10d1861e1a5516627..69cd9ce2fac2fe0f6cc1570ccd7a48bf8f1875c3 100644
--- a/third_party/blink/renderer/core/dom/observable.idl
+++ b/third_party/blink/renderer/core/dom/observable.idl
@@ -45,7 +45,7 @@ dictionary SubscribeOptions {
typedef (ObserverCallback or Observer) ObserverUnion;
typedef (ObserverCallback or ObservableInspector) ObservableInspectorUnion;
-[Exposed=(Window,Worker,ShadowRealm)] // TODO(crbug.com/40282760): This should be Exposed=*
+[Exposed=(Window,Worker,ShadowRealm,PreloadRealm)] // TODO(crbug.com/40282760): This should be Exposed=*
interface Observable {
[CallWith=ScriptState, MeasureAs=ObservableConstructor] constructor(SubscribeCallback callback);
[CallWith=ScriptState] void subscribe(optional ObserverUnion observer = {}, optional SubscribeOptions options = {});
diff --git a/third_party/blink/renderer/core/dom/subscriber.idl b/third_party/blink/renderer/core/dom/subscriber.idl
index 9a09310ba30b872bc8b9d6f78d34d07717f61611..45dc4b90d9e818c36f4553730578ac5a00f497da 100644
--- a/third_party/blink/renderer/core/dom/subscriber.idl
+++ b/third_party/blink/renderer/core/dom/subscriber.idl
@@ -4,7 +4,7 @@
// https://github.com/WICG/observable
-[Exposed=(Window,Worker,ShadowRealm)] // TODO(crbug.com/40282760): This should be Exposed=*
+[Exposed=(Window,Worker,ShadowRealm,PreloadRealm)] // TODO(crbug.com/40282760): This should be Exposed=*
interface Subscriber {
void next(any result);
[CallWith=ScriptState] void error(any error);
diff --git a/third_party/blink/renderer/core/events/error_event.idl b/third_party/blink/renderer/core/events/error_event.idl
index cacba4123e3b97e085c3ea6023eb2ef3efc5a5b5..f176f64f7d3d323a83a5fb1a17a7d150ba032ca2 100644
--- a/third_party/blink/renderer/core/events/error_event.idl
+++ b/third_party/blink/renderer/core/events/error_event.idl
@@ -31,7 +31,7 @@
// https://html.spec.whatwg.org/C/#the-errorevent-interface
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface ErrorEvent : Event {
[CallWith=ScriptState] constructor(DOMString type, optional ErrorEventInit eventInitDict = {});
readonly attribute DOMString message;
diff --git a/third_party/blink/renderer/core/events/promise_rejection_event.idl b/third_party/blink/renderer/core/events/promise_rejection_event.idl
index 693b2e78ee0e9dfb68bd0b08565ea76ce6711620..504f48fb2e3816686b75bb14798e51e722b62ffe 100644
--- a/third_party/blink/renderer/core/events/promise_rejection_event.idl
+++ b/third_party/blink/renderer/core/events/promise_rejection_event.idl
@@ -5,7 +5,7 @@
// https://html.spec.whatwg.org/C/#promiserejectionevent
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface PromiseRejectionEvent : Event {
[CallWith=ScriptState] constructor(DOMString type, PromiseRejectionEventInit eventInitDict);
[CallWith=ScriptState] readonly attribute object promise;
diff --git a/third_party/blink/renderer/core/execution_context/execution_context.h b/third_party/blink/renderer/core/execution_context/execution_context.h
index 07c9dd9c4ffa9b0b805187acce4f0eef4b641ceb..c73a5b68cc3d0b0a813779b1a11812e144a767c5 100644
--- a/third_party/blink/renderer/core/execution_context/execution_context.h
+++ b/third_party/blink/renderer/core/execution_context/execution_context.h
@@ -160,6 +160,7 @@ class CORE_EXPORT ExecutionContext : public Supplementable<ExecutionContext>,
virtual bool IsAudioWorkletGlobalScope() const { return false; }
virtual bool IsLayoutWorkletGlobalScope() const { return false; }
virtual bool IsPaintWorkletGlobalScope() const { return false; }
+ virtual bool IsPreloadRealmGlobalScope() const { return false; }
virtual bool IsThreadedWorkletGlobalScope() const { return false; }
virtual bool IsShadowRealmGlobalScope() const { return false; }
virtual bool IsJSExecutionForbidden() const { return false; }
diff --git a/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc b/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
index 3a52d94a105bcc34c2319940eb10ff381fe975ba..b3e3b1088239342d31090258e2d4b81d58d76fd0 100644
--- a/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
+++ b/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
@@ -38,6 +38,7 @@
#include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
#include "third_party/blink/renderer/core/inspector/v8_inspector_string.h"
#include "third_party/blink/renderer/core/inspector/worker_inspector_controller.h"
+#include "third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h"
#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h"
#include "third_party/blink/renderer/core/workers/dedicated_worker_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
@@ -81,6 +82,10 @@ void WorkerThreadDebugger::ReportConsoleMessage(
SourceLocation* location) {
if (!context)
return;
+ // An Electron preload realm has no worker thread of its own.
+ if (auto* preload_realm = DynamicTo<PreloadRealmGlobalScope>(context)) {
+ context = preload_realm->GetInitiatorExecutionContext();
+ }
ExecutionContext* root_worker_context =
context->IsShadowRealmGlobalScope()
@@ -168,6 +173,10 @@ void WorkerThreadDebugger::ExceptionThrown(WorkerThread* worker_thread,
}
int WorkerThreadDebugger::ContextGroupId(ExecutionContext* context) {
+ // An Electron preload realm belongs to its initiator's context group.
+ if (auto* preload_realm = DynamicTo<PreloadRealmGlobalScope>(context)) {
+ context = preload_realm->GetInitiatorExecutionContext();
+ }
return ContextGroupId(To<WorkerOrWorkletGlobalScope>(context)->GetThread());
}
diff --git a/third_party/blink/renderer/core/preload_realm/build.gni b/third_party/blink/renderer/core/preload_realm/build.gni
new file mode 100644
index 0000000000000000000000000000000000000000..95ad9051f0e81374443f4a7bb9d8e74fd39d230b
--- /dev/null
+++ b/third_party/blink/renderer/core/preload_realm/build.gni
@@ -0,0 +1,8 @@
+# Copyright (c) 2026 Anthropic, PBC.
+# Use of this source code is governed by the MIT license that can be
+# found in the LICENSE file.
+
+blink_core_sources_preload_realm = [
+ "preload_realm_global_scope.cc",
+ "preload_realm_global_scope.h",
+]
diff --git a/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.cc b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.cc
new file mode 100644
index 0000000000000000000000000000000000000000..51a32a4a8d16c7286cd6d1c288397d6b93fc1454
--- /dev/null
+++ b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.cc
@@ -0,0 +1,149 @@
+// Copyright (c) 2026 Anthropic, PBC.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h"
+
+#include "base/notreached.h"
+#include "base/task/single_thread_task_runner.h"
+#include "services/metrics/public/cpp/ukm_source_id.h"
+#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
+#include "third_party/blink/renderer/core/inspector/console_message.h"
+#include "third_party/blink/renderer/core/inspector/inspector_audits_issue.h"
+
+namespace blink {
+
+PreloadRealmGlobalScope::PreloadRealmGlobalScope(
+ ExecutionContext* initiator_execution_context)
+ : ExecutionContext(initiator_execution_context->GetIsolate(),
+ initiator_execution_context->GetAgent()),
+ initiator_execution_context_(initiator_execution_context) {}
+
+void PreloadRealmGlobalScope::Trace(Visitor* visitor) const {
+ visitor->Trace(initiator_execution_context_);
+ ScriptWrappable::Trace(visitor);
+ UniversalGlobalScope::Trace(visitor);
+ ExecutionContext::Trace(visitor);
+}
+
+ExecutionContext* PreloadRealmGlobalScope::GetExecutionContext() const {
+ return const_cast<PreloadRealmGlobalScope*>(this);
+}
+
+// Preload scripts reach the browser through Electron's IPC, never through
+// Blink's own interfaces.
+const BrowserInterfaceBrokerProxy&
+PreloadRealmGlobalScope::GetBrowserInterfaceBroker() const {
+ return GetEmptyBrowserInterfaceBroker();
+}
+
+scoped_refptr<base::SingleThreadTaskRunner>
+PreloadRealmGlobalScope::GetTaskRunner(TaskType task_type) {
+ return initiator_execution_context_->GetTaskRunner(task_type);
+}
+
+// Preload scripts are app code, so their feature usage is not counted as the
+// initiator's.
+void PreloadRealmGlobalScope::CountUse(mojom::blink::WebFeature feature) {}
+
+void PreloadRealmGlobalScope::CountDeprecation(
+ mojom::blink::WebFeature feature) {}
+
+void PreloadRealmGlobalScope::CountWebDXFeature(
+ mojom::blink::WebDXFeature feature) {}
+
+bool PreloadRealmGlobalScope::IsPreloadRealmGlobalScope() const {
+ return true;
+}
+
+const KURL& PreloadRealmGlobalScope::Url() const {
+ return initiator_execution_context_->Url();
+}
+
+const KURL& PreloadRealmGlobalScope::BaseURL() const {
+ return initiator_execution_context_->BaseURL();
+}
+
+KURL PreloadRealmGlobalScope::CompleteURL(const String& url) const {
+ return initiator_execution_context_->CompleteURL(url);
+}
+
+// No Content Security Policy is ever bound to a preload realm, so nothing
+// can ask it to disable eval.
+void PreloadRealmGlobalScope::DisableEval(const String& error_message) {
+ NOTREACHED();
+}
+
+void PreloadRealmGlobalScope::SetWasmEvalErrorMessage(
+ const String& error_message) {
+ NOTREACHED();
+}
+
+String PreloadRealmGlobalScope::UserAgent() const {
+ return initiator_execution_context_->UserAgent();
+}
+
+HttpsState PreloadRealmGlobalScope::GetHttpsState() const {
+ return initiator_execution_context_->GetHttpsState();
+}
+
+// No interface that loads resources is exposed to a preload realm.
+ResourceFetcher* PreloadRealmGlobalScope::Fetcher() {
+ NOTREACHED();
+}
+
+void PreloadRealmGlobalScope::ExceptionThrown(
+ ErrorEvent* error_event,
+ SanitizeScriptErrors sanitize_script_errors) {
+ initiator_execution_context_->ExceptionThrown(error_event,
+ sanitize_script_errors);
+}
+
+void PreloadRealmGlobalScope::AddInspectorIssue(AuditsIssue issue) {
+ initiator_execution_context_->AddInspectorIssue(std::move(issue));
+}
+
+// A preload realm has no `onerror`: uncaught errors go straight to
+// ExceptionThrown().
+EventTarget* PreloadRealmGlobalScope::ErrorEventTarget() {
+ return nullptr;
+}
+
+FrameOrWorkerScheduler* PreloadRealmGlobalScope::GetScheduler() {
+ return initiator_execution_context_->GetScheduler();
+}
+
+bool PreloadRealmGlobalScope::CrossOriginIsolatedCapability() const {
+ return false;
+}
+
+bool PreloadRealmGlobalScope::IsIsolatedContext() const {
+ return false;
+}
+
+ukm::UkmRecorder* PreloadRealmGlobalScope::UkmRecorder() {
+ return nullptr;
+}
+
+ukm::SourceId PreloadRealmGlobalScope::UkmSourceID() const {
+ return ukm::kInvalidSourceId;
+}
+
+// A preload realm identifies as its initiator, like an isolated world
+// identifies as its frame. A token type of its own would have to be taught to
+// every consumer of ExecutionContextToken in the browser process.
+ExecutionContextToken PreloadRealmGlobalScope::GetExecutionContextToken()
+ const {
+ return initiator_execution_context_->GetExecutionContextToken();
+}
+
+bool PreloadRealmGlobalScope::IsSecureContext() const {
+ return initiator_execution_context_->IsSecureContext();
+}
+
+void PreloadRealmGlobalScope::AddConsoleMessageImpl(ConsoleMessage* message,
+ bool discard_duplicates) {
+ initiator_execution_context_->AddConsoleMessage(message, discard_duplicates);
+}
+
+} // namespace blink
diff --git a/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d525b9ddf3009dd93280901a129a72c7a8a8990
--- /dev/null
+++ b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.h
@@ -0,0 +1,89 @@
+// Copyright (c) 2026 Anthropic, PBC.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PRELOAD_REALM_PRELOAD_REALM_GLOBAL_SCOPE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_PRELOAD_REALM_PRELOAD_REALM_GLOBAL_SCOPE_H_
+
+#include "base/task/single_thread_task_runner.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/universal_global_scope.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/casting.h"
+
+namespace blink {
+
+// Global scope of the context Electron runs preload scripts in when the
+// context they are attached to (the "initiator") has no isolated worlds of its
+// own, as is the case for service workers. It exposes only the interfaces
+// marked [Exposed=PreloadRealm] or [Exposed=*] and otherwise defers to the
+// initiator, which owns it.
+class CORE_EXPORT PreloadRealmGlobalScope final : public ScriptWrappable,
+ public UniversalGlobalScope,
+ public ExecutionContext {
+ DEFINE_WRAPPERTYPEINFO();
+
+ public:
+ explicit PreloadRealmGlobalScope(
+ ExecutionContext* initiator_execution_context);
+
+ ExecutionContext* GetInitiatorExecutionContext() const {
+ return initiator_execution_context_.Get();
+ }
+
+ void Trace(Visitor* visitor) const override;
+
+ // UniversalGlobalScope:
+ ExecutionContext* GetExecutionContext() const override;
+
+ // MojoBindingContext:
+ const BrowserInterfaceBrokerProxy& GetBrowserInterfaceBroker() const override;
+ scoped_refptr<base::SingleThreadTaskRunner> GetTaskRunner(
+ TaskType task_type) override;
+
+ // UseCounter:
+ void CountUse(mojom::blink::WebFeature feature) override;
+ void CountDeprecation(mojom::blink::WebFeature feature) override;
+ void CountWebDXFeature(mojom::blink::WebDXFeature feature) override;
+
+ // ExecutionContext:
+ bool IsPreloadRealmGlobalScope() const override;
+ const KURL& Url() const override;
+ const KURL& BaseURL() const override;
+ KURL CompleteURL(const String& url) const override;
+ void DisableEval(const String& error_message) override;
+ void SetWasmEvalErrorMessage(const String& error_message) override;
+ String UserAgent() const override;
+ HttpsState GetHttpsState() const override;
+ ResourceFetcher* Fetcher() override;
+ void ExceptionThrown(ErrorEvent* error_event,
+ SanitizeScriptErrors sanitize_script_errors) override;
+ void AddInspectorIssue(AuditsIssue issue) override;
+ EventTarget* ErrorEventTarget() override;
+ FrameOrWorkerScheduler* GetScheduler() override;
+ bool CrossOriginIsolatedCapability() const override;
+ bool IsIsolatedContext() const override;
+ ukm::UkmRecorder* UkmRecorder() override;
+ ukm::SourceId UkmSourceID() const override;
+ ExecutionContextToken GetExecutionContextToken() const override;
+ bool IsSecureContext() const override;
+
+ private:
+ void AddConsoleMessageImpl(ConsoleMessage* message,
+ bool discard_duplicates) override;
+
+ const Member<ExecutionContext> initiator_execution_context_;
+};
+
+template <>
+struct DowncastTraits<PreloadRealmGlobalScope> {
+ static bool AllowFrom(const ExecutionContext& context) {
+ return context.IsPreloadRealmGlobalScope();
+ }
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_PRELOAD_REALM_PRELOAD_REALM_GLOBAL_SCOPE_H_
diff --git a/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.idl b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.idl
new file mode 100644
index 0000000000000000000000000000000000000000..c74c96d418c86996ca4754f54751e15591478357
--- /dev/null
+++ b/third_party/blink/renderer/core/preload_realm/preload_realm_global_scope.idl
@@ -0,0 +1,12 @@
+// Copyright (c) 2026 Anthropic, PBC.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+// Global scope of the context Electron runs service worker preload scripts in.
+
+[
+ Exposed=PreloadRealm,
+ Global=PreloadRealm
+] interface PreloadRealmGlobalScope {};
+
+PreloadRealmGlobalScope includes UniversalGlobalScope;
diff --git a/third_party/blink/renderer/core/url/url.idl b/third_party/blink/renderer/core/url/url.idl
index 2f83c1225068ac03d3ebf75515cf0f4ea191db2d..74abf60de4f4922f168abd5b055fb1ef2a869cf2 100644
--- a/third_party/blink/renderer/core/url/url.idl
+++ b/third_party/blink/renderer/core/url/url.idl
@@ -27,7 +27,7 @@
// https://url.spec.whatwg.org/#url
[
- Exposed=(Window,Worker,ShadowRealm), // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm), // TODO(crbug.com/41480387): This should be Exposed=*
LegacyWindowAlias=webkitURL
] interface URL {
[RaisesException] constructor(USVString url, optional USVString base);
diff --git a/third_party/blink/renderer/core/url/url_search_params.idl b/third_party/blink/renderer/core/url/url_search_params.idl
index 2ac09cbcf12b2bb0213f0a117d8981ad212f9719..ceb2903ae6a7d084db4e1b0f5e002a41f8b3c2cb 100644
--- a/third_party/blink/renderer/core/url/url_search_params.idl
+++ b/third_party/blink/renderer/core/url/url_search_params.idl
@@ -5,7 +5,7 @@
// https://url.spec.whatwg.org/#interface-urlsearchparams
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface URLSearchParams {
[RaisesException] constructor(optional (sequence<sequence<USVString>> or record<USVString, USVString> or USVString) init = "");
readonly attribute unsigned long size;
diff --git a/third_party/blink/renderer/core/url_pattern/url_pattern.idl b/third_party/blink/renderer/core/url_pattern/url_pattern.idl
index e82a9bb83d39f69191077299144c2b5508274f03..9b44dcb473ee847951e8a05e91c87bc19d869aae 100644
--- a/third_party/blink/renderer/core/url_pattern/url_pattern.idl
+++ b/third_party/blink/renderer/core/url_pattern/url_pattern.idl
@@ -10,7 +10,7 @@ enum URLPatternComponent { "protocol", "username", "password", "hostname",
// https://wicg.github.io/urlpattern/
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface URLPattern {
[RaisesException, CallWith=Isolate, Measure]
constructor(URLPatternInput input, USVString baseURL, optional URLPatternOptions options = {});
diff --git a/third_party/blink/renderer/modules/compression/compression_stream.idl b/third_party/blink/renderer/modules/compression/compression_stream.idl
index 78652ab22ddfac35fa95f65dc5574e2b6ff20170..1243baa84f2fb88423070df12e26f057ef1c5d25 100644
--- a/third_party/blink/renderer/modules/compression/compression_stream.idl
+++ b/third_party/blink/renderer/modules/compression/compression_stream.idl
@@ -1,5 +1,5 @@
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface CompressionStream {
[CallWith=ScriptState, RaisesException, MeasureAs=CompressionStreamConstructor] constructor(DOMString format);
readonly attribute ReadableStream readable;
diff --git a/third_party/blink/renderer/modules/compression/decompression_stream.idl b/third_party/blink/renderer/modules/compression/decompression_stream.idl
index 7856196ec0ac3362b4745a22045f4ee71ef6370d..1c325435ccec3d29382fa394e784c131edccd83e 100644
--- a/third_party/blink/renderer/modules/compression/decompression_stream.idl
+++ b/third_party/blink/renderer/modules/compression/decompression_stream.idl
@@ -1,5 +1,5 @@
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface DecompressionStream {
[CallWith=ScriptState, RaisesException, MeasureAs=DecompressionStreamConstructor] constructor(DOMString format);
readonly attribute ReadableStream readable;
diff --git a/third_party/blink/renderer/modules/encoding/text_decoder.idl b/third_party/blink/renderer/modules/encoding/text_decoder.idl
index 4564e37db341e567fab74ddf93dd5f90423e3f4e..8f83f70b372d27e9176232dc7fe6c3af89fc44a1 100644
--- a/third_party/blink/renderer/modules/encoding/text_decoder.idl
+++ b/third_party/blink/renderer/modules/encoding/text_decoder.idl
@@ -31,7 +31,7 @@
// https://encoding.spec.whatwg.org/#textdecoder
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface TextDecoder {
[RaisesException, MeasureAs=TextDecoderConstructor] constructor(optional DOMString label = "utf-8", optional TextDecoderOptions options = {});
readonly attribute DOMString encoding;
diff --git a/third_party/blink/renderer/modules/encoding/text_decoder_stream.idl b/third_party/blink/renderer/modules/encoding/text_decoder_stream.idl
index b5f09ee82ff68cd7dab3370fbefb42a96fc371df..09b2bf58080085d8eea8f46022cc97fb47ef7397 100644
--- a/third_party/blink/renderer/modules/encoding/text_decoder_stream.idl
+++ b/third_party/blink/renderer/modules/encoding/text_decoder_stream.idl
@@ -4,7 +4,7 @@
// https://encoding.spec.whatwg.org/#interface-textdecoderstream
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface TextDecoderStream {
[CallWith=ScriptState, RaisesException, MeasureAs=TextDecoderStreamConstructor] constructor(optional DOMString label = "utf-8", optional TextDecoderOptions options = {});
readonly attribute DOMString encoding;
diff --git a/third_party/blink/renderer/modules/encoding/text_encoder.idl b/third_party/blink/renderer/modules/encoding/text_encoder.idl
index 8085e6edc8349a9d3763f825313cc3445bf06312..245d53014a80e61b24120ff898c6398b3ef3e07c 100644
--- a/third_party/blink/renderer/modules/encoding/text_encoder.idl
+++ b/third_party/blink/renderer/modules/encoding/text_encoder.idl
@@ -29,7 +29,7 @@
*/
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface TextEncoder {
[CallWith=ExecutionContext, RaisesException, MeasureAs=TextEncoderConstructor] constructor();
readonly attribute DOMString encoding;
diff --git a/third_party/blink/renderer/modules/encoding/text_encoder_stream.idl b/third_party/blink/renderer/modules/encoding/text_encoder_stream.idl
index d2c71dee48bbe0a08e4ba6dee7a28cdbef6c52a2..4daa2b3eca5da7a80475a722fb3c96b8ec3c3fc7 100644
--- a/third_party/blink/renderer/modules/encoding/text_encoder_stream.idl
+++ b/third_party/blink/renderer/modules/encoding/text_encoder_stream.idl
@@ -4,7 +4,7 @@
// https://encoding.spec.whatwg.org/#interface-textencoderstream
[
- Exposed=(Window,Worker,ShadowRealm) // TODO(crbug.com/41480387): This should be Exposed=*
+ Exposed=(Window,Worker,ShadowRealm,PreloadRealm) // TODO(crbug.com/41480387): This should be Exposed=*
] interface TextEncoderStream {
[CallWith=ScriptState, RaisesException, MeasureAs=TextEncoderStreamConstructor] constructor();
readonly attribute DOMString encoding;
@@ -0,0 +1,131 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Wed, 21 Oct 2026 22:35:04 +0900
Subject: feat: add ExecutionContext::GetRootExecutionContext()
Electron runs service worker preload scripts in a PreloadRealmGlobalScope
context on the worker thread. Because a preload realm is a secondary
ExecutionContext attached to an initiator WorkerOrWorkletGlobalScope
rather than a WorkerOrWorkletGlobalScope itself, Blink code on the
worker thread that downcasts ExecutionContext::From(script_state) via
To<WorkerOrWorkletGlobalScope>() will fail a DowncastTraits DCHECK.
Specifically, three worker thread paths need the root worker
ExecutionContext rather than the child realm's ExecutionContext:
1. PromiseRejectHandlerInWorker (v8_initializer.cc), which looks up the
worker's WorkerOrWorkletScriptController::GetRejectedPromises() queue
when an unhandled promise rejection occurs in the preload realm.
2. WorkerThreadDebugger::ReportConsoleMessage (worker_thread_debugger.cc),
which forwards console messages (including unhandled promise rejection
errors) to the worker thread's WorkerReportingProxy.
3. WorkerThreadDebugger::ContextGroupId (worker_thread_debugger.cc),
which resolves the WorkerThread's DevTools context group ID when
DevTools Command Line APIs such as getEventListeners() are evaluated
in the preload realm (or in a ShadowRealm).
Previously, (1) and (2) hardcoded an IsShadowRealmGlobalScope() check
and downcast to ShadowRealmGlobalScope::GetRootInitiatorExecutionContext(),
while (3) did not unwrap child realms at all.
Add a virtual ExecutionContext::GetRootExecutionContext() method (and
overridden by ShadowRealmGlobalScope and Electron's PreloadRealmGlobalScope to
return the root initiator ExecutionContext).
Pending upstream crbug.
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc b/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
index 41a5db4e019084d47763a43bd548b29faa070660..49206eb36d5d4dff4e2c26691f39ecb6db6f0f09 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc
@@ -450,10 +450,7 @@ static void PromiseRejectHandlerInWorker(v8::PromiseRejectMessage data) {
return;
ExecutionContext* root_worker_context =
- execution_context->IsShadowRealmGlobalScope()
- ? To<ShadowRealmGlobalScope>(execution_context)
- ->GetRootInitiatorExecutionContext()
- : execution_context;
+ execution_context->GetRootExecutionContext();
DCHECK(root_worker_context->IsWorkerOrWorkletGlobalScope());
auto* script_controller =
diff --git a/third_party/blink/renderer/core/execution_context/execution_context.h b/third_party/blink/renderer/core/execution_context/execution_context.h
index 07c9dd9c4ffa9b0b805187acce4f0eef4b641ceb..158a1b496753c192d5fe4525234e94dc5f413571 100644
--- a/third_party/blink/renderer/core/execution_context/execution_context.h
+++ b/third_party/blink/renderer/core/execution_context/execution_context.h
@@ -164,6 +164,8 @@ class CORE_EXPORT ExecutionContext : public Supplementable<ExecutionContext>,
virtual bool IsShadowRealmGlobalScope() const { return false; }
virtual bool IsJSExecutionForbidden() const { return false; }
+ virtual ExecutionContext* GetRootExecutionContext() { return this; }
+
// Notifies the execution context that new web socket activity (such as
// sending or receiving a message) has happened.
virtual void NotifyWebSocketActivity() {}
diff --git a/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc b/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
index 3a52d94a105bcc34c2319940eb10ff381fe975ba..5f0e35a3c07edd04750f3898fc73c79aff0224a9 100644
--- a/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
+++ b/third_party/blink/renderer/core/inspector/worker_thread_debugger.cc
@@ -82,11 +82,7 @@ void WorkerThreadDebugger::ReportConsoleMessage(
if (!context)
return;
- ExecutionContext* root_worker_context =
- context->IsShadowRealmGlobalScope()
- ? To<ShadowRealmGlobalScope>(context)
- ->GetRootInitiatorExecutionContext()
- : context;
+ ExecutionContext* root_worker_context = context->GetRootExecutionContext();
To<WorkerOrWorkletGlobalScope>(root_worker_context)
->GetThread()
@@ -168,7 +164,9 @@ void WorkerThreadDebugger::ExceptionThrown(WorkerThread* worker_thread,
}
int WorkerThreadDebugger::ContextGroupId(ExecutionContext* context) {
- return ContextGroupId(To<WorkerOrWorkletGlobalScope>(context)->GetThread());
+ return ContextGroupId(
+ To<WorkerOrWorkletGlobalScope>(context->GetRootExecutionContext())
+ ->GetThread());
}
void WorkerThreadDebugger::PauseWorkerOnStart(WorkerThread* worker_thread) {
diff --git a/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.cc b/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.cc
index 7b77d51ca3ca41a187021a13c213bbd723e997da..06620d26be43508a0c2e3e163ec0f049edf946b8 100644
--- a/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.cc
+++ b/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.cc
@@ -20,10 +20,7 @@ ShadowRealmGlobalScope::ShadowRealmGlobalScope(
ExecutionContext* ShadowRealmGlobalScope::GetRootInitiatorExecutionContext()
const {
- return initiator_execution_context_->IsShadowRealmGlobalScope()
- ? To<ShadowRealmGlobalScope>(initiator_execution_context_.Get())
- ->GetRootInitiatorExecutionContext()
- : initiator_execution_context_.Get();
+ return initiator_execution_context_->GetRootExecutionContext();
}
void ShadowRealmGlobalScope::Trace(Visitor* visitor) const {
@@ -63,6 +60,10 @@ bool ShadowRealmGlobalScope::IsShadowRealmGlobalScope() const {
return true;
}
+ExecutionContext* ShadowRealmGlobalScope::GetRootExecutionContext() {
+ return GetRootInitiatorExecutionContext();
+}
+
const KURL& ShadowRealmGlobalScope::Url() const {
return GetRootInitiatorExecutionContext()->Url();
}
diff --git a/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h b/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h
index 956049a3e86cba9bfd1ee15a92a49f13d105ee30..fabf4706ad2eedd4a0d00562b842276443840903 100644
--- a/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h
+++ b/third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h
@@ -45,6 +45,7 @@ class CORE_EXPORT ShadowRealmGlobalScope final : public EventTarget,
// ExecutionContext:
bool IsShadowRealmGlobalScope() const override;
+ ExecutionContext* GetRootExecutionContext() override;
const KURL& Url() const override;
const KURL& BaseURL() const override;
KURL CompleteURL(const String& url) const override;
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: David Sanders <dsanders11@ucsbalum.com>
Date: Wed, 21 Oct 2026 11:32:00 -0700
Subject: Use forward slashes for the default crdtp include dir in templates
Without this includes in generated source files on Windows look like
`#include "third_party/inspector_protocol\crdtp/cbor.h"` which can
confuse static analysis tools such as clang-tidy.
Refs https://chromium-review.googlesource.com/c/deps/inspector_protocol/+/8624663
diff --git a/third_party/inspector_protocol/code_generator.py b/third_party/inspector_protocol/code_generator.py
index 259313e258710cfb3ba4eee63aa3193123a4c230..71c88f0a3065d730476e0569dac28e0fc687cb1e 100755
--- a/third_party/inspector_protocol/code_generator.py
+++ b/third_party/inspector_protocol/code_generator.py
@@ -5,6 +5,7 @@
import os
import os.path
+import posixpath
import sys
import argparse
import collections
@@ -121,7 +122,7 @@ def read_config():
".lib.export_macro": "",
".lib.export_header": False,
".crdtp": False,
- ".crdtp.dir": os.path.join(inspector_protocol_dir, "crdtp"),
+ ".crdtp.dir": posixpath.join(inspector_protocol_dir, "crdtp"),
".crdtp.namespace": "crdtp",
}
for key_value in config_values:
@@ -796,14 +796,14 @@ v8::MaybeLocal<v8::Context> GetTargetContext(v8::Isolate* isolate,
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
} else if (execution_context->IsPreloadRealmGlobalScope()) {
} else if (maybe_target_context =
electron::preload_realm::GetInitiatorContext(source_context);
!maybe_target_context.IsEmpty()) {
if (world_id != WorldIDs::MAIN_WORLD_ID) {
isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
isolate, "Isolated worlds are not supported in preload realms.")));
return maybe_target_context;
return v8::MaybeLocal<v8::Context>();
}
maybe_target_context =
electron::preload_realm::GetInitiatorContext(source_context);
} else {
NOTREACHED();
}
+33 -49
View File
@@ -9,98 +9,94 @@
#include "base/process/process_metrics.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gc_plugin.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/gin_embedders.h"
#include "shell/common/js2c_bundle_ids.h"
#include "shell/common/node_util.h"
#include "shell/renderer/preload_environment.h"
#include "shell/renderer/preload_realm_global_scope.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/renderer/bindings/core/v8/script_state_impl.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/preload_realm/preload_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 "third_party/blink/renderer/platform/supplementable.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>(electron::kEmbedderElectron);
// This is a helper class to make the initiator ExecutionContext the owner
// of a PreloadRealmGlobalScope and its ScriptState. When the initiator
// ExecutionContext is destroyed, the PreloadRealmGlobalScope is destroyed,
// too.
class PreloadRealmLifetimeController
: public blink::GarbageCollected<PreloadRealmLifetimeController>,
public blink::Supplement<blink::ExecutionContext>,
public blink::ContextLifecycleObserver {
public:
static const char kSupplementName[];
explicit PreloadRealmLifetimeController(
blink::ExecutionContext* initiator_execution_context,
blink::ScriptState* initiator_script_state,
blink::PreloadRealmGlobalScope* preload_realm_global_scope,
blink::ScriptState* preload_realm_script_state,
electron::ServiceWorkerData* service_worker_data)
: initiator_script_state_(initiator_script_state),
: blink::Supplement<blink::ExecutionContext>(*preload_realm_global_scope),
initiator_script_state_(initiator_script_state),
is_initiator_worker_or_worklet_(
initiator_execution_context->IsWorkerOrWorkletGlobalScope()),
preload_realm_global_scope_(preload_realm_global_scope),
preload_realm_script_state_(preload_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),
v8::kEmbedderDataTypeTagDefault);
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, static_cast<void*>(this),
v8::kEmbedderDataTypeTagDefault);
blink::Supplement<blink::ExecutionContext>::ProvideTo(
*initiator_execution_context, this);
blink::Supplement<blink::ExecutionContext>::ProvideTo(
*preload_realm_global_scope, this);
metrics_ = base::ProcessMetrics::CreateCurrentProcessMetrics();
RunInitScript();
}
static PreloadRealmLifetimeController* From(v8::Local<v8::Context> context) {
if (context->GetNumberOfEmbedderDataFields() <=
kElectronContextEmbedderDataIndex) {
if (context.IsEmpty())
return nullptr;
}
auto* controller = static_cast<PreloadRealmLifetimeController*>(
context->GetAlignedPointerFromEmbedderData(
kElectronContextEmbedderDataIndex,
v8::kEmbedderDataTypeTagDefault));
CHECK(controller);
return controller;
return blink::Supplement<blink::ExecutionContext>::From<
PreloadRealmLifetimeController>(blink::ExecutionContext::From(context));
}
void Trace(blink::Visitor* visitor) const override {
visitor->Trace(initiator_script_state_);
visitor->Trace(preload_realm_global_scope_);
visitor->Trace(preload_realm_script_state_);
blink::Supplement<blink::ExecutionContext>::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
bool IsPreloadRealmContext(v8::Local<v8::Context> context) const {
return preload_realm_script_state_ &&
preload_realm_script_state_->ContextIsValid() &&
preload_realm_script_state_->GetContext() == context;
}
v8::MaybeLocal<v8::Context> GetContext() {
return preload_realm_script_state_->ContextIsValid()
return preload_realm_script_state_ &&
preload_realm_script_state_->ContextIsValid()
? preload_realm_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
v8::MaybeLocal<v8::Context> GetInitiatorContext() {
return initiator_script_state_->ContextIsValid()
return initiator_script_state_ && initiator_script_state_->ContextIsValid()
? initiator_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
@@ -112,9 +108,9 @@ class PreloadRealmLifetimeController
protected:
void ContextDestroyed() override {
v8::HandleScope handle_scope(realm_isolate());
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, nullptr,
v8::kEmbedderDataTypeTagDefault);
preload_realm_global_scope_
->blink::Supplementable<blink::ExecutionContext>::RemoveSupplement<
PreloadRealmLifetimeController>();
preload_realm_script_state_->DisposePerContextData();
// No V8 GC runs during worker thread termination, so the ScriptState's
@@ -125,8 +121,6 @@ class PreloadRealmLifetimeController
preload_realm_script_state_.Clear();
preload_realm_global_scope_->NotifyContextDestroyed();
preload_realm_global_scope_.Clear();
self_.Clear();
}
private:
@@ -136,9 +130,6 @@ class PreloadRealmLifetimeController
v8::Local<v8::Context> realm_context() {
return preload_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();
@@ -218,27 +209,20 @@ class PreloadRealmLifetimeController
std::unique_ptr<base::ProcessMetrics> metrics_;
raw_ptr<ServiceWorkerData, DanglingUntriaged> service_worker_data_;
GC_PLUGIN_IGNORE(
"Intentional GC root to keep this object alive until the context is "
"destroyed")
blink::Persistent<PreloadRealmLifetimeController> self_;
};
const char PreloadRealmLifetimeController::kSupplementName[] =
"PreloadRealmLifetimeController";
} // 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->IsPreloadRealmGlobalScope())
return v8::MaybeLocal<v8::Context>();
auto* controller = PreloadRealmLifetimeController::From(context);
if (controller) {
return controller->GetInitiatorContext();
}
return v8::MaybeLocal<v8::Context>();
if (!controller || !controller->IsPreloadRealmContext(context))
return v8::MaybeLocal<v8::Context>();
return controller->GetInitiatorContext();
}
v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
@@ -0,0 +1,158 @@
// Copyright (c) 2026 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_realm_global_scope.h"
#include <utility>
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/core/inspector/console_message.h" // nogncheck
#include "third_party/blink/renderer/core/inspector/inspector_audits_issue.h" // nogncheck
namespace blink {
PreloadRealmGlobalScope::PreloadRealmGlobalScope(
ExecutionContext* initiator_execution_context)
: ExecutionContext(initiator_execution_context->GetIsolate(),
initiator_execution_context->GetAgent()),
initiator_execution_context_(initiator_execution_context) {}
void PreloadRealmGlobalScope::Trace(Visitor* visitor) const {
visitor->Trace(initiator_execution_context_);
ScriptWrappable::Trace(visitor);
UniversalGlobalScope::Trace(visitor);
ExecutionContext::Trace(visitor);
}
ExecutionContext* PreloadRealmGlobalScope::GetExecutionContext() const {
return const_cast<PreloadRealmGlobalScope*>(this);
}
// Preload scripts reach the browser through Electron's IPC, never through
// Blink's own interfaces.
const BrowserInterfaceBrokerProxy&
PreloadRealmGlobalScope::GetBrowserInterfaceBroker() const {
return GetEmptyBrowserInterfaceBroker();
}
scoped_refptr<base::SingleThreadTaskRunner>
PreloadRealmGlobalScope::GetTaskRunner(TaskType task_type) {
return initiator_execution_context_->GetTaskRunner(task_type);
}
// Preload scripts are app code, so their feature usage is not counted as the
// initiator's.
void PreloadRealmGlobalScope::CountUse(mojom::blink::WebFeature feature) {}
void PreloadRealmGlobalScope::CountDeprecation(
mojom::blink::WebFeature feature) {}
void PreloadRealmGlobalScope::CountWebDXFeature(
mojom::blink::WebDXFeature feature) {}
// Return true so V8ScriptValueDeserializer::ExecutionContextExposesInterface
// allows structuredClone() of DOMException in preload realms without patching
// dom_exception.idl.
bool PreloadRealmGlobalScope::IsShadowRealmGlobalScope() const {
return true;
}
ExecutionContext* PreloadRealmGlobalScope::GetRootExecutionContext() {
return initiator_execution_context_->GetRootExecutionContext();
}
const KURL& PreloadRealmGlobalScope::Url() const {
return initiator_execution_context_->Url();
}
const KURL& PreloadRealmGlobalScope::BaseURL() const {
return initiator_execution_context_->BaseURL();
}
KURL PreloadRealmGlobalScope::CompleteURL(const String& url) const {
return initiator_execution_context_->CompleteURL(url);
}
// No Content Security Policy is ever bound to a preload realm, so nothing
// can ask it to disable eval.
void PreloadRealmGlobalScope::DisableEval(const String& error_message) {
NOTREACHED();
}
void PreloadRealmGlobalScope::SetWasmEvalErrorMessage(
const String& error_message) {
NOTREACHED();
}
String PreloadRealmGlobalScope::UserAgent() const {
return initiator_execution_context_->UserAgent();
}
HttpsState PreloadRealmGlobalScope::GetHttpsState() const {
return initiator_execution_context_->GetHttpsState();
}
// No interface that loads resources is exposed to a preload realm.
ResourceFetcher* PreloadRealmGlobalScope::Fetcher() {
NOTREACHED();
}
void PreloadRealmGlobalScope::ExceptionThrown(
ErrorEvent* error_event,
SanitizeScriptErrors sanitize_script_errors) {
initiator_execution_context_->ExceptionThrown(error_event,
sanitize_script_errors);
}
void PreloadRealmGlobalScope::AddInspectorIssue(AuditsIssue issue) {
initiator_execution_context_->AddInspectorIssue(std::move(issue));
}
// A preload realm has no `onerror`: uncaught errors go straight to
// ExceptionThrown().
EventTarget* PreloadRealmGlobalScope::ErrorEventTarget() {
return nullptr;
}
FrameOrWorkerScheduler* PreloadRealmGlobalScope::GetScheduler() {
return initiator_execution_context_->GetScheduler();
}
bool PreloadRealmGlobalScope::CrossOriginIsolatedCapability() const {
return false;
}
bool PreloadRealmGlobalScope::IsIsolatedContext() const {
return false;
}
ukm::UkmRecorder* PreloadRealmGlobalScope::UkmRecorder() {
return nullptr;
}
ukm::SourceId PreloadRealmGlobalScope::UkmSourceID() const {
return ukm::kInvalidSourceId;
}
// A preload realm identifies as its initiator, like an isolated world
// identifies as its frame. A token type of its own would have to be taught to
// every consumer of ExecutionContextToken in the browser process.
ExecutionContextToken PreloadRealmGlobalScope::GetExecutionContextToken()
const {
return initiator_execution_context_->GetExecutionContextToken();
}
bool PreloadRealmGlobalScope::IsSecureContext() const {
return initiator_execution_context_->IsSecureContext();
}
void PreloadRealmGlobalScope::AddConsoleMessageImpl(ConsoleMessage* message,
bool discard_duplicates) {
initiator_execution_context_->AddConsoleMessage(message, discard_duplicates);
}
} // namespace blink
@@ -0,0 +1,81 @@
// Copyright (c) 2026 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_REALM_GLOBAL_SCOPE_H_
#define ELECTRON_SHELL_RENDERER_PRELOAD_REALM_GLOBAL_SCOPE_H_
#include "base/task/single_thread_task_runner.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
#include "third_party/blink/renderer/core/frame/universal_global_scope.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" // nogncheck
#include "third_party/blink/renderer/platform/weborigin/kurl.h" // nogncheck
namespace blink {
// Global scope of the context Electron runs preload scripts in when the
// context they are attached to (the "initiator") has no isolated worlds of its
// own, as is the case for service workers. It exposes only the interfaces
// marked [Exposed=PreloadRealm] or [Exposed=*] and otherwise defers to the
// initiator, which owns it.
class PreloadRealmGlobalScope final : public ScriptWrappable,
public UniversalGlobalScope,
public ExecutionContext {
DEFINE_WRAPPERTYPEINFO();
public:
explicit PreloadRealmGlobalScope(
ExecutionContext* initiator_execution_context);
ExecutionContext* GetInitiatorExecutionContext() const {
return initiator_execution_context_.Get();
}
void Trace(Visitor* visitor) const override;
// UniversalGlobalScope:
ExecutionContext* GetExecutionContext() const override;
// MojoBindingContext:
const BrowserInterfaceBrokerProxy& GetBrowserInterfaceBroker() const override;
scoped_refptr<base::SingleThreadTaskRunner> GetTaskRunner(
TaskType task_type) override;
// UseCounter:
void CountUse(mojom::blink::WebFeature feature) override;
void CountDeprecation(mojom::blink::WebFeature feature) override;
void CountWebDXFeature(mojom::blink::WebDXFeature feature) override;
// ExecutionContext:
bool IsShadowRealmGlobalScope() const override;
ExecutionContext* GetRootExecutionContext() override;
const KURL& Url() const override;
const KURL& BaseURL() const override;
KURL CompleteURL(const String& url) const override;
void DisableEval(const String& error_message) override;
void SetWasmEvalErrorMessage(const String& error_message) override;
String UserAgent() const override;
HttpsState GetHttpsState() const override;
ResourceFetcher* Fetcher() override;
void ExceptionThrown(ErrorEvent* error_event,
SanitizeScriptErrors sanitize_script_errors) override;
void AddInspectorIssue(AuditsIssue issue) override;
EventTarget* ErrorEventTarget() override;
FrameOrWorkerScheduler* GetScheduler() override;
bool CrossOriginIsolatedCapability() const override;
bool IsIsolatedContext() const override;
ukm::UkmRecorder* UkmRecorder() override;
ukm::SourceId UkmSourceID() const override;
ExecutionContextToken GetExecutionContextToken() const override;
bool IsSecureContext() const override;
private:
void AddConsoleMessageImpl(ConsoleMessage* message,
bool discard_duplicates) override;
const Member<ExecutionContext> initiator_execution_context_;
};
} // namespace blink
#endif // ELECTRON_SHELL_RENDERER_PRELOAD_REALM_GLOBAL_SCOPE_H_
@@ -0,0 +1,12 @@
// Copyright (c) 2026 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
// Global scope of the context Electron runs service worker preload scripts in.
[
Exposed=PreloadRealm,
Global=PreloadRealm
] interface PreloadRealmGlobalScope {};
PreloadRealmGlobalScope includes UniversalGlobalScope;