Skip to content

Commit 5f77f95

Browse files
authored
src: fix crash on empty, foreign or truncated --snapshot-blob files
`node --snapshot-blob <file> main.js` aborted with an assertion when the file was empty (`ReadFileSync()` insists on reading one item), was not a Node.js snapshot (`CHECK_EQ(magic, kMagic)`) or had a zero-length startup blob, and read past the end of the buffer when a snapshot was truncated, because `BlobDeserializer` trusted every length field in the blob. `EmbedderSnapshotData::FromFile()` is documented to return an empty pointer for an invalid snapshot and crashed the same way. Bounds-check each read in `BlobDeserializer` and record the failure, have `SnapshotData::FromBlob()` print why and return false, and let `ReadFileSync()` return an empty vector for an empty file. Also add the missing space in the "built with Node.js version" messages. Refs: #38905 Refs: #47933 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65955 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 3ee2951 commit 5f77f95

5 files changed

Lines changed: 88 additions & 9 deletions

File tree

src/blob_serializer_deserializer-inl.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ std::vector<T> BlobDeserializer<Impl>::ReadVector() {
111111
if (count == 0) {
112112
return std::vector<T>();
113113
}
114+
// Every element takes at least one byte, so this bounds the allocation.
115+
if (count > sink.size() - read_total) {
116+
ok = false;
117+
return std::vector<T>();
118+
}
114119
if (is_debug) {
115120
Debug("Reading %d vector elements...\n", count);
116121
}
@@ -143,6 +148,10 @@ std::string_view BlobDeserializer<Impl>::ReadStringView(StringLogMode mode) {
143148
Debug("ReadStringView() read an empty view\n");
144149
return std::string_view();
145150
}
151+
if (length > sink.size() - read_total) {
152+
ok = false;
153+
return std::string_view();
154+
}
146155

147156
std::string_view result(sink.data() + read_total, length);
148157
Debug("%p, read %zu bytes", result.data(), result.size());
@@ -167,6 +176,11 @@ void BlobDeserializer<Impl>::ReadArithmetic(T* out, size_t count) {
167176
}
168177

169178
size_t size = sizeof(T) * count;
179+
if (!ok || count > (sink.size() - read_total) / sizeof(T)) {
180+
ok = false;
181+
memset(out, 0, size);
182+
return;
183+
}
170184
memcpy(out, sink.data() + read_total, size);
171185

172186
if (is_debug) {

src/blob_serializer_deserializer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ class BlobDeserializer : public BlobSerializerDeserializer {
4545

4646
size_t read_total = 0;
4747
std::string_view sink;
48+
// Cleared when a read would go past the end of `sink`; that read and all
49+
// later ones yield zeroes and empty views, so callers can check at the end.
50+
bool ok = true;
4851

4952
Impl* impl() { return static_cast<Impl*>(this); }
5053
const Impl* impl() const { return static_cast<const Impl*>(this); }

src/node_file_utils.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ std::vector<char> ReadFileSync(FILE* fp) {
240240
CHECK_EQ(err, 0);
241241

242242
std::vector<char> contents(size);
243+
if (size == 0) return contents;
243244
size_t num_read = fread(contents.data(), size, 1, fp);
244245
CHECK_EQ(num_read, 1);
245246
return contents;

src/node_snapshotable.cc

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,16 @@ v8::StartupData SnapshotDeserializer::Read() {
187187
int raw_size = ReadArithmetic<int>();
188188
Debug("size=%d\n", raw_size);
189189

190-
CHECK_GT(raw_size, 0); // There should be no startup data of size 0.
190+
if (raw_size <= 0 ||
191+
static_cast<size_t>(raw_size) > sink.size() - read_total) {
192+
ok = false;
193+
return v8::StartupData{nullptr, 0};
194+
}
191195
// The data pointer of v8::StartupData would be deleted so it must be new'ed.
192-
std::unique_ptr<char> buf = std::unique_ptr<char>(new char[raw_size]);
193-
ReadArithmetic<char>(buf.get(), raw_size);
196+
char* buf = new char[raw_size];
197+
ReadArithmetic<char>(buf, raw_size);
194198

195-
return v8::StartupData{buf.release(), raw_size};
199+
return v8::StartupData{buf, raw_size};
196200
}
197201

198202
template <>
@@ -646,10 +650,14 @@ bool SnapshotData::FromBlob(SnapshotData* out, std::string_view in) {
646650
// Metadata
647651
uint32_t magic = r.ReadArithmetic<uint32_t>();
648652
r.Debug("Read magic %" PRIx32 "\n", magic);
649-
CHECK_EQ(magic, kMagic);
653+
if (!r.ok || magic != kMagic) {
654+
fprintf(stderr, "The startup snapshot is not a Node.js snapshot blob.\n");
655+
return false;
656+
}
650657
out->metadata = r.Read<SnapshotMetadata>();
651658
r.Debug("Read metadata\n");
652-
if (!out->Check()) {
659+
if (!r.ok || !out->Check()) {
660+
if (!r.ok) fprintf(stderr, "The startup snapshot is truncated.\n");
653661
return false;
654662
}
655663

@@ -661,13 +669,17 @@ bool SnapshotData::FromBlob(SnapshotData* out, std::string_view in) {
661669
out->code_cache = r.ReadVector<builtins::CodeCacheInfo>();
662670

663671
r.Debug("SnapshotData::FromBlob() read %d bytes\n", r.read_total);
672+
if (!r.ok) {
673+
fprintf(stderr, "The startup snapshot is truncated.\n");
674+
return false;
675+
}
664676
return true;
665677
}
666678

667679
bool SnapshotData::Check() const {
668680
if (metadata.node_version != per_process::metadata.versions.node) {
669681
fprintf(stderr,
670-
"Failed to load the startup snapshot because it was built with"
682+
"Failed to load the startup snapshot because it was built with "
671683
"Node.js version %s and the current Node.js version is %s.\n",
672684
metadata.node_version.c_str(),
673685
NODE_VERSION);
@@ -676,7 +688,7 @@ bool SnapshotData::Check() const {
676688

677689
if (metadata.node_arch != per_process::metadata.arch) {
678690
fprintf(stderr,
679-
"Failed to load the startup snapshot because it was built with"
691+
"Failed to load the startup snapshot because it was built with "
680692
"architecture %s and the architecture is %s.\n",
681693
metadata.node_arch.c_str(),
682694
NODE_ARCH);
@@ -685,7 +697,7 @@ bool SnapshotData::Check() const {
685697

686698
if (metadata.node_platform != per_process::metadata.platform) {
687699
fprintf(stderr,
688-
"Failed to load the startup snapshot because it was built with"
700+
"Failed to load the startup snapshot because it was built with "
689701
"platform %s and the current platform is %s.\n",
690702
metadata.node_platform.c_str(),
691703
NODE_PLATFORM);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
// This tests that Node.js reports an error, rather than crashing, when the
4+
// file passed to --snapshot-blob is empty, is not a snapshot, or is truncated.
5+
6+
require('../common');
7+
const {
8+
spawnSyncAndExit,
9+
spawnSyncAndExitWithoutError,
10+
} = require('../common/child_process');
11+
const tmpdir = require('../common/tmpdir');
12+
const fixtures = require('../common/fixtures');
13+
const fs = require('fs');
14+
15+
tmpdir.refresh();
16+
const entry = fixtures.path('empty.js');
17+
18+
function expectFailure(blobPath, stderr) {
19+
spawnSyncAndExit(process.execPath, ['--snapshot-blob', blobPath, entry], {
20+
cwd: tmpdir.path,
21+
}, {
22+
status: 14,
23+
signal: null,
24+
stderr,
25+
});
26+
}
27+
28+
{
29+
const blobPath = tmpdir.resolve('empty.blob');
30+
fs.writeFileSync(blobPath, '');
31+
expectFailure(blobPath, /not a Node\.js snapshot blob/);
32+
}
33+
34+
{
35+
const blobPath = tmpdir.resolve('garbage.blob');
36+
fs.writeFileSync(blobPath, Buffer.alloc(4096, 0x61));
37+
expectFailure(blobPath, /not a Node\.js snapshot blob/);
38+
}
39+
40+
{
41+
const blobPath = tmpdir.resolve('snapshot.blob');
42+
spawnSyncAndExitWithoutError(process.execPath, [
43+
'--snapshot-blob', blobPath, '--build-snapshot', entry,
44+
], { cwd: tmpdir.path });
45+
const blob = fs.readFileSync(blobPath);
46+
const truncatedPath = tmpdir.resolve('truncated.blob');
47+
fs.writeFileSync(truncatedPath, blob.subarray(0, blob.length >> 1));
48+
expectFailure(truncatedPath, /truncated/);
49+
}

0 commit comments

Comments
 (0)