module: bootstrap module loaders in shadow realm

This bootstraps ESM loaders in the ShadowRealm with
`ShadowRealm.prototype.importValue` as its entry point and enables
loading ESM and CJS modules in the ShadowRealm. The module is imported
without a parent URL and resolved with the current process's working
directory.

PR-URL: https://github.com/nodejs/node/pull/48655
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Chengzhong Wu 2023-11-10 00:21:15 +08:00 committed by legendecas
parent 6b7197cb2b
commit fc2862b7f5
No known key found for this signature in database
GPG Key ID: CB3C9EC2BC27057C
24 changed files with 445 additions and 122 deletions

View File

@ -50,6 +50,8 @@
const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
@ -215,8 +217,8 @@ const internalBuiltinIds = builtinIds
.filter((id) => StringPrototypeStartsWith(id, 'internal/') && id !== selfId);
// When --expose-internals is on we'll add the internal builtin ids to these.
const canBeRequiredByUsersList = new SafeSet(publicBuiltinIds);
const canBeRequiredByUsersWithoutSchemeList =
let canBeRequiredByUsersList = new SafeSet(publicBuiltinIds);
let canBeRequiredByUsersWithoutSchemeList =
new SafeSet(publicBuiltinIds.filter((id) => !schemelessBlockList.has(id)));
/**
@ -269,6 +271,13 @@ class BuiltinModule {
}
}
static setRealmAllowRequireByUsers(ids) {
canBeRequiredByUsersList =
new SafeSet(ArrayPrototypeFilter(ids, (id) => ArrayPrototypeIncludes(publicBuiltinIds, id)));
canBeRequiredByUsersWithoutSchemeList =
new SafeSet(ArrayPrototypeFilter(ids, (id) => !schemelessBlockList.has(id)));
}
// To be called during pre-execution when --expose-internals is on.
// Enables the user-land module loader to access internal modules.
static exposeInternals() {

View File

@ -0,0 +1,21 @@
'use strict';
// This script sets up the context for shadow realms.
const {
prepareShadowRealmExecution,
} = require('internal/process/pre_execution');
const {
BuiltinModule,
} = require('internal/bootstrap/realm');
BuiltinModule.setRealmAllowRequireByUsers([
/**
* The built-in modules exposed in the ShadowRealm must each be providing
* platform capabilities with no authority to cause side effects such as
* I/O or mutation of values that are shared across different realms within
* the same Node.js environment.
*/
]);
prepareShadowRealmExecution();

View File

