Skip to content

Commit e638ea4

Browse files
joyeecheungdanielleadams
authored andcommitted
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: #44132 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent f071028 commit e638ea4

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
@@ -1185,6 +1185,15 @@ in the current working directory.
11851185
When used without `--build-snapshot`, `--snapshot-blob` specifies the
11861186
path to the blob that will be used to restore the application state.
11871187

1188+
When loading a snapshot, Node.js checks that:
1189+
1190+
1. The version, architecture and platform of the running Node.js binary
1191+
are exactly the same as that of the binary that generates the snapshot.
1192+
2. The V8 flags and CPU features are compatible with that of the binary
1193+
that generates the snapshot.
1194+
1195+
If they don't match, Node.js would refuse to load the snapshot and exit with 1.
1196+
11881197
### `--test`
11891198

11901199
<!-- YAML

src/env.cc

+15
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,21 @@ std::ostream& operator<<(std::ostream& output,
265265
return output;
266266
}
267267

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

src/env.h

+19-1
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,19 @@ struct EnvSerializeInfo {
954954
friend std::ostream& operator<<(std::ostream& o, const EnvSerializeInfo& i);
955955
};
956956

957+
struct SnapshotMetadata {
958+
// For now kFullyCustomized is only built with the --build-snapshot CLI flag.
959+
// We might want to add more types of snapshots in the future.
960+
enum class Type : uint8_t { kDefault, kFullyCustomized };
961+
962+
Type type;
963+
std::string node_version;
964+
std::string node_arch;
965+
std::string node_platform;
966+
// Result of v8::ScriptCompiler::CachedDataVersionTag().
967+
uint32_t v8_cache_version_tag;
968+
};
969+
957970
struct SnapshotData {
958971
enum class DataOwnership { kOwned, kNotOwned };
959972

@@ -964,6 +977,8 @@ struct SnapshotData {
964977

965978
DataOwnership data_ownership = DataOwnership::kOwned;
966979

980+
SnapshotMetadata metadata;
981+
967982
// The result of v8::SnapshotCreator::CreateBlob() during the snapshot
968983
// building process.
969984
v8::StartupData v8_snapshot_blob_data{nullptr, 0};
@@ -980,7 +995,10 @@ struct SnapshotData {
980995
std::vector<builtins::CodeCacheInfo> code_cache;
981996

982997
void ToBlob(FILE* out) const;
983-
static void FromBlob(SnapshotData* out, FILE* in);
998+
// If returns false, the metadata doesn't match the current Node.js binary,
999+
// and the caller should not consume the snapshot data.
1000+
bool Check() const;
1001+
static bool FromBlob(SnapshotData* out, FILE* in);
9841002

9851003
~SnapshotData();
9861004

src/node.cc

+12-2
Original file line numberDiff line numberDiff line change
@@ -1279,13 +1279,23 @@ int LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
12791279
return exit_code;
12801280
}
12811281
std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
1282-
SnapshotData::FromBlob(read_data.get(), fp);
1282+
if (!SnapshotData::FromBlob(read_data.get(), fp)) {
1283+
// If we fail to read the customized snapshot, simply exit with 1.
1284+
exit_code = 1;
1285+
return exit_code;
1286+
}
12831287
*snapshot_data_ptr = read_data.release();
12841288
fclose(fp);
12851289
} else if (per_process::cli_options->node_snapshot) {
12861290
// If --snapshot-blob is not specified, we are reading the embedded
12871291
// snapshot, but we will skip it if --no-node-snapshot is specified.
1288-
*snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
1292+
const node::SnapshotData* read_data =
1293+
SnapshotBuilder::GetEmbeddedSnapshotData();
1294+
if (read_data != nullptr && read_data->Check()) {
1295+
// If we fail to read the embedded snapshot, treat it as if Node.js
1296+
// was built without one.
1297+
*snapshot_data_ptr = read_data;
1298+
}
12891299
}
12901300

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

src/node_internals.h

+1
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ std::ostream& operator<<(std::ostream& output,
416416
const TickInfo::SerializeInfo& d);
417417
std::ostream& operator<<(std::ostream& output,
418418
const AsyncHooks::SerializeInfo& d);
419+
std::ostream& operator<<(std::ostream& output, const SnapshotMetadata& d);
419420

420421
namespace performance {
421422
std::ostream& operator<<(std::ostream& output,

src/node_snapshotable.cc

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

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

699750
size_t written_total = 0;
751+
700752
// Metadata
701753
w.Debug("Write magic %" PRIx32 "\n", kMagic);
702754
written_total += w.Write<uint32_t>(kMagic);
703-
w.Debug("Write version %s\n", NODE_VERSION);
704-
written_total += w.WriteString(NODE_VERSION);
705-
w.Debug("Write arch %s\n", NODE_ARCH);
706-
written_total += w.WriteString(NODE_ARCH);
755+
w.Debug("Write metadata\n");
756+
written_total += w.Write<SnapshotMetadata>(metadata);
707757

708758
written_total += w.Write<v8::StartupData>(v8_snapshot_blob_data);
709759
w.Debug("Write isolate_data_indices\n");
@@ -714,22 +764,22 @@ void SnapshotData::ToBlob(FILE* out) const {
714764
w.Debug("SnapshotData::ToBlob() Wrote %d bytes\n", written_total);
715765
}
716766

717-
void SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
767+
bool SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
718768
FileReader r(in);
719769
r.Debug("SnapshotData::FromBlob()\n");
720770

771+
DCHECK_EQ(out->data_ownership, SnapshotData::DataOwnership::kOwned);
772+
721773
// Metadata
722774
uint32_t magic = r.Read<uint32_t>();
723-
r.Debug("Read magic %" PRIx64 "\n", magic);
775+
r.Debug("Read magic %" PRIx32 "\n", magic);
724776
CHECK_EQ(magic, kMagic);
725-
std::string version = r.ReadString();
726-
r.Debug("Read version %s\n", version.c_str());
727-
CHECK_EQ(version, NODE_VERSION);
728-
std::string arch = r.ReadString();
729-
r.Debug("Read arch %s\n", arch.c_str());
730-
CHECK_EQ(arch, NODE_ARCH);
777+
out->metadata = r.Read<SnapshotMetadata>();
778+
r.Debug("Read metadata\n");
779+
if (!out->Check()) {
780+
return false;
781+
}
731782

732-
DCHECK_EQ(out->data_ownership, SnapshotData::DataOwnership::kOwned);
733783
out->v8_snapshot_blob_data = r.Read<v8::StartupData>();
734784
r.Debug("Read isolate_data_info\n");
735785
out->isolate_data_info = r.Read<IsolateDataSerializeInfo>();
@@ -738,6 +788,54 @@ void SnapshotData::FromBlob(SnapshotData* out, FILE* in) {
738788
out->code_cache = r.ReadVector<builtins::CodeCacheInfo>();
739789

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

743841
SnapshotData::~SnapshotData() {
@@ -824,6 +922,10 @@ static const int v8_snapshot_blob_size = )"
824922
// -- data_ownership begins --
825923
SnapshotData::DataOwnership::kNotOwned,
826924
// -- data_ownership ends --
925+
// -- metadata begins --
926+
)" << data->metadata
927+
<< R"(,
928+
// -- metadata ends --
827929
// -- v8_snapshot_blob_data begins --
828930
{ v8_snapshot_blob_data, v8_snapshot_blob_size },
829931
// -- v8_snapshot_blob_data ends --
@@ -920,6 +1022,12 @@ int SnapshotBuilder::Generate(SnapshotData* out,
9201022
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
9211023
});
9221024

1025+
// It's only possible to be kDefault in node_mksnapshot.
1026+
SnapshotMetadata::Type snapshot_type =
1027+
per_process::cli_options->build_snapshot
1028+
? SnapshotMetadata::Type::kFullyCustomized
1029+
: SnapshotMetadata::Type::kDefault;
1030+
9231031
{
9241032
HandleScope scope(isolate);
9251033
TryCatch bootstrapCatch(isolate);
@@ -982,7 +1090,7 @@ int SnapshotBuilder::Generate(SnapshotData* out,
9821090
// point (we currently only support this kind of entry point, but we
9831091
// could also explore snapshotting other kinds of execution modes
9841092
// in the future).
985-
if (per_process::cli_options->build_snapshot) {
1093+
if (snapshot_type == SnapshotMetadata::Type::kFullyCustomized) {
9861094
#if HAVE_INSPECTOR
9871095
// TODO(joyeecheung): move this before RunBootstrapping().
9881096
env->InitializeInspector({});
@@ -1050,6 +1158,12 @@ int SnapshotBuilder::Generate(SnapshotData* out,
10501158
return SNAPSHOT_ERROR;
10511159
}
10521160

1161+
out->metadata = SnapshotMetadata{snapshot_type,
1162+
per_process::metadata.versions.node,
1163+
per_process::metadata.arch,
1164+
per_process::metadata.platform,
1165+
v8::ScriptCompiler::CachedDataVersionTag()};
1166+
10531167
// We cannot resurrect the handles from the snapshot, so make sure that
10541168
// no handles are left open in the environment after the blob is created
10551169
// (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)