Skip to content

Commit 1518b7d

Browse files
codebytereaduh95
authored andcommitted
src: keep the first snapshot blob alive for later isolates
`NewIsolate()` creates every isolate from the snapshot blob the first isolate in the process used, because V8 shares the read-only heap between isolates, and did so by keeping a pointer to the first `CreateParams`. When that blob came from an `EmbedderSnapshotData` the embedder had since released, e.g. a second `CommonEnvironmentSetup::CreateFromSnapshot()` after the first setup and its snapshot were destroyed, V8 deserialized freed memory. Record the first blob and external references under a mutex instead of copying the caller's `CreateParams`, and make `~SnapshotData()` leave that one blob allocated, since its owner can go away before the last isolate is created. Nothing is copied and `node` itself is unaffected. embedtest grows an `--embedder-run-twice` switch so the sequence can be tested. Refs: #45885 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65779 Refs: #32984 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 5e55085 commit 1518b7d

6 files changed

Lines changed: 89 additions & 12 deletions

File tree

src/api/environment.cc

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,34 @@ IsolateGroup GetOrCreateIsolateGroup() {
312312
return IsolateGroup::GetDefault();
313313
}
314314

315+
// V8 shares the read-only heap between isolates and requires them all to be
316+
// created from the same snapshot, so every isolate gets the blob and external
317+
// references the first NewIsolate() call used. ~SnapshotData() leaves that blob
318+
// alone because its owner may be gone before the last isolate is created.
319+
static Mutex first_snapshot_mutex;
320+
static bool first_snapshot_recorded = false;
321+
static v8::StartupData first_snapshot_blob{nullptr, 0};
322+
static const intptr_t* first_external_references = nullptr;
323+
324+
static void UseFirstSnapshot(Isolate::CreateParams* params) {
325+
Mutex::ScopedLock lock(first_snapshot_mutex);
326+
if (!first_snapshot_recorded) {
327+
first_snapshot_recorded = true;
328+
if (params->snapshot_blob != nullptr) {
329+
first_snapshot_blob = *params->snapshot_blob;
330+
}
331+
first_external_references = params->external_references;
332+
}
333+
params->snapshot_blob =
334+
first_snapshot_blob.data != nullptr ? &first_snapshot_blob : nullptr;
335+
params->external_references = first_external_references;
336+
}
337+
338+
bool IsFirstSnapshotBlob(const char* data) {
339+
Mutex::ScopedLock lock(first_snapshot_mutex);
340+
return first_snapshot_recorded && data == first_snapshot_blob.data;
341+
}
342+
315343
// TODO(joyeecheung): we may want to expose this, but then we need to be
316344
// careful about what we override in the params.
317345
Isolate* NewIsolate(Isolate::CreateParams* params,
@@ -327,15 +355,7 @@ Isolate* NewIsolate(Isolate::CreateParams* params,
327355
SnapshotBuilder::InitializeIsolateParams(snapshot_data, params);
328356
}
329357

330-
{
331-
// Because it uses a shared readonly-heap, V8 requires all snapshots used
332-
// for creating Isolates to be identical. This isn't really memory-safe
333-
// but also otherwise just doesn't work, and the only real alternative
334-
// is disabling shared-readonly-heap mode altogether.
335-
static Isolate::CreateParams first_params = *params;
336-
params->snapshot_blob = first_params.snapshot_blob;
337-
params->external_references = first_params.external_references;
338-
}
358+
UseFirstSnapshot(params);
339359

340360
// Register the isolate on the platform before the isolate gets initialized,
341361
// so that the isolate can access the platform during initialization.

src/node.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,9 @@ class NODE_EXTERN CommonEnvironmentSetup {
10091009
// will be empty.
10101010
// env_args will be passed through as arguments to CreateEnvironment(), after
10111011
// `isolate_data` and `context`.
1012+
// `snapshot_data` has to stay alive as long as the setup created from it,
1013+
// and every setup in a process has to use the same snapshot: all isolates
1014+
// are created from the blob the first one used.
10121015
template <typename... EnvironmentArgs>
10131016
static std::unique_ptr<CommonEnvironmentSetup> Create(
10141017
MultiIsolatePlatform* platform,

src/node_internals.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ void DefineZlibConstants(v8::Local<v8::Object> target);
348348
// addresses, so this should be used with care.
349349
v8::IsolateGroup GetOrCreateIsolateGroup();
350350

351+
// The blob every isolate is created from, see NewIsolate(). It is never freed.
352+
bool IsFirstSnapshotBlob(const char* data);
351353
v8::Isolate* NewIsolate(v8::Isolate::CreateParams* params,
352354
uv_loop_t* event_loop,
353355
MultiIsolatePlatform* platform,

src/node_snapshotable.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,8 @@ bool SnapshotData::Check() const {
697697

698698
SnapshotData::~SnapshotData() {
699699
if (data_ownership == DataOwnership::kOwned &&
700-
v8_snapshot_blob_data.data != nullptr) {
700+
v8_snapshot_blob_data.data != nullptr &&
701+
!IsFirstSnapshotBlob(v8_snapshot_blob_data.data)) {
701702
delete[] v8_snapshot_blob_data.data;
702703
}
703704
}

test/embedding/embedtest.cc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,20 @@ NODE_MAIN(int argc, node::argv_type raw_argv[]) {
7070
cppgc::InitializeProcess(platform->GetPageAllocator());
7171
V8::Initialize();
7272

73-
int ret =
74-
RunNodeInstance(platform.get(), result->args(), result->exec_args());
73+
// --embedder-run-twice: two sequential instances in one process, each
74+
// loading (and freeing) its own copy of the snapshot.
75+
std::vector<std::string> instance_args = result->args();
76+
auto twice = std::find(
77+
instance_args.begin(), instance_args.end(), "--embedder-run-twice");
78+
int runs = 1;
79+
if (twice != instance_args.end()) {
80+
instance_args.erase(twice);
81+
runs = 2;
82+
}
83+
int ret = 0;
84+
for (int i = 0; i < runs && ret == 0; i++) {
85+
ret = RunNodeInstance(platform.get(), instance_args, result->exec_args());
86+
}
7587

7688
V8::Dispose();
7789
V8::DisposePlatform();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
// Tests that an embedder can free the EmbedderSnapshotData it created an
4+
// instance from and create a second instance from a fresh copy afterwards.
5+
6+
const common = require('../common');
7+
const assert = require('assert');
8+
const tmpdir = require('../common/tmpdir');
9+
const fixtures = require('../common/fixtures');
10+
const {
11+
spawnSyncAndAssert,
12+
spawnSyncAndExitWithoutError,
13+
} = require('../common/child_process');
14+
15+
const embedtest = common.resolveBuiltBinary('embedtest');
16+
const snapshotFixture = fixtures.path('snapshot', 'echo-args.js');
17+
const blob = tmpdir.resolve('embedder-snapshot.blob');
18+
19+
tmpdir.refresh();
20+
21+
spawnSyncAndExitWithoutError(
22+
embedtest,
23+
[
24+
'--',
25+
`eval(require("fs").readFileSync(${JSON.stringify(snapshotFixture)}, "utf8"))`,
26+
'arg1', 'arg2', '--embedder-snapshot-blob', blob, '--embedder-snapshot-create',
27+
],
28+
{ cwd: tmpdir.path });
29+
30+
spawnSyncAndAssert(
31+
embedtest,
32+
['--', 'arg3', '--embedder-snapshot-blob', blob, '--embedder-run-twice'],
33+
{ cwd: tmpdir.path },
34+
{
35+
stdout(output) {
36+
assert.strictEqual(output.split('arg3').length, 3);
37+
return true;
38+
},
39+
});

0 commit comments

Comments
 (0)