Repository: electron/electron
Pull request: fix: ensure snapshot is valid

Manual backport of #48101

See that PR for details

Notes: none

Changed files:
filename: BUILD.gn
patch: @@ -518,6 +518,10 @@ source_set("electron_lib") {
     "//v8:v8_libplatform",
   ]
 
+  if (v8_use_external_startup_data && use_v8_context_snapshot) {
+    deps += [ ":mksnapshot_checksum_gen" ]
+  }
+
   public_deps = [
     "//base",
     "//base:i18n",
@@ -772,6 +776,14 @@ source_set("electron_lib") {
   }
 }
 
+action("mksnapshot_checksum_gen") {
+  script = "build/checksum_header.py"
+  outputs = [ "$target_gen_dir/snapshot_checksum.h" ]
+  inputs = [ "$root_out_dir/$v8_context_snapshot_filename" ]
+  args = rebase_path(inputs) + rebase_path(outputs)
+  deps = [ "//tools/v8_context_snapshot" ]
+}
+
 electron_paks("packed_resources") {
   if (is_mac) {
     output_dir = "$root_gen_dir/electron_repack"

filename: build/checksum_header.py
patch: @@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import hashlib
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+
+TEMPLATE_H = """
+#ifndef ELECTRON_SNAPSHOT_CHECKSUM_H_
+#define ELECTRON_SNAPSHOT_CHECKSUM_H_
+
+namespace electron::snapshot_checksum {
+
+const std::string kChecksum = "{checksum}";
+
+}  // namespace electron::snapshot_checksum
+
+#endif  // ELECTRON_SNAPSHOT_CHECKSUM_H_
+"""
+
+def calculate_sha256(filepath):
+    sha256_hash = hashlib.sha256()
+    with open(filepath, "rb") as f:
+        for byte_block in iter(lambda: f.read(4096), b""):
+            sha256_hash.update(byte_block)
+    return sha256_hash.hexdigest()
+
+input_file = sys.argv[1]
+output_file = sys.argv[2]
+
+checksum = calculate_sha256(input_file)
+
+checksum_h = TEMPLATE_H.replace("{checksum}", checksum)
+
+with open(output_file, 'w') as f:
+    f.write(checksum_h)

filename: patches/chromium/.patches
patch: @@ -142,3 +142,4 @@ chore_grandfather_in_electron_views_and_delegates.patch
 refactor_patch_electron_permissiontypes_into_blink.patch
 fix_add_macos_memory_query_fallback_to_avoid_crash.patch
 fix_resolve_dynamic_background_material_update_issue_on_windows_11.patch
+feat_add_support_for_embedder_snapshot_validation.patch

filename: patches/chromium/feat_add_support_for_embedder_snapshot_validation.patch
patch: @@ -0,0 +1,67 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <sattard@anthropic.com>
+Date: Fri, 15 Aug 2025 14:58:12 -0700
+Subject: feat: add support for embedder snapshot validation
+
+IsValid is not exposed despite being commented as for embedders, this exposes something that works for us.
+
+diff --git a/gin/v8_initializer.cc b/gin/v8_initializer.cc
+index 03c9bc18566794a668981bba6235b226e07eff74..9ad6909dc6084202e9227801a9d200355b30d164 100644
+--- a/gin/v8_initializer.cc
++++ b/gin/v8_initializer.cc
+@@ -75,11 +75,23 @@ bool GenerateEntropy(unsigned char* buffer, size_t amount) {
+   return true;
+ }
+ 
++static base::RepeatingCallback<void(v8::StartupData*)>& SnapshotValidator() {
++  static base::NoDestructor<base::RepeatingCallback<void(v8::StartupData*)>>
++      validator(
++          base::BindRepeating([](v8::StartupData* data) -> void { /* empty */ }));
++  return *validator;
++}
++
++void SetV8SnapshotValidatorInner(const base::RepeatingCallback<void(v8::StartupData*)>& callback) {
++  SnapshotValidator() = std::move(callback);
++}
++
+ void GetMappedFileData(base::MemoryMappedFile* mapped_file,
+                        v8::StartupData* data) {
+   if (mapped_file) {
+     data->data = reinterpret_cast<const char*>(mapped_file->data());
+     data->raw_size = static_cast<int>(mapped_file->length());
++    SnapshotValidator().Run(data);
+   } else {
+     data->data = nullptr;
+     data->raw_size = 0;
+@@ -223,6 +235,10 @@ constexpr std::string_view kV8FlagFeaturePrefix = "V8Flag_";
+ 
+ }  // namespace
+ 
++void SetV8SnapshotValidator(const base::RepeatingCallback<void(v8::StartupData*)>& callback) {
++  SetV8SnapshotValidatorInner(std::move(callback));
++}
++
+ class V8FeatureVisitor : public base::FeatureVisitor {
+  public:
+   void Visit(const std::string& feature_name,
+diff --git a/gin/v8_initializer.h b/gin/v8_initializer.h
+index 6f7382cd600cd34916d9382878aee4b469dae5d0..61ed0f46437d2e1abbcebcfb64df06d17c8d9139 100644
+--- a/gin/v8_initializer.h
++++ b/gin/v8_initializer.h
+@@ -11,6 +11,7 @@
+ 
+ #include "base/files/file.h"
+ #include "base/files/memory_mapped_file.h"
++#include "base/functional/callback.h"
+ #include "build/build_config.h"
+ #include "gin/array_buffer.h"
+ #include "gin/gin_export.h"
+@@ -28,6 +29,8 @@ class StartupData;
+ 
+ namespace gin {
+ 
++void SetV8SnapshotValidator(const base::RepeatingCallback<void(v8::StartupData*)>& callback);
++
+ class GIN_EXPORT V8Initializer {
+  public:
+   // This should be called by IsolateHolder::Initialize().

filename: shell/app/electron_main_delegate.cc
patch: @@ -19,15 +19,20 @@
 #include "base/logging.h"
 #include "base/path_service.h"
 #include "base/strings/cstring_view.h"
+#include "base/strings/string_number_conversions.cc"
+#include "base/strings/string_util_internal.h"
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/chrome_switches.h"
 #include "components/content_settings/core/common/content_settings_pattern.h"
 #include "content/public/app/initialize_mojo_core.h"
 #include "content/public/common/content_switches.h"
+#include "crypto/hash.h"
 #include "electron/buildflags/buildflags.h"
 #include "electron/fuses.h"
 #include "electron/mas.h"
+#include "electron/snapshot_checksum.h"
 #include "extensions/common/constants.h"
+#include "gin/v8_initializer.h"
 #include "ipc/ipc_buildflags.h"
 #include "sandbox/policy/switches.h"
 #include "services/tracing/public/cpp/stack_sampling/tracing_sampler_profiler.h"
@@ -50,6 +55,7 @@
 #include "third_party/abseil-cpp/absl/types/variant.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/base/ui_base_switches.h"
+#include "v8/include/v8-snapshot.h"
 
 #if BUILDFLAG(IS_MAC)
 #include "shell/app/electron_main_delegate_mac.h"
@@ -208,6 +214,20 @@ void RegisterPathProvider() {
                                       PATH_END);
 }
 
+void ValidateV8Snapshot(v8::StartupData* data) {
+  if (data->data &&
+      electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled()) {
+    CHECK_GT(data->raw_size, 0);
+    UNSAFE_BUFFERS({
+      base::span<const char> span_data(
+          data->data, static_cast<unsigned long>(data->raw_size));
+      CHECK(base::ToLowerASCII(base::HexEncode(
+                crypto::hash::Sha256(base::as_bytes(span_data)))) ==
+            electron::snapshot_checksum::kChecksum);
+    })
+  }
+}
+
 }  // namespace
 
 std::string LoadResourceBundle(const std::string& locale) {
@@ -231,7 +251,9 @@ std::string LoadResourceBundle(const std::string& locale) {
   return loaded_locale;
 }
 
-ElectronMainDelegate::ElectronMainDelegate() = default;
+ElectronMainDelegate::ElectronMainDelegate() {
+  gin::SetV8SnapshotValidator(base::BindRepeating(&ValidateV8Snapshot));
+}
 
 ElectronMainDelegate::~ElectronMainDelegate() = default;