@ -136,6 +136,7 @@ port.on('message', (message) => {
const isLoaderWorker =
doEval === 'internal' &&
filename === require('internal/modules/esm/utils').loaderWorkerId;
// Disable custom loaders in loader worker.
setupUserModules(isLoaderWorker);
if (!hasStdin)

View File

@ -528,9 +528,10 @@ let emittedLoaderFlagWarning = false;
*/
function createModuleLoader() {
let customizations = null;
// Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
// doing so would cause an infinite loop.
if (!require('internal/modules/esm/utils').isLoaderWorker()) {
// Don't spawn a new worker if custom loaders are disabled. For instance, if
// we're already in a worker thread created by instantiating
// CustomizedModuleLoader; doing so would cause an infinite loop.
if (!require('internal/modules/esm/utils').forceDefaultLoader()) {
const userLoaderPaths = getOptionValue('--experimental-loader');
if (userLoaderPaths.length > 0) {
if (!emittedLoaderFlagWarning) {

View File

@ -4,6 +4,7 @@ const {
ArrayIsArray,
SafeSet,
SafeWeakMap,
Symbol,
ObjectFreeze,
} = primordials;
@ -157,6 +158,26 @@ function registerModule(referrer, registry) {
moduleRegistries.set(idSymbol, registry);
}
/**
* Registers the ModuleRegistry for dynamic import() calls with a realm
* as the referrer. Similar to {@link registerModule}, but this function
* generates a new id symbol instead of using the one from the referrer
* object.
* @param {globalThis} globalThis The globalThis object of the realm.
* @param {ModuleRegistry} registry
*/
function registerRealm(globalThis, registry) {
let idSymbol = globalThis[host_defined_option_symbol];
// If the per-realm host-defined options is already registered, do nothing.
if (idSymbol) {
return;
}
// Otherwise, register the per-realm host-defined options.
idSymbol = Symbol('Realm globalThis');
globalThis[host_defined_option_symbol] = idSymbol;
moduleRegistries.set(idSymbol, registry);
}
/**
* Defines the `import.meta` object for a given module.
* @param {symbol} symbol - Reference to the module.
@ -192,28 +213,29 @@ async function importModuleDynamicallyCallback(referrerSymbol, specifier, attrib
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
}
let _isLoaderWorker = false;
let _forceDefaultLoader = false;
/**
* Initializes handling of ES modules.
* This is configured during pre-execution. Specifically it's set to true for
* the loader worker in internal/main/worker_thread.js.
* @param {boolean} [isLoaderWorker=false] - A boolean indicating whether the loader is a worker or not.
* @param {boolean} [forceDefaultLoader=false] - A boolean indicating disabling custom loaders.
*/
function initializeESM(isLoaderWorker = false) {
_isLoaderWorker = isLoaderWorker;
function initializeESM(forceDefaultLoader = false) {
_forceDefaultLoader = forceDefaultLoader;
initializeDefaultConditions();
// Setup per-isolate callbacks that locate data or callbacks that we keep
// Setup per-realm callbacks that locate data or callbacks that we keep
// track of for different ESM modules.
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
}
/**
* Determine whether the current process is a loader worker.
* @returns {boolean} Whether the current process is a loader worker.
* Determine whether custom loaders are disabled and it is forced to use the
* default loader.
* @returns {boolean}
*/
function isLoaderWorker() {
return _isLoaderWorker;
function forceDefaultLoader() {
return _forceDefaultLoader;
}
/**
@ -251,10 +273,11 @@ async function initializeHooks() {
module.exports = {
registerModule,
registerRealm,
initializeESM,
initializeHooks,
getDefaultConditions,
getConditionsSet,
loaderWorkerId: 'internal/modules/esm/worker',
isLoaderWorker,
forceDefaultLoader,
};

View File

@ -67,6 +67,26 @@ function prepareWorkerThreadExecution() {
});
}
function prepareShadowRealmExecution() {
const { registerRealm } = require('internal/modules/esm/utils');
// Patch the process object with legacy properties and normalizations.
// Do not expand argv1 as it is not available in ShadowRealm.
patchProcessObject(false);
setupDebugEnv();
// Disable custom loaders in ShadowRealm.
setupUserModules(true);
registerRealm(globalThis, {
__proto__: null,
importModuleDynamically: (specifier, _referrer, attributes) => {
// The handler for `ShadowRealm.prototype.importValue`.
const { esmLoader } = require('internal/process/esm_loader');
// `parentURL` is not set in the case of a ShadowRealm top-level import.
return esmLoader.import(specifier, undefined, attributes);
},
});
}
function prepareExecution(options) {
const { expandArgv1, initializeModules, isMainThread } = options;
@ -161,16 +181,17 @@ function setupSymbolDisposePolyfill() {
}
}
function setupUserModules(isLoaderWorker = false) {
function setupUserModules(forceDefaultLoader = false) {
initializeCJSLoader();
initializeESMLoader(isLoaderWorker);
initializeESMLoader(forceDefaultLoader);
const CJSLoader = require('internal/modules/cjs/loader');
assert(!CJSLoader.hasLoadedAnyUserCJSModule);
// Loader workers are responsible for doing this themselves.
if (isLoaderWorker) {
return;
}
// Do not enable preload modules if custom loaders are disabled.
// For example, loader workers are responsible for doing this themselves.
// And preload modules are not supported in ShadowRealm as well.
if (!forceDefaultLoader) {
loadPreloadModules();
}
// Need to be done after --require setup.
initializeFrozenIntrinsics();
}
@ -701,9 +722,9 @@ function initializeCJSLoader() {
initializeCJS();
}
function initializeESMLoader(isLoaderWorker) {
function initializeESMLoader(forceDefaultLoader) {
const { initializeESM } = require('internal/modules/esm/utils');
initializeESM(isLoaderWorker);
initializeESM(forceDefaultLoader);
// Patch the vm module when --experimental-vm-modules is on.
// Please update the comments in vm.js when this block changes.
@ -779,6 +800,7 @@ module.exports = {
setupUserModules,
prepareMainThreadExecution,
prepareWorkerThreadExecution,
prepareShadowRealmExecution,
markBootstrapComplete,
loadPreloadModules,
initializeFrozenIntrinsics,

View File

@ -372,8 +372,9 @@ void AsyncWrap::CreatePerContextProperties(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
Realm* realm = Realm::GetCurrent(context);
Environment* env = realm->env();
Isolate* isolate = realm->isolate();
HandleScope scope(isolate);
PropertyAttribute ReadOnlyDontDelete =
@ -446,13 +447,16 @@ void AsyncWrap::CreatePerContextProperties(Local<Object> target,
#undef FORCE_SET_TARGET_FIELD
env->set_async_hooks_init_function(Local<Function>());
env->set_async_hooks_before_function(Local<Function>());
env->set_async_hooks_after_function(Local<Function>());
env->set_async_hooks_destroy_function(Local<Function>());
env->set_async_hooks_promise_resolve_function(Local<Function>());
env->set_async_hooks_callback_trampoline(Local<Function>());
env->set_async_hooks_binding(target);
// TODO(legendecas): async hook functions are not realm-aware yet.
// This simply avoid overriding principal realm's functions when a
// ShadowRealm initializes the binding.
realm->set_async_hooks_init_function(Local<Function>());
realm->set_async_hooks_before_function(Local<Function>());
realm->set_async_hooks_after_function(Local<Function>());
realm->set_async_hooks_destroy_function(Local<Function>());
realm->set_async_hooks_promise_resolve_function(Local<Function>());
realm->set_async_hooks_callback_trampoline(Local<Function>());
realm->set_async_hooks_binding(target);
}
void AsyncWrap::RegisterExternalReferences(

View File

@ -1651,10 +1651,13 @@ void AsyncHooks::MemoryInfo(MemoryTracker* tracker) const {
void AsyncHooks::grow_async_ids_stack() {
async_ids_stack_.reserve(async_ids_stack_.Length() * 3);
env()->async_hooks_binding()->Set(
env()->context(),
env()
->principal_realm()
->async_hooks_binding()
->Set(env()->context(),
env()->async_ids_stack_string(),
async_ids_stack_.GetJSArray()).Check();
async_ids_stack_.GetJSArray())
.Check();
}
void AsyncHooks::FailWithCorruptedAsyncStack(double expected_async_id) {

View File

@ -39,6 +39,7 @@ using v8::MicrotaskQueue;
using v8::Module;
using v8::ModuleRequest;
using v8::Object;
using v8::ObjectTemplate;
using v8::PrimitiveArray;
using v8::Promise;
using v8::ScriptCompiler;
@ -49,15 +50,17 @@ using v8::UnboundModuleScript;
using v8::Undefined;
using v8::Value;
ModuleWrap::ModuleWrap(Environment* env,
ModuleWrap::ModuleWrap(Realm* realm,
Local<Object> object,
Local<Module> module,
Local<String> url,
Local<Object> context_object,
Local<Value> synthetic_evaluation_step)
: BaseObject(env, object),
module_(env->isolate(), module),
: BaseObject(realm, object),
module_(realm->isolate(), module),
module_hash_(module->GetIdentityHash()) {
realm->env()->hash_to_module_map.emplace(module_hash_, this);
object->SetInternalField(kModuleSlot, module);
object->SetInternalField(kURLSlot, url);
object->SetInternalField(kSyntheticEvaluationStepsSlot,
@ -72,7 +75,6 @@ ModuleWrap::ModuleWrap(Environment* env,
}
ModuleWrap::~ModuleWrap() {
HandleScope scope(env()->isolate());
auto range = env()->hash_to_module_map.equal_range(module_hash_);
for (auto it = range.first; it != range.second; ++it) {
if (it->second == this) {
@ -107,8 +109,8 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
CHECK_GE(args.Length(), 3);
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = realm->isolate();
Local<Object> that = args.This();
@ -122,7 +124,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
} else {
CHECK(args[1]->IsObject());
contextify_context = ContextifyContext::ContextFromContextifiedSandbox(
env, args[1].As<Object>());
realm->env(), args[1].As<Object>());
CHECK_NOT_NULL(contextify_context);
context = contextify_context->context();
}
@ -148,8 +150,8 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
Local<Symbol> id_symbol = Symbol::New(isolate, url);
host_defined_options->Set(isolate, HostDefinedOptions::kID, id_symbol);
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
TryCatchScope try_catch(env);
ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
TryCatchScope try_catch(realm->env());
Local<Module> module;
@ -206,7 +208,9 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(),
AppendExceptionLine(realm->env(),
try_catch.Exception(),
try_catch.Message(),
ErrorHandlingMode::MODULE_ERROR);
try_catch.ReThrow();
}
@ -215,18 +219,21 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
if (options == ScriptCompiler::kConsumeCodeCache &&
source.GetCachedData()->rejected) {
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
env, "cachedData buffer was rejected");
realm, "cachedData buffer was rejected");
try_catch.ReThrow();
return;
}
}
}
if (!that->Set(context, env->url_string(), url).FromMaybe(false)) {
if (!that->Set(context, realm->isolate_data()->url_string(), url)
.FromMaybe(false)) {
return;
}
if (that->SetPrivate(context, env->host_defined_option_symbol(), id_symbol)
if (that->SetPrivate(context,
realm->isolate_data()->host_defined_option_symbol(),
id_symbol)
.IsNothing()) {
return;
}
@ -236,28 +243,26 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
// be stored in an internal field.
Local<Object> context_object = context->GetExtrasBindingObject();
Local<Value> synthetic_evaluation_step =
synthetic ? args[3] : Undefined(env->isolate()).As<v8::Value>();
synthetic ? args[3] : Undefined(realm->isolate()).As<v8::Value>();
ModuleWrap* obj = new ModuleWrap(
env, that, module, url, context_object, synthetic_evaluation_step);
realm, that, module, url, context_object, synthetic_evaluation_step);
obj->contextify_context_ = contextify_context;
env->hash_to_module_map.emplace(module->GetIdentityHash(), obj);
that->SetIntegrityLevel(context, IntegrityLevel::kFrozen);
args.GetReturnValue().Set(that);
}
static Local<Object> createImportAttributesContainer(
Environment* env, Isolate* isolate, Local<FixedArray> raw_attributes) {
Realm* realm, Isolate* isolate, Local<FixedArray> raw_attributes) {
Local<Object> attributes =
Object::New(isolate, v8::Null(env->isolate()), nullptr, nullptr, 0);
Object::New(isolate, v8::Null(isolate), nullptr, nullptr, 0);
for (int i = 0; i < raw_attributes->Length(); i += 3) {
attributes
->Set(env->context(),
raw_attributes->Get(env->context(), i).As<String>(),
raw_attributes->Get(env->context(), i + 1).As<Value>())
->Set(realm->context(),
raw_attributes->Get(realm->context(), i).As<String>(),
raw_attributes->Get(realm->context(), i + 1).As<Value>())
.ToChecked();
}
@ -265,7 +270,7 @@ static Local<Object> createImportAttributesContainer(
}
void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
CHECK_EQ(args.Length(), 1);
@ -292,14 +297,14 @@ void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
// call the dependency resolve callbacks
for (int i = 0; i < module_requests_length; i++) {
Local<ModuleRequest> module_request =
module_requests->Get(env->context(), i).As<ModuleRequest>();
module_requests->Get(realm->context(), i).As<ModuleRequest>();
Local<String> specifier = module_request->GetSpecifier();
Utf8Value specifier_utf8(env->isolate(), specifier);
Utf8Value specifier_utf8(realm->isolate(), specifier);
std::string specifier_std(*specifier_utf8, specifier_utf8.length());
Local<FixedArray> raw_attributes = module_request->GetImportAssertions();
Local<Object> attributes =
createImportAttributesContainer(env, isolate, raw_attributes);
createImportAttributesContainer(realm, isolate, raw_attributes);
Local<Value> argv[] = {
specifier,
@ -315,11 +320,11 @@ void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
maybe_resolve_return_value.ToLocalChecked();
if (!resolve_return_value->IsPromise()) {
THROW_ERR_VM_MODULE_LINK_FAILURE(
env, "request for '%s' did not return promise", specifier_std);
realm, "request for '%s' did not return promise", specifier_std);
return;
}
Local<Promise> resolve_promise = resolve_return_value.As<Promise>();
obj->resolve_cache_[specifier_std].Reset(env->isolate(), resolve_promise);
obj->resolve_cache_[specifier_std].Reset(isolate, resolve_promise);
promises[i] = resolve_promise;
}
@ -329,13 +334,13 @@ void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
}
void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Context> context = obj->context();
Local<Module> module = obj->module_.Get(isolate);
TryCatchScope try_catch(env);
TryCatchScope try_catch(realm->env());
USE(module->InstantiateModule(context, ResolveModuleCallback));
// clear resolve cache on instantiate
@ -344,7 +349,9 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(),
AppendExceptionLine(realm->env(),
try_catch.Exception(),
try_catch.Message(),
ErrorHandlingMode::MODULE_ERROR);
try_catch.ReThrow();
return;
@ -352,8 +359,8 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) {
}
void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = realm->isolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Context> context = obj->context();
@ -368,14 +375,14 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 2);
CHECK(args[0]->IsNumber());
int64_t timeout = args[0]->IntegerValue(env->context()).FromJust();
int64_t timeout = args[0]->IntegerValue(realm->context()).FromJust();
CHECK(args[1]->IsBoolean());
bool break_on_sigint = args[1]->IsTrue();
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
TryCatchScope try_catch(env);
Isolate::SafeForTerminationScope safe_for_termination(env->isolate());
ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
TryCatchScope try_catch(realm->env());
Isolate::SafeForTerminationScope safe_for_termination(isolate);
bool timed_out = false;
bool received_signal = false;
@ -406,16 +413,15 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
// Convert the termination exception into a regular exception.
if (timed_out || received_signal) {
if (!env->is_main_thread() && env->is_stopping())
return;
env->isolate()->CancelTerminateExecution();
if (!realm->env()->is_main_thread() && realm->env()->is_stopping()) return;
isolate->CancelTerminateExecution();
// It is possible that execution was terminated by another timeout in
// which this timeout is nested, so check whether one of the watchdogs
// from this invocation is responsible for termination.
if (timed_out) {
THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(env, timeout);
THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(realm->env(), timeout);
} else if (received_signal) {
THROW_ERR_SCRIPT_EXECUTION_INTERRUPTED(env);
THROW_ERR_SCRIPT_EXECUTION_INTERRUPTED(realm->env());
}
}
@ -429,7 +435,7 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
}
void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
@ -439,7 +445,7 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
switch (module->GetStatus()) {
case v8::Module::Status::kUninstantiated:
case v8::Module::Status::kInstantiating:
return env->ThrowError(
return realm->env()->ThrowError(
"cannot get namespace, module has not been instantiated");
case v8::Module::Status::kInstantiated:
case v8::Module::Status::kEvaluating:
@ -466,11 +472,11 @@ void ModuleWrap::GetStatus(const FunctionCallbackInfo<Value>& args) {
void ModuleWrap::GetStaticDependencySpecifiers(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
Local<Module> module = obj->module_.Get(env->isolate());
Local<Module> module = obj->module_.Get(realm->isolate());
Local<FixedArray> module_requests = module->GetModuleRequests();
int count = module_requests->Length();
@ -479,12 +485,12 @@ void ModuleWrap::GetStaticDependencySpecifiers(
for (int i = 0; i < count; i++) {
Local<ModuleRequest> module_request =
module_requests->Get(env->context(), i).As<ModuleRequest>();
module_requests->Get(realm->context(), i).As<ModuleRequest>();
specifiers[i] = module_request->GetSpecifier();
}
args.GetReturnValue().Set(
Array::New(env->isolate(), specifiers.out(), count));
Array::New(realm->isolate(), specifiers.out(), count));
}
void ModuleWrap::GetError(const FunctionCallbackInfo<Value>& args) {
@ -501,15 +507,13 @@ MaybeLocal<Module> ModuleWrap::ResolveModuleCallback(
Local<String> specifier,
Local<FixedArray> import_attributes,
Local<Module> referrer) {
Isolate* isolate = context->GetIsolate();
Environment* env = Environment::GetCurrent(context);
if (env == nullptr) {
Isolate* isolate = context->GetIsolate();
THROW_ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE(isolate);
return MaybeLocal<Module>();
}
Isolate* isolate = env->isolate();
Utf8Value specifier_utf8(isolate, specifier);
std::string specifier_std(*specifier_utf8, specifier_utf8.length());
@ -559,11 +563,16 @@ static MaybeLocal<Promise> ImportModuleDynamically(
THROW_ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE(isolate);
return MaybeLocal<Promise>();
}
Realm* realm = Realm::GetCurrent(context);
if (realm == nullptr) {
// Fallback to the principal realm if it's in a vm context.
realm = env->principal_realm();
}
EscapableHandleScope handle_scope(isolate);
Local<Function> import_callback =
env->host_import_module_dynamically_callback();
realm->host_import_module_dynamically_callback();
Local<Value> id;
Local<FixedArray> options = host_defined_options.As<FixedArray>();
@ -579,7 +588,7 @@ static MaybeLocal<Promise> ImportModuleDynamically(
}
Local<Object> attributes =
createImportAttributesContainer(env, isolate, import_attributes);
createImportAttributesContainer(realm, isolate, import_attributes);
Local<Value> import_args[] = {
id,
@ -603,13 +612,13 @@ static MaybeLocal<Promise> ImportModuleDynamically(
void ModuleWrap::SetImportModuleDynamicallyCallback(
const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
HandleScope handle_scope(isolate);
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsFunction());
Local<Function> import_callback = args[0].As<Function>();
env->set_host_import_module_dynamically_callback(import_callback);
realm->set_host_import_module_dynamically_callback(import_callback);
isolate->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
}
@ -624,10 +633,15 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback(
if (module_wrap == nullptr) {
return;
}
Realm* realm = Realm::GetCurrent(context);
if (realm == nullptr) {
// Fallback to the principal realm if it's in a vm context.
realm = env->principal_realm();
}
Local<Object> wrap = module_wrap->object();
Local<Function> callback =
env->host_initialize_import_meta_object_callback();
realm->host_initialize_import_meta_object_callback();
Local<Value> id;
if (!wrap->GetPrivate(context, env->host_defined_option_symbol())
.ToLocal(&id)) {
@ -637,7 +651,7 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback(
Local<Value> args[] = {id, meta};
TryCatchScope try_catch(env);
USE(callback->Call(
context, Undefined(env->isolate()), arraysize(args), args));
context, Undefined(realm->isolate()), arraysize(args), args));
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
try_catch.ReThrow();
}
@ -645,13 +659,13 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback(
void ModuleWrap::SetInitializeImportMetaObjectCallback(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Realm* realm = Realm::GetCurrent(args);
Isolate* isolate = realm->isolate();
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsFunction());
Local<Function> import_meta_callback = args[0].As<Function>();
env->set_host_initialize_import_meta_object_callback(import_meta_callback);
realm->set_host_initialize_import_meta_object_callback(import_meta_callback);
isolate->SetHostInitializeImportMetaObjectCallback(
HostInitializeImportMetaObjectCallback);
@ -742,12 +756,9 @@ void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) {
}
}
void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Local<FunctionTemplate> tpl = NewFunctionTemplate(isolate, New);
tpl->InstanceTemplate()->SetInternalFieldCount(
@ -767,21 +778,29 @@ void ModuleWrap::Initialize(Local<Object> target,
"getStaticDependencySpecifiers",
GetStaticDependencySpecifiers);
SetConstructorFunction(context, target, "ModuleWrap", tpl);
SetConstructorFunction(isolate, target, "ModuleWrap", tpl);
SetMethod(context,
SetMethod(isolate,
target,
"setImportModuleDynamicallyCallback",
SetImportModuleDynamicallyCallback);
SetMethod(context,
SetMethod(isolate,
target,
"setInitializeImportMetaObjectCallback",
SetInitializeImportMetaObjectCallback);
}
void ModuleWrap::CreatePerContextProperties(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Realm* realm = Realm::GetCurrent(context);
Isolate* isolate = realm->isolate();
#define V(name) \
target->Set(context, \
FIXED_ONE_BYTE_STRING(env->isolate(), #name), \
Integer::New(env->isolate(), Module::Status::name)) \
target \
->Set(context, \
FIXED_ONE_BYTE_STRING(isolate, #name), \
Integer::New(isolate, Module::Status::name)) \
.FromJust()
V(kUninstantiated);
V(kInstantiating);
@ -812,7 +831,9 @@ void ModuleWrap::RegisterExternalReferences(
} // namespace loader
} // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(module_wrap,
node::loader::ModuleWrap::Initialize)
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
module_wrap, node::loader::ModuleWrap::CreatePerContextProperties)
NODE_BINDING_PER_ISOLATE_INIT(
module_wrap, node::loader::ModuleWrap::CreatePerIsolateProperties)
NODE_BINDING_EXTERNAL_REFERENCE(
module_wrap, node::loader::ModuleWrap::RegisterExternalReferences)

View File

@ -10,6 +10,7 @@
namespace node {
class IsolateData;
class Environment;
class ExternalReferenceRegistry;
@ -40,7 +41,9 @@ class ModuleWrap : public BaseObject {
kInternalFieldCount
};
static void Initialize(v8::Local<v8::Object> target,
static void CreatePerIsolateProperties(IsolateData* isolate_data,
v8::Local<v8::ObjectTemplate> target);
static void CreatePerContextProperties(v8::Local<v8::Object> target,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv);
@ -66,7 +69,7 @@ class ModuleWrap : public BaseObject {
}
private:
ModuleWrap(Environment* env,
ModuleWrap(Realm* realm,
v8::Local<v8::Object> object,
v8::Local<v8::Module> module,
v8::Local<v8::String> url,

View File

@ -40,6 +40,7 @@ static_assert(static_cast<int>(NM_F_LINKED) ==
V(fs_dir) \
V(messaging) \
V(mksnapshot) \
V(module_wrap) \
V(performance) \
V(process_methods) \
V(timers) \

View File

@ -123,6 +123,10 @@ void AppendExceptionLine(Environment* env,
inline void THROW_##code( \
Environment* env, const char* format, Args&&... args) { \
THROW_##code(env->isolate(), format, std::forward<Args>(args)...); \
} \
template <typename... Args> \
inline void THROW_##code(Realm* realm, const char* format, Args&&... args) { \
THROW_##code(realm->isolate(), format, std::forward<Args>(args)...); \
}
ERRORS_WITH_CODE(V)
#undef V

View File

@ -1,6 +1,7 @@
#include "node_shadow_realm.h"
#include "env-inl.h"
#include "node_errors.h"
#include "node_process.h"
namespace node {
namespace shadow_realm {
@ -9,6 +10,8 @@ using v8::EscapableHandleScope;
using v8::HandleScope;
using v8::Local;
using v8::MaybeLocal;
using v8::Object;
using v8::String;
using v8::Value;
using TryCatchScope = node::errors::TryCatchScope;
@ -16,6 +19,11 @@ using TryCatchScope = node::errors::TryCatchScope;
// static
ShadowRealm* ShadowRealm::New(Environment* env) {
ShadowRealm* realm = new ShadowRealm(env);
// TODO(legendecas): required by node::PromiseRejectCallback.
// Remove this once promise rejection doesn't need to be handled across
// realms.
realm->context()->SetSecurityToken(
env->principal_realm()->context()->GetSecurityToken());
// We do not expect the realm bootstrapping to throw any
// exceptions. If it does, exit the current Node.js instance.
@ -32,6 +40,10 @@ MaybeLocal<Context> HostCreateShadowRealmContextCallback(
Local<Context> initiator_context) {
Environment* env = Environment::GetCurrent(initiator_context);
EscapableHandleScope scope(env->isolate());
// We do not expect the realm bootstrapping to throw any
// exceptions. If it does, exit the current Node.js instance.
TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
ShadowRealm* realm = ShadowRealm::New(env);
if (realm != nullptr) {
return scope.Escape(realm->context());
@ -137,6 +149,28 @@ v8::MaybeLocal<v8::Value> ShadowRealm::BootstrapRealm() {
}
}
// The process object is not exposed globally in ShadowRealm yet.
// However, the process properties need to be setup for built-in modules.
// Specifically, process.cwd() is needed by the ESM loader.
if (ExecuteBootstrapper(
"internal/bootstrap/switches/does_not_own_process_state")
.IsEmpty()) {
return MaybeLocal<Value>();
}
// Setup process.env proxy.
Local<String> env_string = FIXED_ONE_BYTE_STRING(isolate_, "env");
Local<Object> env_proxy;
if (!isolate_data()->env_proxy_template()->NewInstance(context()).ToLocal(
&env_proxy) ||
process_object()->Set(context(), env_string, env_proxy).IsNothing()) {
return MaybeLocal<Value>();
}
if (ExecuteBootstrapper("internal/bootstrap/shadow_realm").IsEmpty()) {
return MaybeLocal<Value>();
}
return v8::True(isolate_);
}

View File

@ -0,0 +1,15 @@
// This fixture is used to test that custom loaders are not enabled in the ShadowRealm.
'use strict';
const assert = require('assert');
async function workInChildProcess() {
// Assert that the process is running with a custom loader.
const moduleNamespace = await import('file:///42.mjs');
assert.strictEqual(moduleNamespace.default, 42);
const realm = new ShadowRealm();
await assert.rejects(realm.importValue('file:///42.mjs', 'default'), TypeError);
}
workInChildProcess();

View File

@ -0,0 +1,9 @@
// This fixture is used to test that --require preload modules are not enabled in the ShadowRealm.
'use strict';
const assert = require('assert');
assert.strictEqual(globalThis.preload, 42);
const realm = new ShadowRealm();
const value = realm.evaluate(`globalThis.preload`);
assert.strictEqual(value, undefined);

View File

@ -0,0 +1 @@
globalThis.preload = 42;

View File

@ -0,0 +1,3 @@
// This module verifies that the module specifier is resolved relative to the
// current module and not the current working directory in the ShadowRealm.
export { getCounter } from "./state-counter.mjs";

View File

@ -0,0 +1,4 @@
let counter = 0;
export const getCounter = () => {
return counter++;
};

View File

@ -0,0 +1,21 @@
// Flags: --experimental-shadow-realm
'use strict';
const common = require('../common');
const assert = require('assert');
async function main() {
// Verifies that builtin modules can not be imported in the ShadowRealm.
const realm = new ShadowRealm();
// The error object created inside the ShadowRealm with the error code
// property is not copied on the realm boundary. Only the error message
// is copied. Simply check the error message here.
await assert.rejects(realm.importValue('fs', 'readFileSync'), {
message: /Cannot find package 'fs'/,
});
// As above, we can only validate the error message, not the error code.
await assert.rejects(realm.importValue('node:fs', 'readFileSync'), {
message: /No such built-in module: node:fs/,
});
}
main().then(common.mustCall());

View File

@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const commonArgs = [
'--experimental-shadow-realm',
'--no-warnings',
];
async function main() {
// Verifies that custom loaders are not enabled in the ShadowRealm.
const child = await common.spawnPromisified(process.execPath, [
...commonArgs,
'--experimental-loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--experimental-loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
fixtures.path('es-module-shadow-realm', 'custom-loaders.js'),
]);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
}
main().then(common.mustCall());

View File

@ -0,0 +1,20 @@
// Flags: --experimental-shadow-realm --max-old-space-size=20
'use strict';
/**
* Verifying modules imported by ShadowRealm instances can be correctly
* garbage collected.
*/
const common = require('../common');
const fixtures = require('../common/fixtures');
async function main() {
const mod = fixtures.fileURL('es-module-shadow-realm', 'state-counter.mjs');
for (let i = 0; i < 100; i++) {
const realm = new ShadowRealm();
await realm.importValue(mod, 'getCounter');
}
}
main().then(common.mustCall());

View File

@ -0,0 +1,28 @@
// Flags: --experimental-shadow-realm
'use strict';
const common = require('../common');
const assert = require('assert');
const path = require('path');
common.skipIfWorker('process.chdir is not supported in workers.');
async function main() {
const realm = new ShadowRealm();
const dirname = __dirname;
// Set process cwd to the parent directory of __dirname.
const cwd = path.dirname(dirname);
process.chdir(cwd);
// Hardcode the relative path to ensure the string is still a valid relative
// URL string.
const relativePath = './fixtures/es-module-shadow-realm/re-export-state-counter.mjs';
// Make sure that the module can not be resolved relative to __filename.
assert.throws(() => require.resolve(relativePath), { code: 'MODULE_NOT_FOUND' });
// Resolve relative to the current working directory.
const getCounter = await realm.importValue(relativePath, 'getCounter');
assert.strictEqual(typeof getCounter, 'function');
}
main().then(common.mustCall());

View File

@ -0,0 +1,29 @@
// Flags: --experimental-shadow-realm
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
async function main() {
const realm = new ShadowRealm();
const mod = fixtures.fileURL('es-module-shadow-realm', 'state-counter.mjs');
const getCounter = await realm.importValue(mod, 'getCounter');
assert.strictEqual(getCounter(), 0);
const getCounter1 = await realm.importValue(mod, 'getCounter');
// Returned value is a newly wrapped function.
assert.notStrictEqual(getCounter, getCounter1);
// Verify that the module state is shared between two `importValue` calls.
assert.strictEqual(getCounter1(), 1);
assert.strictEqual(getCounter(), 2);
const { getCounter: getCounterThisRealm } = await import(mod);
assert.notStrictEqual(getCounterThisRealm, getCounter);
// Verify that the module state is not shared between two realms.
assert.strictEqual(getCounterThisRealm(), 0);
assert.strictEqual(getCounter(), 3);
// Verify that shadow realm rejects to import a non-existing module.
await assert.rejects(realm.importValue('non-exists', 'exports'), TypeError);
}
main().then(common.mustCall());

View File

@ -0,0 +1,20 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
const commonArgs = [
'--experimental-shadow-realm',
];
async function main() {
// Verifies that --require preload modules are not enabled in the ShadowRealm.
spawnSyncAndExitWithoutError(process.execPath, [
...commonArgs,
'--require',
fixtures.path('es-module-shadow-realm', 'preload.js'),
fixtures.path('es-module-shadow-realm', 'preload-main.js'),
]);
}
main().then(common.mustCall());