Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v20.x] module: bootstrap module loaders in shadow realm #51239

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@

const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
Expand Down Expand Up @@ -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)));

/**
Expand Down Expand Up @@ -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() {
Expand Down
21 changes: 21 additions & 0 deletions lib/internal/bootstrap/shadow_realm.js
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 5 additions & 6 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,15 +524,14 @@ let emittedLoaderFlagWarning = false;
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
* only one used for loading the main module and everything in its dependency graph, though separate instances of this
* class might be instantiated as part of bootstrap for other purposes.
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
* @returns {ModuleLoader}
*/
function createModuleLoader(useCustomLoadersIfPresent = true) {
function createModuleLoader() {
let customizations = null;
if (useCustomLoadersIfPresent &&
// 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.
!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) {
Expand Down
43 changes: 33 additions & 10 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
ArrayIsArray,
SafeSet,
SafeWeakMap,
Symbol,
ObjectFreeze,
} = primordials;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -253,10 +275,11 @@ async function initializeHooks() {

module.exports = {
registerModule,
registerRealm,
initializeESM,
initializeHooks,
getDefaultConditions,
getConditionsSet,
loaderWorkerId: 'internal/modules/esm/worker',
isLoaderWorker,
forceDefaultLoader,
};
4 changes: 2 additions & 2 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ let esmLoader;

module.exports = {
get esmLoader() {
return esmLoader ??= createModuleLoader(true);
return esmLoader ??= createModuleLoader();
},
async loadESM(callback) {
esmLoader ??= createModuleLoader(true);
esmLoader ??= createModuleLoader();
try {
const userImports = getOptionValue('--import');
if (userImports.length > 0) {
Expand Down
5 changes: 2 additions & 3 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const {
StringPrototypeStartsWith,
Symbol,
SymbolIterator,
Uint32Array,
} = primordials;

const {
Expand Down Expand Up @@ -65,10 +64,10 @@ function refreshHrtimeBuffer() {
// The 3 entries filled in by the original process.hrtime contains
// the upper/lower 32 bits of the second part of the value,
// and the remaining nanoseconds of the value.
hrValues = new Uint32Array(binding.hrtimeBuffer);
hrValues = binding.hrtimeBuffer;
// Use a BigUint64Array in the closure because this is actually a bit
// faster than simply returning a BigInt from C++ in V8 7.1.
hrBigintValues = new BigUint64Array(binding.hrtimeBuffer, 0, 1);
hrBigintValues = new BigUint64Array(binding.hrtimeBuffer.buffer, 0, 1);
}

// Create the buffers.
Expand Down
38 changes: 30 additions & 8 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -160,16 +180,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();
}
Expand Down Expand Up @@ -687,9 +708,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.
Expand Down Expand Up @@ -765,6 +786,7 @@ module.exports = {
setupUserModules,
prepareMainThreadExecution,
prepareWorkerThreadExecution,
prepareShadowRealmExecution,
markBootstrapComplete,
loadPreloadModules,
initializeFrozenIntrinsics,
Expand Down
22 changes: 13 additions & 9 deletions src/async_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 8 additions & 4 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ void IsolateData::CreateProperties() {
binding::CreateInternalBindingTemplates(this);

contextify::ContextifyContext::InitializeGlobalTemplates(this);
CreateEnvProxyTemplate(this);
}

constexpr uint16_t kDefaultCppGCEmebdderID = 0x90de;
Expand Down Expand Up @@ -1653,10 +1654,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) {
Expand Down
Loading
Loading