Skip to content

Commit 5291096

Browse files
authored
bootstrap: check more metadata when loading the snapshot
This patch stores the metadata about the Node.js binary into the SnapshotData and adds fields denoting how the snapshot was generated, on what platform it was generated as well as the V8 cached data version flag. Instead of simply crashing when the metadata doesn't match, Node.js now prints an error message and exit with 1 for the customized snapshot, or ignore the snapshot and start from scratch if it's the default one. PR-URL: nodejs#44132 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent b427924 commit 5291096

File tree

7 files changed

+260
-17
lines changed

7 files changed

+260
-17
lines changed

doc/api/cli.md

+9
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,15 @@ in the current working directory.
11981198
When used without `--build-snapshot`, `--snapshot-blob` specifies the
11991199
path to the blob that will be used to restore the application state.
12001200

1201+
When loading a snapshot, Node.js checks that:
1202+
1203+
1. The version, architecture and platform of the running Node.js binary
1204+
are exactly the same as that of the binary that generates the snapshot.
1205+
2. The V8 flags and CPU features are compatible with that of the binary
1206+
that generates the snapshot.
1207+
1208+
If they don't match, Node.js would refuse to load the snapshot and exit with 1.
1209+
12011210
### `--test`
12021211

12031212
<!-- YAML

src/env.cc

+15
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,21 @@ std::ostream& operator<<(std::ostream& output,
261261
return output;
262262
}
263263

