From fc2862b7f55bbc0e54092423e5272c213df0ed32 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Fri, 10 Nov 2023 00:21:15 +0800 Subject: [PATCH] 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 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Joyee Cheung --- lib/internal/bootstrap/realm.js | 13 +- lib/internal/bootstrap/shadow_realm.js | 21 ++ lib/internal/main/worker_thread.js | 1 + lib/internal/modules/esm/loader.js | 7 +- lib/internal/modules/esm/utils.js | 43 +++- lib/internal/process/pre_execution.js | 38 +++- src/async_wrap.cc | 22 ++- src/env.cc | 11 +- src/module_wrap.cc | 183 ++++++++++-------- src/module_wrap.h | 13 +- src/node_binding.h | 1 + src/node_errors.h | 4 + src/node_shadow_realm.cc | 34 ++++ .../es-module-shadow-realm/custom-loaders.js | 15 ++ .../es-module-shadow-realm/preload-main.js | 9 + .../es-module-shadow-realm/preload.js | 1 + .../re-export-state-counter.mjs | 3 + .../es-module-shadow-realm/state-counter.mjs | 4 + ...st-shadow-realm-allowed-builtin-modules.js | 21 ++ .../test-shadow-realm-custom-loaders.js | 26 +++ test/parallel/test-shadow-realm-gc-module.js | 20 ++ .../test-shadow-realm-import-value-resolve.js | 28 +++ test/parallel/test-shadow-realm-module.js | 29 +++ .../test-shadow-realm-preload-module.js | 20 ++ 24 files changed, 445 insertions(+), 122 deletions(-) create mode 100644 lib/internal/bootstrap/shadow_realm.js create mode 100644 test/fixtures/es-module-shadow-realm/custom-loaders.js create mode 100644 test/fixtures/es-module-shadow-realm/preload-main.js create mode 100644 test/fixtures/es-module-shadow-realm/preload.js create mode 100644 test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs create mode 100644 test/fixtures/es-module-shadow-realm/state-counter.mjs create mode 100644 test/parallel/test-shadow-realm-allowed-builtin-modules.js create mode 100644 test/parallel/test-shadow-realm-custom-loaders.js create mode 100644 test/parallel/test-shadow-realm-gc-module.js create mode 100644 test/parallel/test-shadow-realm-import-value-resolve.js create mode 100644 test/parallel/test-shadow-realm-module.js create mode 100644 test/parallel/test-shadow-realm-preload-module.js diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index c6935c6f7775fc..6034af9a36003c 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -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() { diff --git a/lib/internal/bootstrap/shadow_realm.js b/lib/internal/bootstrap/shadow_realm.js new file mode 100644 index 00000000000000..b99502355d9818 --- /dev/null +++ b/lib/internal/bootstrap/shadow_realm.js @@ -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(); diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 0dda4c760e4acb..c14091ffe09ca7 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -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) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index d1ce6f1df1bceb..0694c7ef2b902d 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -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) { diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 3fe1daabe98f1d..df15169f68faa1 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -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, }; diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index c535f8e8ed8a5d..1cf204a3f7d3e6 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -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(); } - 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, diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 42cddc52aed285..8b784cddf41603 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -372,8 +372,9 @@ void AsyncWrap::CreatePerContextProperties(Local target, Local unused, Local 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 target, #undef FORCE_SET_TARGET_FIELD - env->set_async_hooks_init_function(Local()); - env->set_async_hooks_before_function(Local()); - env->set_async_hooks_after_function(Local()); - env->set_async_hooks_destroy_function(Local()); - env->set_async_hooks_promise_resolve_function(Local()); - env->set_async_hooks_callback_trampoline(Local()); - 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()); + realm->set_async_hooks_before_function(Local()); + realm->set_async_hooks_after_function(Local()); + realm->set_async_hooks_destroy_function(Local()); + realm->set_async_hooks_promise_resolve_function(Local()); + realm->set_async_hooks_callback_trampoline(Local()); + realm->set_async_hooks_binding(target); } void AsyncWrap::RegisterExternalReferences( diff --git a/src/env.cc b/src/env.cc index 0befcbd6c25570..4bfdfae2354741 100644 --- a/src/env.cc +++ b/src/env.cc @@ -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()->async_ids_stack_string(), - async_ids_stack_.GetJSArray()).Check(); + env() + ->principal_realm() + ->async_hooks_binding() + ->Set(env()->context(), + env()->async_ids_stack_string(), + async_ids_stack_.GetJSArray()) + .Check(); } void AsyncHooks::FailWithCorruptedAsyncStack(double expected_async_id) { diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 1d61fe5f0522ba..bad60d02dfe953 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -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, Local module, Local url, Local context_object, Local 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& 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 that = args.This(); @@ -122,7 +124,7 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { } else { CHECK(args[1]->IsObject()); contextify_context = ContextifyContext::ContextFromContextifiedSandbox( - env, args[1].As()); + realm->env(), args[1].As()); CHECK_NOT_NULL(contextify_context); context = contextify_context->context(); } @@ -148,8 +150,8 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { Local 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; @@ -206,7 +208,9 @@ void ModuleWrap::New(const FunctionCallbackInfo& 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& 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& args) { // be stored in an internal field. Local context_object = context->GetExtrasBindingObject(); Local synthetic_evaluation_step = - synthetic ? args[3] : Undefined(env->isolate()).As(); + synthetic ? args[3] : Undefined(realm->isolate()).As(); 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 createImportAttributesContainer( - Environment* env, Isolate* isolate, Local raw_attributes) { + Realm* realm, Isolate* isolate, Local raw_attributes) { Local 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(), - raw_attributes->Get(env->context(), i + 1).As()) + ->Set(realm->context(), + raw_attributes->Get(realm->context(), i).As(), + raw_attributes->Get(realm->context(), i + 1).As()) .ToChecked(); } @@ -265,7 +270,7 @@ static Local createImportAttributesContainer( } void ModuleWrap::Link(const FunctionCallbackInfo& 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& args) { // call the dependency resolve callbacks for (int i = 0; i < module_requests_length; i++) { Local module_request = - module_requests->Get(env->context(), i).As(); + module_requests->Get(realm->context(), i).As(); Local 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 raw_attributes = module_request->GetImportAssertions(); Local attributes = - createImportAttributesContainer(env, isolate, raw_attributes); + createImportAttributesContainer(realm, isolate, raw_attributes); Local argv[] = { specifier, @@ -315,11 +320,11 @@ void ModuleWrap::Link(const FunctionCallbackInfo& 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 resolve_promise = resolve_return_value.As(); - 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& args) { } void ModuleWrap::Instantiate(const FunctionCallbackInfo& 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 = obj->context(); Local 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& 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& args) { } void ModuleWrap::Evaluate(const FunctionCallbackInfo& 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 = obj->context(); @@ -368,14 +375,14 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& 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& 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& args) { } void ModuleWrap::GetNamespace(const FunctionCallbackInfo& 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& 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& args) { void ModuleWrap::GetStaticDependencySpecifiers( const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); + Realm* realm = Realm::GetCurrent(args); ModuleWrap* obj; ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); - Local module = obj->module_.Get(env->isolate()); + Local module = obj->module_.Get(realm->isolate()); Local module_requests = module->GetModuleRequests(); int count = module_requests->Length(); @@ -479,12 +485,12 @@ void ModuleWrap::GetStaticDependencySpecifiers( for (int i = 0; i < count; i++) { Local module_request = - module_requests->Get(env->context(), i).As(); + module_requests->Get(realm->context(), i).As(); 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& args) { @@ -501,15 +507,13 @@ MaybeLocal ModuleWrap::ResolveModuleCallback( Local specifier, Local import_attributes, Local 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(); } - Isolate* isolate = env->isolate(); - Utf8Value specifier_utf8(isolate, specifier); std::string specifier_std(*specifier_utf8, specifier_utf8.length()); @@ -559,11 +563,16 @@ static MaybeLocal ImportModuleDynamically( THROW_ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE(isolate); return MaybeLocal(); } + 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 import_callback = - env->host_import_module_dynamically_callback(); + realm->host_import_module_dynamically_callback(); Local id; Local options = host_defined_options.As(); @@ -579,7 +588,7 @@ static MaybeLocal ImportModuleDynamically( } Local attributes = - createImportAttributesContainer(env, isolate, import_attributes); + createImportAttributesContainer(realm, isolate, import_attributes); Local import_args[] = { id, @@ -603,13 +612,13 @@ static MaybeLocal ImportModuleDynamically( void ModuleWrap::SetImportModuleDynamicallyCallback( const FunctionCallbackInfo& 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 import_callback = args[0].As(); - 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 wrap = module_wrap->object(); Local callback = - env->host_initialize_import_meta_object_callback(); + realm->host_initialize_import_meta_object_callback(); Local id; if (!wrap->GetPrivate(context, env->host_defined_option_symbol()) .ToLocal(&id)) { @@ -637,7 +651,7 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback( Local 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& 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 import_meta_callback = args[0].As(); - 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& args) { } } -void ModuleWrap::Initialize(Local target, - Local unused, - Local context, - void* priv) { - Environment* env = Environment::GetCurrent(context); - Isolate* isolate = env->isolate(); +void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, + Local target) { + Isolate* isolate = isolate_data->isolate(); Local tpl = NewFunctionTemplate(isolate, New); tpl->InstanceTemplate()->SetInternalFieldCount( @@ -767,28 +778,36 @@ void ModuleWrap::Initialize(Local 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 target, + Local unused, + Local 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)) \ - .FromJust() - V(kUninstantiated); - V(kInstantiating); - V(kInstantiated); - V(kEvaluating); - V(kEvaluated); - V(kErrored); + target \ + ->Set(context, \ + FIXED_ONE_BYTE_STRING(isolate, #name), \ + Integer::New(isolate, Module::Status::name)) \ + .FromJust() + V(kUninstantiated); + V(kInstantiating); + V(kInstantiated); + V(kEvaluating); + V(kEvaluated); + V(kErrored); #undef V } @@ -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) diff --git a/src/module_wrap.h b/src/module_wrap.h index 1fc801edced9c5..e17048357feca2 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -10,6 +10,7 @@ namespace node { +class IsolateData; class Environment; class ExternalReferenceRegistry; @@ -40,10 +41,12 @@ class ModuleWrap : public BaseObject { kInternalFieldCount }; - static void Initialize(v8::Local target, - v8::Local unused, - v8::Local context, - void* priv); + static void CreatePerIsolateProperties(IsolateData* isolate_data, + v8::Local target); + static void CreatePerContextProperties(v8::Local target, + v8::Local unused, + v8::Local context, + void* priv); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static void HostInitializeImportMetaObjectCallback( v8::Local context, @@ -66,7 +69,7 @@ class ModuleWrap : public BaseObject { } private: - ModuleWrap(Environment* env, + ModuleWrap(Realm* realm, v8::Local object, v8::Local module, v8::Local url, diff --git a/src/node_binding.h b/src/node_binding.h index 2901231090cce3..7256bf2bbcf732 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -40,6 +40,7 @@ static_assert(static_cast(NM_F_LINKED) == V(fs_dir) \ V(messaging) \ V(mksnapshot) \ + V(module_wrap) \ V(performance) \ V(process_methods) \ V(timers) \ diff --git a/src/node_errors.h b/src/node_errors.h index 0f4a2d0cc6eaaf..e804c250ab169a 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -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)...); \ + } \ + template \ + inline void THROW_##code(Realm* realm, const char* format, Args&&... args) { \ + THROW_##code(realm->isolate(), format, std::forward(args)...); \ } ERRORS_WITH_CODE(V) #undef V diff --git a/src/node_shadow_realm.cc b/src/node_shadow_realm.cc index 1cf0a57617b8b2..e7199e18756521 100644 --- a/src/node_shadow_realm.cc +++ b/src/node_shadow_realm.cc @@ -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 HostCreateShadowRealmContextCallback( Local 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 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(); + } + + // Setup process.env proxy. + Local env_string = FIXED_ONE_BYTE_STRING(isolate_, "env"); + Local env_proxy; + if (!isolate_data()->env_proxy_template()->NewInstance(context()).ToLocal( + &env_proxy) || + process_object()->Set(context(), env_string, env_proxy).IsNothing()) { + return MaybeLocal(); + } + + if (ExecuteBootstrapper("internal/bootstrap/shadow_realm").IsEmpty()) { + return MaybeLocal(); + } + return v8::True(isolate_); } diff --git a/test/fixtures/es-module-shadow-realm/custom-loaders.js b/test/fixtures/es-module-shadow-realm/custom-loaders.js new file mode 100644 index 00000000000000..bf4402250cc4f8 --- /dev/null +++ b/test/fixtures/es-module-shadow-realm/custom-loaders.js @@ -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(); diff --git a/test/fixtures/es-module-shadow-realm/preload-main.js b/test/fixtures/es-module-shadow-realm/preload-main.js new file mode 100644 index 00000000000000..4258b012ad6139 --- /dev/null +++ b/test/fixtures/es-module-shadow-realm/preload-main.js @@ -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); diff --git a/test/fixtures/es-module-shadow-realm/preload.js b/test/fixtures/es-module-shadow-realm/preload.js new file mode 100644 index 00000000000000..dbbcb65e5a17e7 --- /dev/null +++ b/test/fixtures/es-module-shadow-realm/preload.js @@ -0,0 +1 @@ +globalThis.preload = 42; diff --git a/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs b/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs new file mode 100644 index 00000000000000..50a6aa3fe1e5b1 --- /dev/null +++ b/test/fixtures/es-module-shadow-realm/re-export-state-counter.mjs @@ -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"; diff --git a/test/fixtures/es-module-shadow-realm/state-counter.mjs b/test/fixtures/es-module-shadow-realm/state-counter.mjs new file mode 100644 index 00000000000000..c547658bf455ba --- /dev/null +++ b/test/fixtures/es-module-shadow-realm/state-counter.mjs @@ -0,0 +1,4 @@ +let counter = 0; +export const getCounter = () => { + return counter++; +}; diff --git a/test/parallel/test-shadow-realm-allowed-builtin-modules.js b/test/parallel/test-shadow-realm-allowed-builtin-modules.js new file mode 100644 index 00000000000000..2aa550ac7bb55b --- /dev/null +++ b/test/parallel/test-shadow-realm-allowed-builtin-modules.js @@ -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()); diff --git a/test/parallel/test-shadow-realm-custom-loaders.js b/test/parallel/test-shadow-realm-custom-loaders.js new file mode 100644 index 00000000000000..80cda74bb88940 --- /dev/null +++ b/test/parallel/test-shadow-realm-custom-loaders.js @@ -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()); diff --git a/test/parallel/test-shadow-realm-gc-module.js b/test/parallel/test-shadow-realm-gc-module.js new file mode 100644 index 00000000000000..7f822bdd52fe1d --- /dev/null +++ b/test/parallel/test-shadow-realm-gc-module.js @@ -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()); diff --git a/test/parallel/test-shadow-realm-import-value-resolve.js b/test/parallel/test-shadow-realm-import-value-resolve.js new file mode 100644 index 00000000000000..ee1c17d67c12f1 --- /dev/null +++ b/test/parallel/test-shadow-realm-import-value-resolve.js @@ -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()); diff --git a/test/parallel/test-shadow-realm-module.js b/test/parallel/test-shadow-realm-module.js new file mode 100644 index 00000000000000..bc0c2c04f69f2b --- /dev/null +++ b/test/parallel/test-shadow-realm-module.js @@ -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()); diff --git a/test/parallel/test-shadow-realm-preload-module.js b/test/parallel/test-shadow-realm-preload-module.js new file mode 100644 index 00000000000000..ebd29c1c4a8b80 --- /dev/null +++ b/test/parallel/test-shadow-realm-preload-module.js @@ -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());