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

Notes: no-notes

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,18 @@ 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)
+
+  args += 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: @@ -137,3 +137,4 @@ build_set_mac_sdk_minimum_to_10.patch
 fix_add_macos_memory_query_fallback_to_avoid_crash.patch
 fix_resolve_dynamic_background_material_update_issue_on_windows_11.patch
 chore_restore_some_deprecated_wrapper_utility_in_gin.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 1abeb30142251c7abea1b33f7921f79d85532475..758022b4ca4c5cb69ff3df0399194dfca5c1b432 100644
+--- a/gin/v8_initializer.cc
++++ b/gin/v8_initializer.cc
+@@ -76,11 +76,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;
+@@ -225,6 +237,10 @@ constexpr std::string_view kV8FlagParam = "V8FlagParam";
+ 
+ }  // 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 "sandbox/policy/switches.h"
 #include "services/tracing/public/cpp/stack_sampling/tracing_sampler_profiler.h"
 #include "shell/app/command_line_args.h"
@@ -49,6 +54,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"
@@ -207,6 +213,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) {
@@ -230,7 +250,9 @@ std::string LoadResourceBundle(const std::string& locale) {
   return loaded_locale;
 }
 
-ElectronMainDelegate::ElectronMainDelegate() = default;
+ElectronMainDelegate::ElectronMainDelegate() {
+  gin::SetV8SnapshotValidator(base::BindRepeating(&ValidateV8Snapshot));
+}
 
 ElectronMainDelegate::~ElectronMainDelegate() = default;