264+
std::ostream& operator<<(std::ostream& output, const SnapshotMetadata& i) {
265+
output << "{\n"
266+
<< " "
267+
<< (i.type == SnapshotMetadata::Type::kDefault
268+
? "SnapshotMetadata::Type::kDefault"
269+
: "SnapshotMetadata::Type::kFullyCustomized")
270+
<< ", // type\n"
271+
<< " \"" << i.node_version << "\", // node_version\n"
272+
<< " \"" << i.node_arch << "\", // node_arch\n"
273+
<< " \"" << i.node_platform << "\", // node_platform\n"
274+
<< " " << i.v8_cache_version_tag << ", // v8_cache_version_tag\n"
275+
<< "}";
276+
return output;
277+
}
278+
264279
IsolateDataSerializeInfo IsolateData::Serialize(SnapshotCreator* creator) {
265280
Isolate* isolate = creator->GetIsolate();
266281
IsolateDataSerializeInfo info;

src/env.h

+19-1
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,19 @@ struct EnvSerializeInfo {
984984
friend std::ostream& operator<<(std::ostream& o, const EnvSerializeInfo& i);
985985
};
986986

987+
struct SnapshotMetadata {
988+
// For now kFullyCustomized is only built with the --build-snapshot CLI flag.
989+
// We might want to add more types of snapshots in the future.
990+
enum class Type : uint8_t { kDefault, kFullyCustomized };
991+
992+
Type type;
993+
std::string node_version;
994+
std::string node_arch;
995+
std::string node_platform;
996+
// Result of v8::ScriptCompiler::CachedDataVersionTag().
997+
uint32_t v8_cache_version_tag;
998+
};
999+
9871000
struct SnapshotData {
9881001
enum class DataOwnership { kOwned, kNotOwned };
9891002

@@ -993,6 +1006,8 @@ struct SnapshotData {
9931006

9941007
DataOwnership data_ownership = DataOwnership::kOwned;
9951008

1009+
SnapshotMetadata metadata;
1010+
9961011
// The result of v8::SnapshotCreator::CreateBlob() during the snapshot
9971012
// building process.
9981013
v8::StartupData v8_snapshot_blob_data{nullptr, 0};
@@ -1009,7 +1024,10 @@ struct SnapshotData {
10091024
std::vector<builtins::CodeCacheInfo> code_cache;
10101025

10111026
void ToBlob(FILE* out) const;
1012-
static void FromBlob(SnapshotData* out, FILE* in);
1027+
// If returns false, the metadata doesn't match the current Node.js binary,
1028+
// and the caller should not consume the snapshot data.
1029+
bool Check() const;
1030+
static bool FromBlob(SnapshotData* out, FILE* in);
10131031

10141032
~SnapshotData();
10151033

src/node.cc

+12-2
Original file line numberDiff line numberDiff line change
@@ -1254,13 +1254,23 @@ int LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
12541254
return exit_code;
12551255
}
12561256
std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
1257-
SnapshotData::FromBlob(read_data.get(), fp);
1257+
if (!SnapshotData::FromBlob(read_data.get(), fp)) {
1258+
// If we fail to read the customized snapshot, simply exit with 1.
1259+
exit_code = 1;
1260+
return exit_code;
1261+
}
12581262
*snapshot_data_ptr = read_data.release();
12591263
fclose(fp);
12601264
} else if (per_process::cli_options->node_snapshot) {
12611265
// If --snapshot-blob is not specified, we are reading the embedded
12621266
// snapshot, but we will skip it if --no-node-snapshot is specified.
1263-
*snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
1267+
const node::SnapshotData* read_data =
1268+
SnapshotBuilder::GetEmbeddedSnapshotData();
1269+
if (read_data != nullptr && read_data->Check()) {
1270+
// If we fail to read the embedded snapshot, treat it as if Node.js
1271+
// was built without one.
1272+
*snapshot_data_ptr = read_data;
1273+
}
12641274
}
12651275

12661276
if ((*snapshot_data_ptr) != nullptr) {

src/node_internals.h

+1
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ std::ostream& operator<<(std::ostream& output,
414414
const TickInfo::SerializeInfo& d);
415415
std::ostream& operator<<(std::ostream& output,
416416
const AsyncHooks::SerializeInfo& d);
417+
std::ostream& operator<<(std::ostream& output, const SnapshotMetadata& d);
417418

418419
namespace performance {
419420
std::ostream& operator<<(std::ostream& output,

src/node_snapshotable.cc

+128-14
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,57 @@ size_t FileWriter::Write(const EnvSerializeInfo& data) {
679679
return written_total;
680680
}
681681

682+
// Layout of SnapshotMetadata
683+
// [ 1 byte ] type of the snapshot
684+
// [ 4/8 bytes ] length of the node version string
685+
// [ ... ] |length| bytes of node version
686+
// [ 4/8 bytes ] length of the node arch string
687+
// [ ... ] |length| bytes of node arch
688+
// [ 4/8 bytes ] length of the node platform string
689+
// [ ... ] |length| bytes of node platform
690+
// [ 4 bytes ] v8 cache version tag
691+
template <>
692+
SnapshotMetadata FileReader::Read() {
693+
per_process::Debug(DebugCategory::MKSNAPSHOT, "Read<SnapshotMetadata>()\n");
694+
695+
SnapshotMetadata result;
696+
result.type = static_cast<SnapshotMetadata::Type>(Read<uint8_t>());
697+
result.node_version = ReadString();
698+
result.node_arch = ReadString();
699+
result.node_platform = ReadString();
700+
result.v8_cache_version_tag = Read<uint32_t>();
701+
702+
if (is_debug) {
703+
std::string str = ToStr(result);
704+
Debug("Read<SnapshotMetadata>() %s\n", str.c_str());
705+
}
706+
return result;
707+
}
708+
709+
template <>
710+
size_t FileWriter::Write(const SnapshotMetadata& data) {
711+
if (is_debug) {
712+
std::string str = ToStr(data);
713+
Debug("\nWrite<SnapshotMetadata>() %s\n", str.c_str());
714+
}
715+
size_t written_total = 0;
716+
// We need the Node.js version, platform and arch to match because
717+
// Node.js may perform synchronizations that are platform-specific and they
718+
// can be changed in semver-patches.
719+
Debug("Write snapshot type %" PRIu8 "\n", static_cast<uint8_t>(data.type));
720+
written_total += Write<uint8_t>(static_cast<uint8_t>(data.type));
721+
Debug("Write Node.js version %s\n", data.node_version.c_str());
722+
written_total += WriteString(data.node_version);
723+
Debug("Write Node.js arch %s\n", data.node_arch);
724+
written_total += WriteString(data.node_arch);
725+
Debug("Write Node.js platform %s\n", data.node_platform);
726+
written_total += WriteString(data.node_platform);
727+
Debug("Write V8 cached data version tag %" PRIx32 "\n",
728+
data.v8_cache_version_tag);
729+
written_total += Write<uint32_t>(data.v8_cache_version_tag);
730+
return written_total;
731+
}
732+
682733
// Layout of the snapshot blob
683734
// [ 4 bytes ] kMagic
684735
// [ 4/8 bytes ] length of Node.js version string
@@ -695,13 +746,12 @@ void SnapshotData::ToBlob(FILE* out) const {
695746
w.Debug("SnapshotData::ToBlob()\n");
696747

697748
size_t written_total = 0;
749+
698750
// Metadata
699751
w.Debug("Write magic %" PRIx32 "\n", kMagic);
700752
written_total += w.Write<uint32_t>(kMagic);
701-
w.Debug("Write version %s\n", NODE_VERSION);
702-
written_total += w.WriteString(NODE_VERSION);
703-
w.Debug("Write arch %s\n", NODE_ARCH);
704-
written_total += w.WriteString(NODE_ARCH);
753+
w.Debug("Write metadata\n");
754+
written_total += w.Write<SnapshotMetadata>(metadata);
705755

706756
written_total += w.Write<v8::StartupData>(v8_snapshot_blob_data);
707757
w.Debug("Write isolate_data_indices\n");
@@ -712,22 +762,22 @@ void SnapshotData::ToBlob(FILE* out) const {
712762
w.Debug("SnapshotData::ToBlob() Wrote %d bytes\n", written_total);
713763
}
714764

715-
void SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
765+
bool SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
716766
FileReader r(in);
717767
r.Debug("SnapshotData::FromBlob()\n");
718768

769+
DCHECK_EQ(out->data_ownership, SnapshotData::DataOwnership::kOwned);
770+
719771
// Metadata
720772
uint32_t magic = r.Read<uint32_t>();
721-
r.Debug("Read magic %" PRIx64 "\n", magic);
773+
r.Debug("Read magic %" PRIx32 "\n", magic);
722774
CHECK_EQ(magic, kMagic);
723-
std::string version = r.ReadString();
724-
r.Debug("Read version %s\n", version.c_str());
725-
CHECK_EQ(version, NODE_VERSION);
726-
std::string arch = r.ReadString();
727-
r.Debug("Read arch %s\n", arch.c_str());
728-
CHECK_EQ(arch, NODE_ARCH);
775+
out->metadata = r.Read<SnapshotMetadata>();
776+
r.Debug("Read metadata\n");
777+
if (!out->Check()) {
778+
return false;
779+
}
729780

730-
DCHECK_EQ(out->data_ownership, SnapshotData::DataOwnership::kOwned);
731781
out->v8_snapshot_blob_data = r.Read<v8::StartupData>();
732782
r.Debug("Read isolate_data_info\n");
733783
out->isolate_data_info = r.Read<IsolateDataSerializeInfo>();
@@ -736,6 +786,54 @@ void SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
736786
out->code_cache = r.ReadVector<builtins::CodeCacheInfo>();
737787

738788
r.Debug("SnapshotData::FromBlob() read %d bytes\n", r.read_total);
789+
return true;
790+
}
791+
792+
bool SnapshotData::Check() const {
793+
if (metadata.node_version != per_process::metadata.versions.node) {
794+
fprintf(stderr,
795+
"Failed to load the startup snapshot because it was built with"
796+
"Node.js version %s and the current Node.js version is %s.\n",
797+
metadata.node_version.c_str(),
798+
NODE_VERSION);
799+
return false;
800+
}
801+
802+
if (metadata.node_arch != per_process::metadata.arch) {
803+
fprintf(stderr,
804+
"Failed to load the startup snapshot because it was built with"
805+
"architecture %s and the architecture is %s.\n",
806+
metadata.node_arch.c_str(),
807+
NODE_ARCH);
808+
return false;
809+
}
810+
811+
if (metadata.node_platform != per_process::metadata.platform) {
812+
fprintf(stderr,
813+
"Failed to load the startup snapshot because it was built with"
814+
"platform %s and the current platform is %s.\n",
815+
metadata.node_platform.c_str(),
816+
NODE_PLATFORM);
817+
return false;
818+
}
819+
820+
uint32_t current_cache_version = v8::ScriptCompiler::CachedDataVersionTag();
821+
if (metadata.v8_cache_version_tag != current_cache_version &&
822+
metadata.type == SnapshotMetadata::Type::kFullyCustomized) {
823+
// For now we only do this check for the customized snapshots - we know
824+
// that the flags we use in the default snapshot are limited and safe
825+
// enough so we can relax the constraints for it.
826+
fprintf(stderr,
827+
"Failed to load the startup snapshot because it was built with "
828+
"a different version of V8 or with different V8 configurations.\n"
829+
"Expected tag %" PRIx32 ", read %" PRIx32 "\n",
830+
current_cache_version,
831+
metadata.v8_cache_version_tag);
832+
return false;
833+
}
834+
835+
// TODO(joyeecheung): check incompatible Node.js flags.
836+
return true;
739837
}
740838

741839
SnapshotData::~SnapshotData() {
@@ -822,6 +920,10 @@ static const int v8_snapshot_blob_size = )"
822920
// -- data_ownership begins --
823921
SnapshotData::DataOwnership::kNotOwned,
824922
// -- data_ownership ends --
923+
// -- metadata begins --
924+
)" << data->metadata
925+
<< R"(,
926+
// -- metadata ends --
825927
// -- v8_snapshot_blob_data begins --
826928
{ v8_snapshot_blob_data, v8_snapshot_blob_size },
827929
// -- v8_snapshot_blob_data ends --
@@ -908,6 +1010,12 @@ int SnapshotBuilder::Generate(SnapshotData* out,
9081010
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
9091011
});
9101012

1013+
// It's only possible to be kDefault in node_mksnapshot.
1014+
SnapshotMetadata::Type snapshot_type =
1015+
per_process::cli_options->build_snapshot
1016+
? SnapshotMetadata::Type::kFullyCustomized
1017+
: SnapshotMetadata::Type::kDefault;
1018+
9111019
{
9121020
HandleScope scope(isolate);
9131021
TryCatch bootstrapCatch(isolate);
@@ -956,7 +1064,7 @@ int SnapshotBuilder::Generate(SnapshotData* out,
9561064
// point (we currently only support this kind of entry point, but we
9571065
// could also explore snapshotting other kinds of execution modes
9581066
// in the future).
959-
if (per_process::cli_options->build_snapshot) {
1067+
if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) {
9601068
#if HAVE_INSPECTOR
9611069
// TODO(joyeecheung): move this before RunBootstrapping().
9621070
env->InitializeInspector({});
@@ -1020,6 +1128,12 @@ int SnapshotBuilder::Generate(SnapshotData* out,
10201128
return SNAPSHOT_ERROR;
10211129
}
10221130

1131+
out->metadata = SnapshotMetadata{snapshot_type,
1132+
per_process::metadata.versions.node,
1133+
per_process::metadata.arch,
1134+
per_process::metadata.platform,
1135+
v8::ScriptCompiler::CachedDataVersionTag()};
1136+
10231137
// We cannot resurrect the handles from the snapshot, so make sure that
10241138
// no handles are left open in the environment after the blob is created
10251139
// (which should trigger a GC and close all handles that can be closed).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
// This tests that Node.js refuses to load snapshots built with incompatible
4+
// V8 configurations.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { spawnSync } = require('child_process');
9+
const tmpdir = require('../common/tmpdir');
10+
const fixtures = require('../common/fixtures');
11+
const path = require('path');
12+
const fs = require('fs');
13+
14+
tmpdir.refresh();
15+
const blobPath = path.join(tmpdir.path, 'snapshot.blob');
16+
const entry = fixtures.path('empty.js');
17+
18+
// The flag used can be any flag that makes a difference in
19+
// v8::ScriptCompiler::CachedDataVersionTag(). --harmony
20+
// is chosen here because it's stable enough and makes a difference.
21+
{
22+
// Build a snapshot with --harmony.
23+
const child = spawnSync(process.execPath, [
24+
'--harmony',
25+
'--snapshot-blob',
26+
blobPath,
27+
'--build-snapshot',
28+
entry,
29+
], {
30+
cwd: tmpdir.path
31+
});
32+
if (child.status !== 0) {
33+
console.log(child.stderr.toString());
34+
console.log(child.stdout.toString());
35+
assert.strictEqual(child.status, 0);
36+
}
37+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
38+
assert(stats.isFile());
39+
}
40+
41+
{
42+
// Now load the snapshot without --harmony, which should fail.
43+
const child = spawnSync(process.execPath, [
44+
'--snapshot-blob',
45+
blobPath,
46+
], {
47+
cwd: tmpdir.path,
48+
env: {
49+
...process.env,
50+
}
51+
});
52+
53+
const stderr = child.stderr.toString().trim();
54+
assert.match(stderr, /Failed to load the startup snapshot/);
55+
assert.strictEqual(child.status, 1);
56+
}
57+
58+
{
59+
// Load it again with --harmony and it should work.
60+
const child = spawnSync(process.execPath, [
61+
'--harmony',
62+
'--snapshot-blob',
63+
blobPath,
64+
], {
65+
cwd: tmpdir.path,
66+
env: {
67+
...process.env,
68+
}
69+
});
70+
71+
if (child.status !== 0) {
72+
console.log(child.stderr.toString());
73+
console.log(child.stdout.toString());
74+
assert.strictEqual(child.status, 0);
75+
}
76+
}

0 commit comments

Comments
 (0)