// Several node::Environments on one v8::Isolate that the embedder created,
// registered with the platform and gave a CppHeap, each in its own
// embedder-created v8::Context, driven by one event loop and freed one at a
// time while the others keep running. doc/api/embedding.md allows this and
// embedders that host Node.js inside an existing JS runtime depend on it.
#include "cppgc/allocation.h"
#include "cppgc/garbage-collected.h"
#include "env-inl.h"
#include "node_test_fixture.h"
#include "quic/guard.h"
#include "v8-cppgc.h"
#if HAVE_OPENSSL
#include "crypto/crypto_context.h"
#endif
#ifndef OPENSSL_NO_QUIC
#include "node_realm-inl.h"
#include "quic/bindingdata.h"
#endif
#include <string>
#include <vector>
using node::Environment;
using node::IsolateData;
using v8::Context;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Value;
namespace EnvironmentFlags = node::EnvironmentFlags;
namespace {
class EmbedderObject final : public v8::Object::Wrappable {
public:
static int alive;
EmbedderObject() { alive++; }
~EmbedderObject() override { alive--; }
};
int EmbedderObject::alive = 0;
void SetFlag(void* flag) {
*static_cast<bool*>(flag) = true;
}
int cleanup_hook_runs = 0;
void CountCleanupHook(void* arg) {
(*static_cast<int*>(arg))++;
}
enum class IsolateDataMode {
// One IsolateData created with the platform, shared by every Environment.
kShared,
// One IsolateData per Environment, created without a platform and freed
// together with its Environment.
kPerEnvironmentWithoutPlatform,
};
struct Instance {
int id;
v8::Global<Context> context;
IsolateData* isolate_data = nullptr;
Environment* env = nullptr;
bool exit_handler_called = false;
int exit_code = -1;
node::StopFlags::Flags stop_flags = node::StopFlags::kDoNotTerminateIsolate;
};
} // namespace
class SharedIsolateTest
: public NodeZeroIsolateTestFixture,
public ::testing::WithParamInterface<IsolateDataMode> {
protected:
Isolate* isolate_ = nullptr;
v8::CppHeap* cpp_heap_ = nullptr;
IsolateData* shared_isolate_data_ = nullptr;
void SetUp() override {
NodeZeroIsolateTestFixture::SetUp();
Isolate::CreateParams params;
params.array_buffer_allocator = allocator.get();
params.cpp_heap =
v8::CppHeap::Create(platform.get(), v8::CppHeapCreateParams{{}})
.release();
cpp_heap_ = params.cpp_heap;
isolate_ = Isolate::Allocate();
CHECK_NOT_NULL(isolate_);
platform->RegisterIsolate(isolate_, ¤t_loop);
Isolate::Initialize(isolate_, params);
node::IsolateSettings settings;
settings.flags |=
node::IsolateSettingsFlags::SHOULD_NOT_SET_PREPARE_STACK_TRACE_CALLBACK;
node::SetIsolateUpForNode(isolate_, settings);
isolate_->Enter();
if (GetParam() == IsolateDataMode::kShared) {
HandleScope handle_scope(isolate_);
shared_isolate_data_ =
node::CreateIsolateData(isolate_, ¤t_loop, platform.get());
CHECK_NOT_NULL(shared_isolate_data_);
}
}
void TearDown() override {
if (shared_isolate_data_ != nullptr) {
node::FreeIsolateData(shared_isolate_data_);
shared_isolate_data_ = nullptr;
}
EXPECT_EQ(isolate_->GetCppHeap(), cpp_heap_);
platform->DrainTasks(isolate_);
isolate_->Exit();
bool platform_finished = false;
platform->AddIsolateFinishedCallback(isolate_, SetFlag, &platform_finished);
isolate_->Dispose();
platform->UnregisterIsolate(isolate_);
while (!platform_finished) uv_run(¤t_loop, UV_RUN_ONCE);
isolate_ = nullptr;
}
std::unique_ptr<Instance> CreateInstance(int id,
EnvironmentFlags::Flags flags,
bool throw_after_bootstrap = false) {
auto instance = std::make_unique<Instance>();
instance->id = id;
HandleScope handle_scope(isolate_);
Local<Context> context = Context::New(isolate_);
CHECK(node::InitializeContext(context).FromJust());
instance->context.Reset(isolate_, context);
Context::Scope context_scope(context);
if (GetParam() == IsolateDataMode::kShared) {
instance->isolate_data = shared_isolate_data_;
} else {
instance->isolate_data =
node::CreateIsolateData(isolate_, ¤t_loop, nullptr);
CHECK_NOT_NULL(instance->isolate_data);
}
std::vector<std::string> args{"node"};
if (throw_after_bootstrap) args.push_back("--throw");
std::vector<std::string> exec_args;
instance->env = node::CreateEnvironment(
instance->isolate_data, context, args, exec_args, flags);
CHECK_NOT_NULL(instance->env);
Instance* raw = instance.get();
if (throw_after_bootstrap) instance->stop_flags = node::StopFlags::kNoFlags;
node::SetProcessExitHandler(instance->env,
[raw](Environment* env, int exit_code) {
raw->exit_handler_called = true;
raw->exit_code = exit_code;
node::Stop(env, raw->stop_flags);
});
std::string script =
"const id = " + std::to_string(id) +
";\n"
"const vm = require('vm');\n"
"const { setInterval, clearInterval, setImmediate } ="
" require('timers');\n"
"require('net');\n"
"globalThis.state = { id, ticks: 0, immediates: 0, events: [] };\n"
"const interval = setInterval(() => {\n"
" state.ticks++;\n"
" new vm.Script('1 + 1');\n"
"}, 1);\n"
"(function again() {\n"
" state.immediates++;\n"
" setImmediate(again);\n"
"})();\n"
"process.on('beforeExit', () => state.events.push('beforeExit'));\n"
"process.on('exit', () => {\n"
" state.events.push('exit');\n"
" clearInterval(interval);\n"
"});\n"
"if (process.argv.includes('--throw')) throw new Error('uncaught');\n"
"return id;\n";
v8::MaybeLocal<Value> result =
node::LoadEnvironment(instance->env, script.c_str());
if (throw_after_bootstrap) {
CHECK(result.IsEmpty());
CHECK(instance->exit_handler_called);
} else {
CHECK_EQ(result.ToLocalChecked()->Int32Value(context).FromJust(), id);
}
node::AddEnvironmentCleanupHook(
isolate_, CountCleanupHook, &cleanup_hook_runs);
return instance;
}
void FreeInstance(std::unique_ptr<Instance> instance) {
node::FreeEnvironment(instance->env);
if (GetParam() == IsolateDataMode::kPerEnvironmentWithoutPlatform) {
node::FreeIsolateData(instance->isolate_data);
}
instance->context.Reset();
}
Local<Value> Evaluate(Instance* instance, const char* source) {
v8::EscapableHandleScope handle_scope(isolate_);
Local<Context> context = instance->context.Get(isolate_);
Context::Scope context_scope(context);
Local<v8::Script> script =
v8::Script::Compile(
context, v8::String::NewFromUtf8(isolate_, source).ToLocalChecked())
.ToLocalChecked();
return handle_scope.Escape(script->Run(context).ToLocalChecked());
}
bool TryEvaluate(Instance* instance, const char* source) {
HandleScope handle_scope(isolate_);
Local<Context> context = instance->context.Get(isolate_);
Context::Scope context_scope(context);
v8::TryCatch try_catch(isolate_);
Local<v8::Script> script =
v8::Script::Compile(
context, v8::String::NewFromUtf8(isolate_, source).ToLocalChecked())
.ToLocalChecked();
return !script->Run(context).IsEmpty() && !try_catch.HasTerminated();
}
int EvaluateInt(Instance* instance, const char* source) {
HandleScope handle_scope(isolate_);
Local<Context> context = instance->context.Get(isolate_);
return Evaluate(instance, source)->Int32Value(context).FromJust();
}
std::string EvaluateString(Instance* instance, const char* source) {
HandleScope handle_scope(isolate_);
v8::String::Utf8Value utf8(isolate_, Evaluate(instance, source));
return *utf8;
}
// What an embedder's message pump does: a bounded number of loop turns and
// foreground task flushes, never SpinEventLoop() on one Environment.
void PumpLoop(int turns) {
for (int i = 0; i < turns; i++) {
uv_run(¤t_loop, UV_RUN_NOWAIT);
platform->DrainTasks(isolate_);
}
}
// Runs the loop until every instance's interval timer and immediate chain
// have both run again; a callback lost to a stray termination or exception
// breaks its chain and shows up here.
void PumpUntilAllTicked(const std::vector<Instance*>& instances) {
std::vector<int> ticks, immediates;
for (Instance* instance : instances) {
ticks.push_back(EvaluateInt(instance, "state.ticks"));
immediates.push_back(EvaluateInt(instance, "state.immediates"));
}
for (int turn = 0; turn < 10000; turn++) {
uv_run(¤t_loop, UV_RUN_ONCE);
platform->DrainTasks(isolate_);
bool all_progressed = true;
for (size_t i = 0; i < instances.size(); i++) {
if (EvaluateInt(instances[i], "state.ticks") <= ticks[i] ||
EvaluateInt(instances[i], "state.immediates") <= immediates[i]) {
all_progressed = false;
}
}
if (all_progressed) return;
}
FAIL() << "an Environment stopped running its timers or immediates";
}
};
TEST_P(SharedIsolateTest, EnvironmentsComeAndGoWhileSiblingsRun) {
const HandleScope handle_scope(isolate_);
cleanup_hook_runs = 0;
const auto sibling_flags = static_cast<EnvironmentFlags::Flags>(
EnvironmentFlags::kNoCreateInspector |
EnvironmentFlags::kNoBrowserGlobals |
EnvironmentFlags::kNoRegisterESMLoader |
EnvironmentFlags::kNoGlobalSearchPaths);
std::unique_ptr<Instance> owner =
CreateInstance(0, EnvironmentFlags::kDefaultFlags);
std::unique_ptr<Instance> second = CreateInstance(1, sibling_flags);
std::unique_ptr<Instance> third = CreateInstance(2, sibling_flags);
EXPECT_EQ(isolate_->GetCppHeap(), cpp_heap_);
PumpUntilAllTicked({owner.get(), second.get(), third.get()});
// The embedder's own cppgc objects live on the same heap as Node's.
v8::Global<v8::Object> embedder_holder;
{
HandleScope inner(isolate_);
Local<Context> context = second->context.Get(isolate_);
Context::Scope context_scope(context);
Local<v8::Object> holder = v8::FunctionTemplate::New(isolate_)
->InstanceTemplate()
->NewInstance(context)
.ToLocalChecked();
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(
isolate_,
holder,
cppgc::MakeGarbageCollected<EmbedderObject>(
isolate_->GetCppHeap()->GetAllocationHandle()));
embedder_holder.Reset(isolate_, holder);
}
isolate_->LowMemoryNotification();
EXPECT_EQ(EmbedderObject::alive, 1);
#if HAVE_INSPECTOR
EXPECT_EQ(EvaluateInt(
owner.get(),
"(() => {\n"
" const { Session } = process.getBuiltinModule('inspector');\n"
" const session = new Session();\n"
" session.connect();\n"
" let value = -1;\n"
" session.post('Runtime.evaluate', { expression: 'state.id + "
"42' },\n"
" (err, res) => { value = err ? -2 : "
"res.result.value; });\n"
" console.time('session'); console.timeEnd('session');\n"
" session.disconnect();\n"
" return value;\n"
"})()"),
42);
// A sibling without an inspector of its own gets an exception, not an abort.
EXPECT_EQ(
EvaluateString(
second.get(),
"(() => {\n"
" try {\n"
" const { Session } = process.getBuiltinModule('inspector');\n"
" const session = new Session();\n"
" session.connect();\n"
" session.disconnect();\n"
" return 'connected';\n"
" } catch (e) { return String(e.code); }\n"
"})()"),
"ERR_INSPECTOR_NOT_AVAILABLE");
#endif // HAVE_INSPECTOR
if (GetParam() == IsolateDataMode::kShared) {
// Workers need a platform; their exit must not disturb the siblings.
EXPECT_EQ(
EvaluateInt(third.get(),
"(() => {\n"
" const { Worker } ="
" process.getBuiltinModule('worker_threads');\n"
" state.workerExit = -1;\n"
" new Worker('process.exit(7)', { eval: true })\n"
" .on('exit', (code) => { state.workerExit = code; });\n"
" return 0;\n"
"})()"),
0);
// Loop turns do not bound Worker startup time. Yield to the Worker and
// allow it to finish even when the main thread pumps the loop quickly.
const uint64_t deadline = uv_hrtime() + 30ULL * 1000 * 1000 * 1000;
while (uv_hrtime() < deadline) {
PumpLoop(1);
if (EvaluateInt(third.get(), "state.workerExit") == 7) break;
uv_sleep(1);
}
EXPECT_EQ(EvaluateInt(third.get(), "state.workerExit"), 7);
}
PumpUntilAllTicked({owner.get(), second.get(), third.get()});
// Free one Environment while its siblings have timers and immediates due.
const int owner_ticks = EvaluateInt(owner.get(), "state.ticks");
FreeInstance(std::move(second));
EXPECT_EQ(cleanup_hook_runs, 1);
EXPECT_FALSE(owner->exit_handler_called);
EXPECT_FALSE(third->exit_handler_called);
PumpUntilAllTicked({owner.get(), third.get()});
EXPECT_GT(EvaluateInt(owner.get(), "state.ticks"), owner_ticks);
// The embedder's object outlives the Environment whose context wrapped it.
isolate_->LowMemoryNotification();
EXPECT_EQ(EmbedderObject::alive, 1);
embedder_holder.Reset();
// An uncaught exception whose exit handler calls Stop() without
// kDoNotTerminateIsolate, then free: the termination it requests must not
// leak into the sibling's next script.
third->stop_flags = node::StopFlags::kNoFlags;
EXPECT_EQ(EvaluateInt(third.get(),
"process.getBuiltinModule('timers').setImmediate("
"() => { throw new Error('uncaught'); }), 0"),
0);
for (int turn = 0; turn < 100 && !third->exit_handler_called; turn++) {
PumpLoop(1);
}
EXPECT_TRUE(third->exit_handler_called);
EXPECT_EQ(third->exit_code, 1);
FreeInstance(std::move(third));
EXPECT_EQ(cleanup_hook_runs, 2);
EXPECT_TRUE(TryEvaluate(owner.get(), "state.ticks"));
PumpUntilAllTicked({owner.get()});
EXPECT_FALSE(owner->exit_handler_called);
// The same when the exception is thrown by the bootstrap script itself and
// no JavaScript of that Environment runs after its exit handler.
std::unique_ptr<Instance> failed = CreateInstance(4, sibling_flags, true);
EXPECT_EQ(failed->exit_code, 1);
FreeInstance(std::move(failed));
EXPECT_EQ(cleanup_hook_runs, 3);
EXPECT_TRUE(TryEvaluate(owner.get(), "state.ticks"));
PumpUntilAllTicked({owner.get()});
// A new sibling can join after others left, here a second one that owns an
// inspector and debug signal handler of its own.
std::unique_ptr<Instance> late =
CreateInstance(3, EnvironmentFlags::kDefaultFlags);
PumpUntilAllTicked({owner.get(), late.get()});
// beforeExit / exit are per Environment.
{
HandleScope inner(isolate_);
Context::Scope context_scope(late->context.Get(isolate_));
node::EmitProcessBeforeExit(late->env).Check();
}
EXPECT_EQ(EvaluateString(late.get(), "state.events.join()"), "beforeExit");
EXPECT_EQ(EvaluateString(owner.get(), "state.events.join()"), "");
{
HandleScope inner(isolate_);
Context::Scope context_scope(late->context.Get(isolate_));
EXPECT_EQ(node::EmitProcessExit(late->env).FromJust(), 0);
}
EXPECT_EQ(EvaluateString(late.get(), "state.events.join()"),
"beforeExit,exit");
EXPECT_EQ(EvaluateString(owner.get(), "state.events.join()"), "");
node::Stop(late->env, node::StopFlags::kDoNotTerminateIsolate);
PumpUntilAllTicked({owner.get()});
EXPECT_EQ(EvaluateInt(late.get(), "state.events.length"), 2);
FreeInstance(std::move(late));
EXPECT_EQ(cleanup_hook_runs, 4);
PumpUntilAllTicked({owner.get()});
node::Stop(owner->env, node::StopFlags::kDoNotTerminateIsolate);
FreeInstance(std::move(owner));
EXPECT_EQ(cleanup_hook_runs, 5);
// Nothing the Environments left behind keeps the shared loop busy.
PumpLoop(2);
EXPECT_EQ(uv_loop_alive(¤t_loop), 0);
}
TEST_P(SharedIsolateTest, FreeIsolateDataBeforeItsEnvironmentAsserts) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
const HandleScope handle_scope(isolate_);
std::unique_ptr<Instance> instance =
CreateInstance(0, EnvironmentFlags::kNoCreateInspector);
IsolateData* isolate_data = instance->isolate_data;
EXPECT_DEATH_IF_SUPPORTED(node::FreeIsolateData(isolate_data),
"environment_count_");
node::Stop(instance->env, node::StopFlags::kDoNotTerminateIsolate);
FreeInstance(std::move(instance));
}
#if HAVE_OPENSSL
TEST_P(SharedIsolateTest, RootCertStoreIsPerEnvironment) {
const HandleScope handle_scope(isolate_);
std::unique_ptr<Instance> first =
CreateInstance(0, EnvironmentFlags::kNoCreateInspector);
std::unique_ptr<Instance> second =
CreateInstance(1, EnvironmentFlags::kNoCreateInspector);
auto store_size = [](Instance* instance) {
return sk_X509_OBJECT_num(X509_STORE_get0_objects(
node::crypto::GetOrCreateRootCertStore(instance->env)));
};
auto set_default_ca_count = [this](Instance* instance, int count) {
std::string source =
"const tls = process.getBuiltinModule('tls');"
"tls.setDefaultCACertificates(tls.rootCertificates.slice(0, " +
std::to_string(count) + "))";
Evaluate(instance, source.c_str());
};
set_default_ca_count(first.get(), 1);
set_default_ca_count(second.get(), 2);
EXPECT_EQ(store_size(first.get()), 1);
EXPECT_EQ(store_size(second.get()), 2);
FreeInstance(std::move(first));
EXPECT_EQ(store_size(second.get()), 2);
FreeInstance(std::move(second));
}
#endif // HAVE_OPENSSL
#ifndef OPENSSL_NO_QUIC
TEST_P(SharedIsolateTest, QuicAllocatorIsPerEnvironment) {
const HandleScope handle_scope(isolate_);
std::unique_ptr<Instance> first =
CreateInstance(0, EnvironmentFlags::kNoCreateInspector);
std::unique_ptr<Instance> second =
CreateInstance(1, EnvironmentFlags::kNoCreateInspector);
auto allocator = [this](Instance* instance) {
HandleScope inner(isolate_);
Local<Context> context = instance->context.Get(isolate_);
Context::Scope context_scope(context);
Local<Value> name = v8::String::NewFromUtf8Literal(isolate_, "quic");
instance->env->principal_realm()
->internal_binding_loader()
->Call(context, v8::Undefined(isolate_), 1, &name)
.ToLocalChecked();
return node::quic::BindingData::Get(instance->env).ngtcp2_allocator();
};
ngtcp2_mem* first_mem = allocator(first.get());
void* first_ptr = first_mem->malloc(64, first_mem->user_data);
ngtcp2_mem* second_mem = allocator(second.get());
void* tracked = second_mem->malloc(16, second_mem->user_data);
void* untracked = second_mem->malloc(16, second_mem->user_data);
node::quic::BindingData::Get(second->env).StopTrackingMemory(untracked);
EXPECT_NE(first_mem, second_mem);
first_mem->free(first_ptr, first_mem->user_data);
FreeInstance(std::move(second));
second_mem->free(untracked, second_mem->user_data);
tracked = second_mem->realloc(tracked, 32, second_mem->user_data);
second_mem->free(tracked, second_mem->user_data);
FreeInstance(std::move(first));
}
#endif // OPENSSL_NO_QUIC
INSTANTIATE_TEST_SUITE_P(
EnvironmentTest,
SharedIsolateTest,
::testing::Values(IsolateDataMode::kShared,
IsolateDataMode::kPerEnvironmentWithoutPlatform),
[](const ::testing::TestParamInfo<IsolateDataMode>& info) {
return info.param == IsolateDataMode::kShared
? "SharedIsolateData"
: "IsolateDataPerEnvironmentWithoutPlatform";
});