build: update v8_embedder_string from net patch effects (#54618)

* fix: derive V8 cache compatibility from net patch effects

Replace the custom V8 cache salt and Wasm embedded blob guard
from #54234 with embedder string version hashing.

Generate -electron.<16-hex fingerprint> during GN configuration from
the net committed changes between Chromium's pinned V8 revision and
patched HEAD. This requires no release time bookkeeping and preserves
cache identities across packaging only changes to the patch series.

* build: fix metadata generation and restoration

* build: address review follow-ups

- Keep code cache hash checks free of process global mutation
- Limit the helper to DCHECK enabled builds
- Make the fingerprint algorithm self contained and include it
in the source cache key
- Allow an explicit packager identity
- Skip Wasm tests without generated assets
- Correct the documentation structure

* build: simplify V8 fingerprint regeneration tracking

Track Git HEAD and the exported V8 patch series instead of symbolic
branch refs, packed-refs, or Git directories. Detached checkout updates
and patch exports trigger regeneration, while equivalent re-exports
preserve the committed source fingerprint.

Reject reftable storage explicitly and provide actionable diagnostics
when the checkout does not contain Chromium's pinned V8 revision.

* fix: gn check for release builds

* chore: convert to vitest imports

* chore: cleanup testing binding dependency

* chore: update patches

* chore: remove redundant dep declaration
This commit is contained in:
Robo authored and GitHub committed 2026-10-15 08:39:57 -05:00
1 parent 876e9ca62f
commit 4565cf72fa
21 files changed
+777 -302

No files matched your search

+4
View File
@@ -162,6 +162,10 @@ runs:
shell: bash
run: |
node src/electron/script/patches-stats.mjs --upload-stats || true
- name: Record V8 patch fingerprint for the source cache
if: ${{ steps.check-cache.outputs.cache_exists == 'false' && inputs.use-cache == 'true' }}
shell: bash
run: python3 src/electron/script/v8-patch-fingerprint.py --write-ci-cache
# delete all .git directories under src/ except for
# third_party/angle/ and third_party/dawn/ because of build time generation of files
# gen/angle/commit.h depends on third_party/angle/.git/HEAD
@@ -107,6 +107,11 @@ jobs:
cd src/electron
node script/yarn.js install --immutable
node script/yarn.js lint
- name: Run V8 Fingerprint Tests
shell: bash
run: |
cd src/electron
python3 script/v8-patch-fingerprint-test.py
- name: Run Script Typechecker
shell: bash
run: |
+21
View File
@@ -648,6 +648,10 @@ source_set("electron_lib") {
if (!is_mac) {
sources += filenames.lib_sources_views
}
if (is_debug || dcheck_always_on) {
sources += [ "shell/common/api/electron_api_testing.cc" ]
deps += [ "shell/common:v8_code_cache_test_helpers" ]
}
if (electron_generate_js2c_code_cache) {
sources += [
@@ -2049,8 +2053,22 @@ action("libcxx_objects_zip") {
args = rebase_path(outputs)
}
if (is_debug || dcheck_always_on) {
# Test runners receive these assets rather than a full V8 checkout.
copy("v8_code_cache_spec_assets") {
sources = [
"//v8/LICENSE",
"//v8/test/mjsunit/wasm/wasm-module-builder.js",
]
outputs = [ "$root_gen_dir/electron/v8-code-cache/{{source_file_part}}" ]
}
}
group("electron") {
public_deps = [ ":electron_app" ]
if (is_debug || dcheck_always_on) {
data_deps = [ ":v8_code_cache_spec_assets" ]
}
}
##### node_headers
@@ -2122,6 +2140,9 @@ group("testing_build") {
":electron_mksnapshot_zip",
":node_headers",
]
if (is_debug || dcheck_always_on) {
data_deps = [ ":v8_code_cache_spec_assets" ]
}
if (!is_mas_build) {
public_deps += [ ":electron_xcache_zip" ]
}
+22 -12
View File
@@ -8,20 +8,30 @@ node_module_version = 150
node_openssl_path = "//third_party/boringssl"
v8_promise_internal_field_count = 1
v8_embedder_string = "-electron.0"
# Salt the hash V8 uses to key code caches and serialized wasm modules with a
# fingerprint of the patches we apply on top of the V8 release. Release
# branches cherry-pick V8 fixes without changing V8's own version number, so
# without this a build with a different patch set would still accept (and
# mis-link) serialized code written by the previous release. V8 hashes these
# files at `gn gen` time and re-runs gn whenever one of them changes.
_v8_patch_dir = "//electron/patches/v8"
v8_version_hash_salt_files = [ "$_v8_patch_dir/.patches" ]
foreach(_patch, read_file("$_v8_patch_dir/.patches", "list lines")) {
if (_patch != "") {
v8_version_hash_salt_files += [ "$_v8_patch_dir/$_patch" ]
# Args are evaluated before GN installs its script interpreter. Invoke the
# hermetic Python executable directly to generate the ordinary V8 argument.
_v8_embedder_override = getenv("ELECTRON_V8_EMBEDDER_STRING")
if (_v8_embedder_override != "") {
v8_embedder_string = _v8_embedder_override
} else {
_v8_fingerprint_python = "//third_party/cpython3/host/bin/python3"
if (getenv("OS") == "Windows_NT") {
_v8_fingerprint_python += ".exe"
}
_v8_fingerprint_script = "//electron/script/v8-patch-fingerprint.py"
_v8_fingerprint_inputs = exec_script(_v8_fingerprint_python,
[
rebase_path(_v8_fingerprint_script),
"--inputs",
],
"json",
[ _v8_fingerprint_script ])
v8_embedder_string =
exec_script(_v8_fingerprint_python,
[ rebase_path(_v8_fingerprint_script) ],
"trim string",
_v8_fingerprint_inputs + [ _v8_fingerprint_script ])
}
# TODO: this breaks mksnapshot
+39
View File
@@ -94,3 +94,42 @@ $ ../../electron/script/git-import-patches -3 ../../electron/patches/node
```
If `git-import-patches -3` encounters a merge conflict that it can't resolve automatically, it will pause and allow you to resolve the conflict manually. Once you have resolved the conflict, `git add` the resolved files and continue to apply the rest of the patches by running `git am --continue`.
### V8 serialized bytecode compatibility
V8 includes its embedder string in the version hash used to validate JavaScript
code caches and serialized WebAssembly modules. Electron's common GN arguments
automatically set `v8_embedder_string` to `-electron.<fingerprint>`. No release time bump is required.
The fingerprint identifies the net committed source changes between the patched
V8 `HEAD` produced by sync and the revision pinned by Chromium's `DEPS`. It
includes changed paths, Git normalized content
identities and file modes, including additions and deletions. Patch filenames,
descriptions, authors and commit history are not inputs. Reordering, splitting or
re-exporting patches preserves the fingerprint when the resulting source is
identical. Actual source changes invalidate caches conservatively, even when
they are ABI-compatible.
Uncommitted local edits do not change the fingerprint. Compatibility between
cached code from differently modified development builds is outside this
mechanism's scope, clear local caches when testing such changes.
Local builds derive the fingerprint directly from the synced V8 Git checkout
without writing sidecar files. CI source caches deliberately omit V8 Git
metadata, so cache creation records the same Git derived fingerprint in
`v8/.electron-patch-fingerprint.json` before stripping it. Restored caches verify
the pinned V8 revision, patch series provenance and fingerprint schema before
using that identity. Missing or stale metadata fails generation rather than
substituting a weaker cache key.
The source cache key in `script/generate-deps-hash.js` hashes
`script/v8-patch-fingerprint.py`, which contains the fingerprint algorithm and
Git input handling. Changes to the algorithm or metadata schema invalidate
existing source caches automatically.
Packagers building source trees without V8 Git metadata or CI fingerprint
metadata can explicitly set `ELECTRON_V8_EMBEDDER_STRING` before GN generation
to supply their own compatibility identity. This bypasses automatic fingerprint
generation and validation; the packager must change the identity whenever V8
cache compatibility changes. GN does not track environment variable changes,
so changing or removing this override requires rerunning `gn gen` explicitly.
-1
View File
@@ -619,7 +619,6 @@ filenames = {
"shell/common/api/electron_api_net.cc",
"shell/common/api/electron_api_shared_texture.cc",
"shell/common/api/electron_api_shell.cc",
"shell/common/api/electron_api_testing.cc",
"shell/common/api/electron_api_url_loader.cc",
"shell/common/api/electron_api_url_loader.h",
"shell/common/api/electron_api_v8_util.cc",
@@ -5,11 +5,8 @@ Subject: build: allow electron to use exec_script
This is similar to the //build usecase so we're OK adding ourselves here.
//v8/gni/version_hash_salt.gni comes from our V8 patch that derives the
code-cache salt from patches/v8 at gen time.
diff --git a/.gn b/.gn
index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..e35f9ed26b0c9451c3f8f010c1940f3a19727f74 100644
index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..f2ab0a1711b94dd03ec5474e6d383161d0032a88 100644
--- a/.gn
+++ b/.gn
@@ -462,6 +462,31 @@ exec_script_allowlist =
@@ -18,7 +15,7 @@ index 3af2022171dbdebf4d1d2564f8f159124cb68c7c..e35f9ed26b0c9451c3f8f010c1940f3a
"//third_party/dawn/third_party/meson/meson_target.gni",
+
+ "//electron/BUILD.gn",
+ "//v8/gni/version_hash_salt.gni",
+ "//electron/build/args/all.gn",
+ "//third_party/electron_node/deps/ada/unofficial.gni",
+ "//third_party/electron_node/deps/base64/BUILD.gn",
+ "//third_party/electron_node/deps/base64/unofficial.gni",
-1
View File
@@ -1,4 +1,3 @@
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
src_use_legacy_trace_macros_in_perfetto_to_support_all_phases.patch
feat_key_code_caches_to_the_patch_set_and_the_embedded_blob.patch
fix_allow_embedder_slots_of_non-api_objects_in_snapshot.patch
@@ -1,230 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Sam Attard <sattard@anthropic.com>
Date: Wed, 23 Sep 2026 17:52:51 +0000
Subject: feat: key code caches to the patch set and the embedded blob
V8 keys serialized code (JS code caches and serialized wasm modules) on
Version::Hash(), i.e. on major.minor.build.patch only. Electron applies
V8 fixes on release branches as patches without changing that version
number, so a build with a different patch set still accepts serialized
code written by the previous release. Serialized wasm code stores calls
to builtins as raw Builtin ids, so when a patch adds, removes or reorders
a builtin the deserialized code is relinked against the wrong builtins
and crashes or traps at runtime.
This adds two things:
* a v8_version_hash_salt GN arg that is mixed into Version::Hash(), plus
v8_version_hash_salt_files to derive the salt at gn gen time from a
list of files. Electron points the latter at patches/v8 (see
build/args/all.gn), so any change to the patch set invalidates every
code cache and every serialized wasm module. The salt reaches
version.cc through a header written at gn gen time so that changing it
recompiles one file.
* the embedded blob's data and code hashes in the serialized wasm module
header, so that a module is only ever deserialized into a build whose
builtins are identical to the one that produced it, whatever the
version number says.
Both parts are suitable for upstreaming.
diff --git a/BUILD.gn b/BUILD.gn
index cbab33240a3e8a9007ebacc6e7d0f954a8c22b1b..bfe1248266f9e5c90ad0e77a99923b99af7cce4f 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -21,6 +21,7 @@ import("//third_party/icu/config.gni")
import("gni/cluster_build.gni")
import("gni/snapshot_toolchain.gni")
import("gni/v8.gni")
+import("gni/version_hash_salt.gni")
if (is_clang) {
import("//build/config/clang/clang.gni")
@@ -1330,6 +1331,14 @@ enabled_external_defines =
disabled_external_defines =
disabled_external_v8_defines + disabled_external_cppgc_defines
+# The salt is written to a generated header that only src/utils/version.cc
+# includes, so that changing it recompiles a single file rather than all of
+# V8. write_file() only touches the file when its contents change.
+assert(v8_version_hash_salt_files == [] || v8_version_hash_salt != "",
+ "v8_version_hash_salt_files is set but produced an empty salt")
+write_file("$target_gen_dir/src/utils/version-hash-salt.inc",
+ [ "#define V8_VERSION_HASH_SALT \"$v8_version_hash_salt\"" ])
+
# Put defines here that are only used in our internal files and NEVER in
# external headers that embedders (such as chromium and node) might include.
config("features") {
diff --git a/gni/version_hash_salt.gni b/gni/version_hash_salt.gni
new file mode 100644
index 0000000000000000000000000000000000000000..80031afb917abf21a175198537a4d2b784a66ee6
--- /dev/null
+++ b/gni/version_hash_salt.gni
@@ -0,0 +1,27 @@
+# Copyright 2026 the V8 project authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+declare_args() {
+ # An arbitrary string that is mixed into the hash that keys code caches and
+ # serialized wasm modules (see Version::Hash). Embedders that carry patches
+ # on top of a V8 release should derive this from their patch set so that
+ # serialized code produced by a differently-patched build of the same V8
+ # version is rejected instead of deserialized.
+ v8_version_hash_salt = ""
+
+ # Alternatively, a list of files (for example the embedder's patch series)
+ # whose contents are fingerprinted at gn gen time to produce the salt.
+ # Changing any of them re-runs gn gen.
+ v8_version_hash_salt_files = []
+}
+
+if (v8_version_hash_salt_files != []) {
+ assert(v8_version_hash_salt == "",
+ "Set either v8_version_hash_salt or v8_version_hash_salt_files")
+ v8_version_hash_salt =
+ exec_script("../tools/gen-version-hash-salt.py",
+ rebase_path(v8_version_hash_salt_files, root_build_dir),
+ "trim string",
+ v8_version_hash_salt_files)
+}
diff --git a/src/utils/version.cc b/src/utils/version.cc
index 550db661144947dbc31b5eb62c0da7ea2e6e1139..c643c809f7f33d9644dea148ee6e69ad4a1b8f2f 100644
--- a/src/utils/version.cc
+++ b/src/utils/version.cc
@@ -9,6 +9,9 @@
#include "src/base/hashing.h"
#include "src/base/strings.h"
#include "src/base/vector.h"
+// Generated at gn gen time from the v8_version_hash_salt GN arg; defines
+// V8_VERSION_HASH_SALT.
+#include "src/utils/version-hash-salt.inc"
// Define SONAME to have the build system put a specific SONAME into the
// shared library instead the generic SONAME generated from the V8 version
@@ -31,6 +34,7 @@ uint32_t Version::Hash() {
base::Hasher hasher;
hasher.Add(major_).Add(minor_).Add(build_).Add(patch_);
hasher.AddRange(base::OneByteVector(embedder_));
+ hasher.AddRange(base::OneByteVector(V8_VERSION_HASH_SALT));
return static_cast<uint32_t>(hasher.hash());
}
diff --git a/src/utils/version.h b/src/utils/version.h
index 0a0134e043722757bb85db55cf555974420e9b57..918d4bacf66e45bbd6ffc63d01de755f2ef179b9 100644
--- a/src/utils/version.h
+++ b/src/utils/version.h
@@ -27,6 +27,12 @@ class V8_EXPORT Version {
static int GetPatch() { return patch_; }
static const char* GetEmbedder() { return embedder_; }
static bool IsCandidate() { return candidate_; }
+
+ // Keys serialized code (code caches, serialized wasm modules) to the V8
+ // build that produced it. This covers the version number and embedder
+ // string plus the embedder-provided v8_version_hash_salt GN arg, so that
+ // embedders which apply patches without changing the version number still
+ // invalidate serialized code whenever their patch set changes.
static uint32_t Hash();
// Calculate the V8 version string.
diff --git a/src/wasm/wasm-serialization.cc b/src/wasm/wasm-serialization.cc
index a955b8f3a3d942d381c06b366928e8aadb9e4c9d..efb5aa20ae1f1ea87837ac029f82669440538d4f 100644
--- a/src/wasm/wasm-serialization.cc
+++ b/src/wasm/wasm-serialization.cc
@@ -9,6 +9,7 @@
#include "src/codegen/assembler-inl.h"
#include "src/debug/debug.h"
#include "src/runtime/runtime.h"
+#include "src/snapshot/embedded/embedded-data.h"
#include "src/snapshot/snapshot-data.h"
#include "src/utils/ostreams.h"
#include "src/utils/version.h"
@@ -129,9 +130,20 @@ class Reader {
const uint8_t* pos_;
};
+// Serialized code refers to builtins by id and embeds root-register-relative
+// offsets, so it is only valid for the exact embedded blob it was compiled
+// against. The version hash alone does not capture that for builds which
+// patch V8 without changing the version number.
+uint32_t EmbeddedBlobHash() {
+ EmbeddedData embedded_data = EmbeddedData::FromBlob();
+ return static_cast<uint32_t>(
+ base::hash_combine(embedded_data.EmbeddedBlobDataHash(),
+ embedded_data.EmbeddedBlobCodeHash()));
+}
+
size_t MeasureHeader(const CompileTimeImports& compile_imports) {
- return 5 * kUInt32Size + // magic number, version hash, cpu features, flags
- // hash, enabled features
+ return 6 * kUInt32Size + // magic number, version hash, embedded blob hash,
+ // cpu features, flags hash, enabled features
sizeof(typename CompileTimeImportFlags::StorageType) +
sizeof(uint32_t) + // length of constants_module.
compile_imports.constants_module().size();
@@ -142,6 +154,7 @@ void WriteHeader(Writer* writer, WasmEnabledFeatures enabled_features,
DCHECK_EQ(0, writer->bytes_written());
writer->Write(SerializedData::kMagicNumber);
writer->Write(Version::Hash());
+ writer->Write(EmbeddedBlobHash());
writer->Write(CpuFeatures::SupportedFeatures().ToIntegral());
writer->Write(FlagList::Hash());
writer->Write(enabled_features.ToIntegral());
diff --git a/src/wasm/wasm-serialization.h b/src/wasm/wasm-serialization.h
index 3e570ad246e065c12b12d10a8766e79599362298..0806e312e76d7eec05a7d586c3b32247ae977f34 100644
--- a/src/wasm/wasm-serialization.h
+++ b/src/wasm/wasm-serialization.h
@@ -31,10 +31,11 @@ class V8_EXPORT_PRIVATE WasmSerializer {
// The data header consists of (see {WriteHeader}):
// [0] magic number (uint32_t)
// [1] version hash (uint32_t)
- // [2] supported CPU features (uint32_t)
- // [3] flag hash (uint32_t)
- // [4] enabled features (via flags and OT) (uint32_t)
- // [5] compile time imports (variable size)
+ // [2] embedded blob hash (uint32_t)
+ // [3] supported CPU features (uint32_t)
+ // [4] flag hash (uint32_t)
+ // [5] enabled features (via flags and OT) (uint32_t)
+ // [6] compile time imports (variable size)
static constexpr size_t kVersionHashOffset = kUInt32Size;
private:
diff --git a/tools/gen-version-hash-salt.py b/tools/gen-version-hash-salt.py
new file mode 100755
index 0000000000000000000000000000000000000000..bd443f28d11f130ef419d7f04b5b86800bd01fc0
--- /dev/null
+++ b/tools/gen-version-hash-salt.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# Copyright 2026 the V8 project authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Prints a short, stable fingerprint of the given files.
+
+Called by GN at gen time (see gni/version_hash_salt.gni) to derive
+v8_version_hash_salt from v8_version_hash_salt_files.
+
+Usage:
+ gen-version-hash-salt.py <file1> [file2] ...
+"""
+
+import hashlib
+import os
+import sys
+
+
+def main(paths):
+ digest = hashlib.sha256()
+ for path in paths:
+ digest.update(os.path.basename(path).encode('utf-8') + b'\n')
+ with open(path, 'rb') as f:
+ # Normalise line endings so that a CRLF checkout yields the same salt.
+ digest.update(f.read().replace(b'\r\n', b'\n'))
+ print(digest.hexdigest()[:16])
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
+1
View File
@@ -49,6 +49,7 @@ move_src_dirs_if_exist() {
mkdir src_artifacts
dirs=("src/out/Default/gen/node_headers" \
"src/out/Default/gen/electron/v8-code-cache" \
"src/out/Default/overlapped-checker" \
"src/out/Default/ffmpeg" \
"src/out/Default/hunspell_dictionaries" \
+2 -1
View File
@@ -14,7 +14,8 @@ const filesToHash = [
path.resolve(__dirname, '../DEPS'),
path.resolve(__dirname, '../yarn.lock'),
path.resolve(__dirname, '../script/sysroots.json'),
path.resolve(__dirname, '../.github/actions/checkout/action.yml')
path.resolve(__dirname, '../.github/actions/checkout/action.yml'),
path.resolve(__dirname, '../script/v8-patch-fingerprint.py')
];
const addAllFiles = (dir) => {
+8 -4
View File
@@ -134,13 +134,17 @@ def update_ref(repo, ref, newvalue):
return subprocess.check_call(args)
def _run(repo, *args):
return subprocess.check_output(
['git', '--no-optional-locks', '-C', str(repo), *args])
def get_commit_for_ref(repo, ref):
args = ['git', '-C', repo, 'rev-parse', '--verify', ref]
return subprocess.check_output(args).decode('utf-8').strip()
return _run(repo, 'rev-parse', '--verify', ref).decode('utf-8').strip()
def get_commit_count(repo, commit_range):
args = ['git', '-C', repo, 'rev-list', '--count', commit_range]
return int(subprocess.check_output(args).decode('utf-8').strip())
return int(_run(repo, 'rev-list', '--count', commit_range))
def guess_base_commit(repo, ref):
"""Guess which commit the patches might be based on"""
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Focused tests for V8 patch fingerprints; requires Python and Git only."""
import importlib.util
import io
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from contextlib import redirect_stdout
from unittest import mock
SPEC = importlib.util.spec_from_file_location(
'v8_patch_fingerprint', Path(__file__).with_name('v8-patch-fingerprint.py'))
fingerprint = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(fingerprint)
def run_main(*args):
output = io.StringIO()
with mock.patch('sys.argv', ['v8-patch-fingerprint.py', *args]), redirect_stdout(output):
fingerprint.main()
return output.getvalue().strip()
class RepositoryTests(unittest.TestCase):
def setUp(self):
# Cleanup must run after the test, not after setUp.
temporary = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
prefix='electron-v8-fingerprint-')
self.addCleanup(temporary.cleanup)
self.repo = Path(temporary.name).resolve()
self.git('-c', 'init.defaultRefFormat=files',
'init', '--quiet', '--initial-branch=main')
self.git('config', 'user.name', 'Fingerprint test')
self.git('config', 'user.email', 'fingerprint@example.invalid')
self.git('config', 'commit.gpgsign', 'false')
self.git('config', 'core.autocrlf', 'false')
self.git('config', 'core.filemode', 'true')
(self.repo / 'first.txt').write_text('before\n', encoding='utf-8')
(self.repo / 'second.txt').write_text('before\n', encoding='utf-8')
self.commit('base')
self.base = self.git('rev-parse', 'HEAD')
def git(self, *args):
return subprocess.check_output(
['git', '-C', str(self.repo), *args], stderr=subprocess.PIPE
).decode().strip()
def commit(self, message):
self.git('add', '--all')
self.git('commit', '--quiet', '--message', message)
def identity(self):
return fingerprint.repository_fingerprint(self.repo, self.base)
def test_equivalent_commit_histories(self):
original = self.identity()
for name in ('first.txt', 'second.txt'):
(self.repo / name).write_text('after\n', encoding='utf-8')
self.commit('combined patch')
expected = self.identity()
self.assertNotEqual(original, expected)
self.assertRegex(expected, r'^-electron\.[0-9a-f]{16}$')
for index, order in enumerate((('first.txt', 'second.txt'),
('second.txt', 'first.txt'))):
self.git('checkout', '--quiet', '-b', f'split-{index}', self.base)
for name in order:
(self.repo / name).write_text('after\n', encoding='utf-8')
self.commit(f'change {name}')
self.assertEqual(expected, self.identity())
self.git('commit', '--amend', '--quiet', '--message', 're-exported description')
self.assertEqual(expected, self.identity())
(self.repo / 'first.txt').write_text('uncommitted\n', encoding='utf-8')
self.assertEqual(expected, self.identity())
@unittest.skipIf(os.name == 'nt', 'Requires POSIX filenames and executable modes')
def test_raw_diff_paths_modes_and_objects(self):
unusual = 'space tab\tnewline\n.txt'
(self.repo / unusual).write_text('added\n', encoding='utf-8')
(self.repo / 'first.txt').write_text('changed\n', encoding='utf-8')
(self.repo / 'first.txt').chmod(0o755)
(self.repo / 'second.txt').unlink()
self.commit('content, mode, addition and deletion')
changes = {name: (before, after) for name, before, after in
fingerprint.get_tree_diff(self.repo, self.base, 'HEAD')}
self.assertEqual(set(changes), {'first.txt', 'second.txt', unusual})
self.assertEqual(changes['first.txt'], (
('100644', self.git('rev-parse', f'{self.base}:first.txt')),
('100755', self.git('rev-parse', 'HEAD:first.txt'))))
self.assertEqual(changes['second.txt'], (
('100644', self.git('rev-parse', f'{self.base}:second.txt')),
('000000', '0' * 40)))
self.assertEqual(changes[unusual], (
('000000', '0' * 40),
('100644', self.git('rev-parse', f'HEAD:{unusual}'))))
def test_gn_inputs_track_head_and_exported_series(self):
patches = self.repo / 'exported-patches'
patches.mkdir()
manifest = patches / '.patches'
patch = patches / 'example.patch'
manifest.write_text('example.patch\n', encoding='utf-8')
patch.write_text('exported patch\n', encoding='utf-8')
with mock.patch.object(fingerprint, 'V8_ROOT', self.repo), \
mock.patch.object(fingerprint, 'PATCH_ROOT', patches), \
mock.patch.object(fingerprint, 'pinned_revision', return_value=self.base):
dependencies = {Path(name) for name in json.loads(run_main('--inputs'))}
self.assertEqual(dependencies, {
self.repo / '.git' / 'HEAD', self.repo.parent / 'DEPS', manifest, patch})
self.git('checkout', '--quiet', '--detach')
self.assertEqual(dependencies, {Path(name) for name in json.loads(run_main('--inputs'))})
additional = patches / 'additional.patch'
additional.write_text('additional exported patch\n', encoding='utf-8')
manifest.write_text('example.patch\nadditional.patch\n', encoding='utf-8')
self.assertIn(str(additional), json.loads(run_main('--inputs')))
def test_non_ancestor_has_actionable_error(self):
self.git('checkout', '--quiet', '--orphan', 'unrelated')
self.commit('unrelated root')
with self.assertRaisesRegex(ValueError, 're-sync.*v8_revision') as context:
self.identity()
self.assertIn(self.base, str(context.exception))
self.assertIn(str(self.repo), str(context.exception))
self.assertIsInstance(context.exception.__cause__, subprocess.CalledProcessError)
class MetadataTests(unittest.TestCase):
def setUp(self):
# Cleanup must run after the test, not after setUp.
temporary = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
prefix='electron-v8-metadata-')
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.metadata_path = self.root / 'metadata.json'
self.patch = self.root / 'example.patch'
self.patch.write_bytes(b'patch contents\n')
patcher = mock.patch.object(fingerprint, 'CACHE_METADATA', self.metadata_path)
patcher.start()
self.addCleanup(patcher.stop)
self.base = 'a' * 40
self.metadata = {
'schema': fingerprint.SCHEMA,
'base_revision': self.base,
'patch_provenance': fingerprint.patch_provenance([self.patch]),
'fingerprint': '-electron.0123456789abcdef',
}
def write_metadata(self, metadata):
self.metadata_path.write_text(json.dumps(metadata), encoding='utf-8')
def test_cached_metadata_validation(self):
with self.assertRaisesRegex(ValueError, 'regenerate the source cache'):
fingerprint.cached_fingerprint(self.base, [self.patch])
self.write_metadata(self.metadata)
self.assertEqual(fingerprint.cached_fingerprint(self.base, [self.patch]),
self.metadata['fingerprint'])
cases = [
('schema', 999, 'unsupported.*schema'),
('base_revision', 'b' * 40, 'does not match Chromium DEPS'),
('patch_provenance', 'wrong', 'does not match the patch series'),
('fingerprint', '-electron.not-a-hash', 'invalid V8 patch fingerprint'),
]
for field, value, message in cases:
with self.subTest(field=field, value=value):
self.write_metadata({**self.metadata, field: value})
with self.assertRaisesRegex(ValueError, message):
fingerprint.cached_fingerprint(self.base, [self.patch])
if __name__ == '__main__':
unittest.main()
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Identify net committed source changes on top of Chromium's pinned V8."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
ELECTRON_ROOT = Path(__file__).resolve().parent.parent
V8_ROOT = ELECTRON_ROOT.parent / 'v8'
PATCH_ROOT = ELECTRON_ROOT / 'patches' / 'v8'
CACHE_METADATA = V8_ROOT / '.electron-patch-fingerprint.json'
SCHEMA = 1
def run_git(repo, *args):
return subprocess.check_output(
['git', '--no-optional-locks', '-C', str(repo), *args])
def get_tree_diff(repo, before, after):
"""Return changed paths and their before/after (mode, object ID) pairs."""
records = run_git(repo, 'diff', '--raw', '-z', '--no-renames', '--abbrev=64',
before, after).split(b'\0')
changes = []
for index in range(0, len(records) - 1, 2):
before_mode, after_mode, before_id, after_id, _status = (
records[index].decode('ascii').split())
changes.append((os.fsdecode(records[index + 1]),
(before_mode[1:], before_id),
(after_mode, after_id)))
return changes
def get_ref_inputs(repo):
"""Track HEAD; commits on a branch are tracked through exported patches."""
repo = Path(repo).resolve()
ref_format = run_git(repo, 'rev-parse', '--show-ref-format').decode().strip()
# Git before 2.45 echoes this unknown option and predates reftable support.
if ref_format not in ('files', '--show-ref-format'):
raise ValueError(
f'Unsupported V8 Git ref storage {ref_format!r} in {repo}; '
'use a checkout with files-based refs for automatic GN regeneration')
path = Path(run_git(repo, 'rev-parse', '--git-path', 'HEAD').decode().strip())
if not path.is_absolute():
path = repo / path
return {path.resolve()}
def pinned_revision(deps):
match = re.search(r"""['"]v8_revision['"]:\s*['"]([0-9a-f]{40,64})['"]""",
deps.read_text(encoding='utf-8'))
if not match:
raise ValueError(f'Cannot find the pinned V8 revision in {deps}')
return match.group(1)
def hash_changes(object_format, changes):
digest = hashlib.sha256()
fields = [f'electron-v8-patch-effect-v{SCHEMA}', object_format]
for name, before, after in sorted(changes):
fields.extend([name, *before, *after])
for field in fields:
encoded = os.fsencode(field)
digest.update(len(encoded).to_bytes(8, 'big'))
digest.update(encoded)
return '-electron.' + digest.hexdigest()[:16]
def patch_inputs():
manifest = PATCH_ROOT / '.patches'
return [manifest] + [
PATCH_ROOT / name for name in manifest.read_text(encoding='utf-8').splitlines()
if name]
def patch_provenance(inputs):
digest = hashlib.sha256()
for path in inputs:
for field in (path.name.encode('utf-8'),
path.read_bytes().replace(b'\r\n', b'\n')):
digest.update(len(field).to_bytes(8, 'big'))
digest.update(field)
return digest.hexdigest()
def repository_fingerprint(repo, base):
try:
run_git(repo, 'merge-base', '--is-ancestor', base, 'HEAD')
except subprocess.CalledProcessError as error:
detail = ('does not contain' if error.returncode == 1 else
'could not be checked against')
raise ValueError(
f'V8 HEAD in {repo} {detail} Chromium\'s pinned revision {base}; '
're-sync the checkout and check any v8_revision custom override') from error
head_tree = run_git(repo, 'rev-parse', '--verify', 'HEAD^{tree}').decode().strip()
changes = get_tree_diff(repo, base, head_tree)
object_format = run_git(repo, 'rev-parse', '--show-object-format').decode().strip()
return hash_changes(object_format, changes)
def cached_fingerprint(base, inputs):
if not CACHE_METADATA.is_file():
raise ValueError(
'V8 Git metadata is unavailable and the source cache has no V8 patch '
'fingerprint metadata; regenerate the source cache from a synced checkout')
metadata = json.loads(CACHE_METADATA.read_text(encoding='utf-8'))
if not isinstance(metadata, dict):
raise ValueError('The source cache contains invalid V8 fingerprint metadata')
if metadata.get('schema') != SCHEMA:
raise ValueError('The source cache uses an unsupported V8 fingerprint schema')
if metadata.get('base_revision') != base:
raise ValueError('The source cache V8 fingerprint does not match Chromium DEPS')
if metadata.get('patch_provenance') != patch_provenance(inputs):
raise ValueError('The source cache V8 fingerprint does not match the patch series')
fingerprint = metadata.get('fingerprint')
if not isinstance(fingerprint, str) or not re.fullmatch(
r'-electron\.[0-9a-f]{16}', fingerprint):
raise ValueError('The source cache contains an invalid V8 patch fingerprint')
return fingerprint
def main():
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--inputs', action='store_true')
mode.add_argument('--write-ci-cache', action='store_true',
help='Record the Git derived identity before CI removes V8 Git metadata')
args = parser.parse_args()
base = pinned_revision(V8_ROOT.parent / 'DEPS')
has_git = (V8_ROOT / '.git').exists()
if args.write_ci_cache:
if not has_git:
parser.error('--write-ci-cache requires a synced V8 Git checkout')
fingerprint = repository_fingerprint(V8_ROOT, base)
metadata = {
'schema': SCHEMA,
'base_revision': base,
'patch_provenance': patch_provenance(patch_inputs()),
'fingerprint': fingerprint,
}
with CACHE_METADATA.open('w', encoding='utf-8', newline='\n') as output:
json.dump(metadata, output, sort_keys=True)
output.write('\n')
print(fingerprint)
return
series_inputs = patch_inputs()
fingerprint = None if has_git else cached_fingerprint(base, series_inputs)
if args.inputs:
inputs = get_ref_inputs(V8_ROOT) if has_git else {CACHE_METADATA}
inputs.update([V8_ROOT.parent / 'DEPS', *series_inputs])
print(json.dumps(sorted(str(path).replace('\\', '/') for path in inputs)))
else:
print(repository_fingerprint(V8_ROOT, base) if has_git else fingerprint)
if __name__ == '__main__':
main()
+19
View File
@@ -1,5 +1,24 @@
import("//mojo/public/tools/bindings/mojom.gni")
source_set("v8_code_cache_test_helpers") {
sources = [
"v8_code_cache_test_helpers.cc",
"v8_code_cache_test_helpers.h",
]
include_dirs = [
"//electron",
"//v8",
]
configs += [ "//v8:external_config" ]
deps = [
"//base:dcheck_is_on",
"//base:debugging_buildflags",
"//v8",
"//v8:v8_libbase",
"//v8:v8_version",
]
}
source_set("serialized_value") {
sources = [
"serialized_value.cc",
+69 -3
View File
@@ -5,11 +5,12 @@
#include <atomic>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <utility>
#include <variant>
#include "base/command_line.h"
#include "base/dcheck_is_on.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
@@ -25,6 +26,7 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/common/content_switches.h"
#include "gin/array_buffer.h"
#include "shell/browser/native_window.h"
#include "shell/browser/webauthn/electron_authenticator_request_client_delegate.h"
#include "shell/browser/window_list.h"
@@ -36,9 +38,11 @@
#include "shell/common/gin_helper/function_template_cache.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_code_cache_test_helpers.h" // nogncheck
#include "ui/accessibility/platform/ax_platform.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"
#include "v8/include/v8-wasm.h"
#include "v8/include/v8.h"
#if BUILDFLAG(IS_LINUX)
@@ -49,7 +53,6 @@
#include <windows.h>
#endif
#if DCHECK_IS_ON()
namespace {
std::atomic<int> live_callback_holder_probes{0};
@@ -96,6 +99,64 @@ int GetLiveCallbackHolderProbeCountForTesting() {
return live_callback_holder_probes.load();
}
v8::Local<v8::Value> SerializeWasmModuleForTesting(
v8::Isolate* isolate,
gin_helper::ErrorThrower thrower,
v8::Local<v8::Value> module) {
if (!module->IsWasmModuleObject()) {
thrower.ThrowTypeError("Expected a WebAssembly.Module");
return {};
}
auto serialized =
module.As<v8::WasmModuleObject>()->GetCompiledModule().Serialize();
return node::Buffer::Copy(
isolate, reinterpret_cast<const char*>(serialized.buffer.get()),
serialized.size)
.ToLocalChecked();
}
v8::Local<v8::Value> CompileWasmModuleWithCacheForTesting(
v8::Isolate* isolate,
const gin::ArrayBufferView& wire_bytes,
const gin::ArrayBufferView& cached_bytes) {
auto promise =
std::make_unique<gin_helper::Promise<v8::Local<v8::Value>>>(isolate);
auto handle = promise->GetHandle();
using ModuleResult =
std::variant<v8::Local<v8::WasmModuleObject>, v8::Local<v8::Value>>;
auto resolve = electron::AdaptCallbackForRepeating(base::BindOnce(
[](std::unique_ptr<gin_helper::Promise<v8::Local<v8::Value>>> promise,
ModuleResult result) {
if (auto* module =
std::get_if<v8::Local<v8::WasmModuleObject>>(&result)) {
v8::Local<v8::Value> value = *module;
promise->Resolve(value);
} else {
promise->Reject(std::get<v8::Local<v8::Value>>(result));
}
},
std::move(promise)));
v8::WasmModuleCompilation compilation;
compilation.SetHasCompiledModuleBytes();
compilation.OnBytesReceived(wire_bytes.span().data(),
wire_bytes.span().size());
bool accepted = false;
compilation.Finish(
isolate,
[&accepted,
&cached_bytes](v8::WasmStreaming::ModuleCachingInterface& cache) {
accepted = cache.SetCachedCompiledModuleBytes(
std::span(cached_bytes.span().data(), cached_bytes.span().size()));
},
[resolve = std::move(resolve)](ModuleResult result) {
resolve.Run(std::move(result));
});
auto result = gin_helper::Dictionary::CreateEmpty(isolate);
result.Set("accepted", accepted);
result.Set("module", handle);
return result.GetHandle();
}
class CallbackTestingHelper final {
public:
void HoldRepeatingCallback(const base::RepeatingClosure& callback) {
@@ -442,6 +503,12 @@ void Initialize(v8::Local<v8::Object> exports,
"getCachedCallbackTemplateForTesting");
dict.SetMethod<&GetLiveCallbackHolderProbeCountForTesting>(
"getLiveCallbackHolderProbeCountForTesting");
dict.SetMethod<&electron::testing::ComputeV8VersionHash>(
"computeV8VersionHashForTesting");
dict.SetMethod<&SerializeWasmModuleForTesting>(
"serializeWasmModuleForTesting");
dict.SetMethod<&CompileWasmModuleWithCacheForTesting>(
"compileWasmModuleWithCacheForTesting");
dict.SetMethod<&Log>("log");
dict.SetMethod<&GetLoggingDestination>("getLoggingDestination");
dict.SetMethod<&IsPlatformCaretBrowsingEnabled>(
@@ -479,4 +546,3 @@ void Initialize(v8::Local<v8::Object> exports,
} // namespace
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_common_testing, Initialize)
#endif
@@ -0,0 +1,28 @@
// Copyright (c) 2026 Microsoft Corporation.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/v8_code_cache_test_helpers.h"
#include "base/dcheck_is_on.h"
#if DCHECK_IS_ON()
#include "v8/include/v8-version.h"
#include "v8/src/base/hashing.h" // nogncheck
#include "v8/src/base/vector.h" // nogncheck
namespace electron::testing {
uint32_t ComputeV8VersionHash(const std::string& embedder) {
// Mirror Version::Hash without changing the process-wide embedder string.
v8::base::Hasher hasher;
hasher.Add(V8_MAJOR_VERSION)
.Add(V8_MINOR_VERSION)
.Add(V8_BUILD_NUMBER)
.Add(V8_PATCH_LEVEL);
hasher.AddRange(v8::base::OneByteVector(embedder.c_str()));
return static_cast<uint32_t>(hasher.hash());
}
} // namespace electron::testing
#endif
+17
View File
@@ -0,0 +1,17 @@
// Copyright (c) 2026 Microsoft Corporation.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
#define ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
#include <cstdint>
#include <string>
namespace electron::testing {
[[nodiscard]] uint32_t ComputeV8VersionHash(const std::string& embedder);
} // namespace electron::testing
#endif // ELECTRON_SHELL_COMMON_V8_CODE_CACHE_TEST_HELPERS_H_
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { ifdescribe, ifit, isTestingBindingAvailable, startRemoteControlApp } from './lib/spec-helpers.ts';
interface CacheResult {
matchingAccepted: boolean;
matchingValue: number;
foreignAccepted: boolean;
foreignValue: number;
rebuiltAccepted: boolean;
rebuiltValue: number;
}
const fixture = path.resolve(import.meta.dirname, 'fixtures/api/v8-code-cache.cjs');
const buildDir =
process.platform === 'darwin'
? path.resolve(path.dirname(process.execPath), '../../..')
: path.dirname(process.execPath);
const builder = path.join(buildDir, 'gen', 'electron', 'v8-code-cache', 'wasm-module-builder.js');
describe('V8 patch code cache compatibility', () => {
it('exposes a nonempty V8 version with a 16-hex Electron patch fingerprint', () => {
expect(process.versions.v8).toBeTypeOf('string');
expect(process.versions.v8).not.toBe('');
expect(process.versions.v8).toMatch(/^\d+\.\d+\.\d+(?:\.\d+)?-electron\.[0-9a-f]{16}(?: \(candidate\))?$/);
});
ifdescribe(isTestingBindingAvailable())('serialized caches', () => {
it('rejects JavaScript caches from a different patch fingerprint and falls back to source', async () => {
const { remotely } = await startRemoteControlApp();
const result: CacheResult = await remotely((file: string) => require(file).javascript(), fixture);
expect(result).toEqual({
matchingAccepted: true,
matchingValue: 42,
foreignAccepted: false,
foreignValue: 42,
rebuiltAccepted: true,
rebuiltValue: 42
});
});
ifit(fs.existsSync(builder))(
'rejects serialized WebAssembly from a different patch fingerprint and falls back safely',
async () => {
const { remotely } = await startRemoteControlApp(['--js-flags=--no-liftoff --no-wasm-lazy-compilation']);
const result: CacheResult = await remotely(
(file: string, helper: string) => require(file).webassembly(helper),
fixture,
builder
);
expect(result).toEqual({
matchingAccepted: true,
matchingValue: 1,
foreignAccepted: false,
foreignValue: 1,
rebuiltAccepted: true,
rebuiltValue: 1
});
}
);
});
});
+44 -45
View File
@@ -2733,9 +2733,6 @@ describe('webContents module', () => {
describe('caretBrowsingEnabled', () => {
afterEach(closeAllWindows);
const platformCaretBrowsing = () =>
process._linkedBinding('electron_common_testing').isPlatformCaretBrowsingEnabled();
it('defaults to false', () => {
const w = new BrowserWindow({ show: false });
expect(w.webContents.caretBrowsingEnabled).toBe(false);
@@ -2828,6 +2825,9 @@ describe('webContents module', () => {
// These depend on a binding that is only available when DCHECK_IS_ON.
ifdescribe(isTestingBindingAvailable())('process-wide accessibility notification', () => {
const platformCaretBrowsing = () =>
process._linkedBinding('electron_common_testing').isPlatformCaretBrowsingEnabled();
it('stays on while another WebContents still has caret browsing enabled', () => {
const w1 = new BrowserWindow({ show: false });
const w2 = new BrowserWindow({ show: false });
@@ -2845,6 +2845,47 @@ describe('webContents module', () => {
expect(platformCaretBrowsing()).toBe(false);
});
describe('guest preference inheritance', () => {
it('releases exactly one reference when an inherited preference is disabled', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
w.webContents.caretBrowsingEnabled = true;
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
const [, guest] = await created;
expect(guest.caretBrowsingEnabled).toBe(true);
// An inherited preference has to come with its own reference, otherwise
// the guest keeps caret browsing on with nothing holding the count up.
w.webContents.caretBrowsingEnabled = false;
expect(platformCaretBrowsing()).toBe(true);
guest.caretBrowsingEnabled = false;
expect(platformCaretBrowsing()).toBe(false);
});
it('releases an inherited reference when the guest is destroyed', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
w.webContents.caretBrowsingEnabled = true;
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
const [, guest] = await created;
expect(guest.caretBrowsingEnabled).toBe(true);
expect(platformCaretBrowsing()).toBe(true);
// An attached guest's WebContents is owned by its embedder frame, so the
// wrapper holding the reference is only deleted at garbage collection.
const destroyed = once(guest, 'destroyed');
w.close();
await destroyed;
expect(platformCaretBrowsing()).toBe(false);
});
});
it('takes at most one reference per WebContents however often it is set', () => {
const w = new BrowserWindow({ show: false });
@@ -2952,48 +2993,6 @@ describe('webContents module', () => {
expect(guest.getType()).toBe('webview');
expect(guest.caretBrowsingEnabled).toBe(true);
});
ifit(isTestingBindingAvailable())(
'releases exactly one reference when an inherited preference is disabled',
async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
w.webContents.caretBrowsingEnabled = true;
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
const [, guest] = await created;
expect(guest.caretBrowsingEnabled).toBe(true);
// An inherited preference has to come with its own reference, otherwise
// the guest keeps caret browsing on with nothing holding the count up.
w.webContents.caretBrowsingEnabled = false;
expect(platformCaretBrowsing()).toBe(true);
guest.caretBrowsingEnabled = false;
expect(platformCaretBrowsing()).toBe(false);
}
);
ifit(isTestingBindingAvailable())('releases an inherited reference when the guest is destroyed', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
w.webContents.caretBrowsingEnabled = true;
const created = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
w.loadURL('data:text/html,<webview src="data:text/html,hi"></webview>');
const [, guest] = await created;
expect(guest.caretBrowsingEnabled).toBe(true);
expect(platformCaretBrowsing()).toBe(true);
// An attached guest's WebContents is owned by its embedder frame, so the
// wrapper holding the reference is only deleted at garbage collection.
const destroyed = once(guest, 'destroyed');
w.close();
await destroyed;
expect(platformCaretBrowsing()).toBe(false);
});
});
});
+92
View File
@@ -0,0 +1,92 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const vm = require('node:vm');
const testing = process._linkedBinding('electron_common_testing');
const embedder = process.versions.v8.match(
/^\d+\.\d+\.\d+(?:\.\d+)?(-electron\.[0-9a-f]{16})(?: \(candidate\))?$/
)?.[1];
assert.ok(embedder, `V8 is missing its patch fingerprint: ${process.versions.v8}`);
const foreignEmbedder =
embedder === '-electron.0000000000000000' ? '-electron.1111111111111111' : '-electron.0000000000000000';
const versionHash = testing.computeV8VersionHashForTesting(embedder);
const foreignVersionHash = testing.computeV8VersionHashForTesting(foreignEmbedder);
assert.notEqual(versionHash, foreignVersionHash);
const readVersionHash = (cache) => (os.endianness() === 'LE' ? cache.readUInt32LE(4) : cache.readUInt32BE(4));
const foreignCache = (cache) => {
const copy = Buffer.from(cache);
if (os.endianness() === 'LE') copy.writeUInt32LE(foreignVersionHash, 4);
else copy.writeUInt32BE(foreignVersionHash, 4);
return copy;
};
const report = (matching, foreign, rebuilt) => ({
matchingAccepted: matching.accepted,
matchingValue: matching.value,
foreignAccepted: foreign.accepted,
foreignValue: foreign.value,
rebuiltAccepted: rebuilt.accepted,
rebuiltValue: rebuilt.value
});
exports.javascript = () => {
const source = '(() => 42)()';
const producer = new vm.Script(source, { filename: 'producer.js' });
const cache = producer.createCachedData();
assert.equal(readVersionHash(cache), versionHash);
const consume = (cachedData, filename) => {
const script = new vm.Script(source, { filename, cachedData });
return {
accepted: !script.cachedDataRejected,
value: script.runInNewContext(),
cache: script.createCachedData()
};
};
const matching = consume(cache, 'matching.js');
const foreign = consume(foreignCache(cache), 'foreign.js');
assert.equal(readVersionHash(foreign.cache), versionHash);
const rebuilt = consume(foreign.cache, 'rebuilt.js');
return report(matching, foreign, rebuilt);
};
exports.webassembly = async (builderPath) => {
const builderSource = fs.readFileSync(builderPath, 'utf8');
const wireBytes = vm.runInNewContext(
`${builderSource}
;(() => {
const builder = new WasmModuleBuilder();
builder.addMemory(1);
builder.addFunction('grow', makeSig([kWasmI32], [kWasmI32]))
.addBody([kExprLocalGet, 0, kExprMemoryGrow, 0])
.exportFunc();
return builder.toBuffer();
})()`,
{},
{ filename: builderPath }
);
const module = new WebAssembly.Module(wireBytes);
const cache = testing.serializeWasmModuleForTesting(module);
assert.ok(cache.length > 32, 'Expected serialized optimized Wasm code');
assert.equal(readVersionHash(cache), versionHash);
const consume = async (cachedData) => {
const result = testing.compileWasmModuleWithCacheForTesting(wireBytes, cachedData);
const module = await result.module;
const instance = new WebAssembly.Instance(module);
const value = instance.exports.grow(1);
assert.equal(instance.exports.grow(0), 2);
return {
accepted: result.accepted,
value,
cache: testing.serializeWasmModuleForTesting(module)
};
};
const matching = await consume(cache);
const foreign = await consume(foreignCache(cache));
assert.equal(readVersionHash(foreign.cache), versionHash);
const rebuilt = await consume(foreign.cache);
return report(matching, foreign, rebuilt);
};