mirror of
https://github.com/nodejs/node.git
synced 2025-05-06 01:43:00 +00:00

Previously when managing the importModuleDynamically callback of vm.compileFunction(), we use an ID number as the host defined option and maintain a per-Environment ID -> CompiledFnEntry map to retain the top-level referrer function returned by vm.compileFunction() in order to pass it back to the callback, but it would leak because with how we used v8::Persistent to maintain this reference, V8 would not be able to understand the cycle and would just think that the CompiledFnEntry was supposed to live forever. We made an attempt to make that reference known to V8 by making the CompiledFnEntry weak and using a private symbol to make CompiledFnEntry strongly references the top-level referrer function in https://github.com/nodejs/node/pull/46785, but that turned out to be unsound, because the there's no guarantee that the top-level function must be alive while import() can still be initiated from that function, since V8 could discard the top-level function and only keep inner functions alive, so relying on the top-level function to keep the CompiledFnEntry alive could result in use-after-free which caused a revert of that fix. With this patch we use a symbol in the host defined options instead of a number, because with the stage-3 symbol-as-weakmap-keys proposal we could directly use that symbol to keep the referrer alive using a WeakMap. As a bonus this also keeps the other kinds of referrers alive as long as import() can still be initiated from that Script/Module, so this also fixes the long-standing crash caused by vm.Script being GC'ed too early when its importModuleDynamically callback still needs it. PR-URL: https://github.com/nodejs/node/pull/48510 Refs: https://github.com/nodejs/node/issues/44211 Refs: https://github.com/nodejs/node/issues/42080 Refs: https://github.com/nodejs/node/issues/47096 Refs: https://github.com/nodejs/node/issues/43205 Refs: https://github.com/nodejs/node/issues/38695 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
211 lines
6.4 KiB
JavaScript
211 lines
6.4 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayIsArray,
|
|
SafeSet,
|
|
SafeWeakMap,
|
|
ObjectFreeze,
|
|
} = primordials;
|
|
|
|
const {
|
|
privateSymbols: {
|
|
host_defined_option_symbol,
|
|
},
|
|
} = internalBinding('util');
|
|
const {
|
|
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
|
|
ERR_INVALID_ARG_VALUE,
|
|
} = require('internal/errors').codes;
|
|
const { getOptionValue } = require('internal/options');
|
|
const {
|
|
loadPreloadModules,
|
|
initializeFrozenIntrinsics,
|
|
} = require('internal/process/pre_execution');
|
|
const { pathToFileURL } = require('internal/url');
|
|
const {
|
|
setImportModuleDynamicallyCallback,
|
|
setInitializeImportMetaObjectCallback,
|
|
} = internalBinding('module_wrap');
|
|
const assert = require('internal/assert');
|
|
|
|
let defaultConditions;
|
|
function getDefaultConditions() {
|
|
assert(defaultConditions !== undefined);
|
|
return defaultConditions;
|
|
}
|
|
|
|
let defaultConditionsSet;
|
|
function getDefaultConditionsSet() {
|
|
assert(defaultConditionsSet !== undefined);
|
|
return defaultConditionsSet;
|
|
}
|
|
|
|
// This function is called during pre-execution, before any user code is run.
|
|
function initializeDefaultConditions() {
|
|
const userConditions = getOptionValue('--conditions');
|
|
const noAddons = getOptionValue('--no-addons');
|
|
const addonConditions = noAddons ? [] : ['node-addons'];
|
|
|
|
defaultConditions = ObjectFreeze([
|
|
'node',
|
|
'import',
|
|
...addonConditions,
|
|
...userConditions,
|
|
]);
|
|
defaultConditionsSet = new SafeSet(defaultConditions);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} [conditions]
|
|
* @returns {Set<string>}
|
|
*/
|
|
function getConditionsSet(conditions) {
|
|
if (conditions !== undefined && conditions !== getDefaultConditions()) {
|
|
if (!ArrayIsArray(conditions)) {
|
|
throw new ERR_INVALID_ARG_VALUE('conditions', conditions,
|
|
'expected an array');
|
|
}
|
|
return new SafeSet(conditions);
|
|
}
|
|
return getDefaultConditionsSet();
|
|
}
|
|
|
|
/**
|
|
* @callback ImportModuleDynamicallyCallback
|
|
* @param {string} specifier
|
|
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
|
|
* @param {object} assertions
|
|
* @returns { Promise<void> }
|
|
*/
|
|
|
|
/**
|
|
* @callback InitializeImportMetaCallback
|
|
* @param {object} meta
|
|
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* callbackReferrer: ModuleWrap|ContextifyScript|Function|vm.Module
|
|
* initializeImportMeta? : InitializeImportMetaCallback,
|
|
* importModuleDynamically? : ImportModuleDynamicallyCallback
|
|
* }} ModuleRegistry
|
|
*/
|
|
|
|
/**
|
|
* @type {WeakMap<symbol, ModuleRegistry>}
|
|
*/
|
|
const moduleRegistries = new SafeWeakMap();
|
|
|
|
/**
|
|
* V8 would make sure that as long as import() can still be initiated from
|
|
* the referrer, the symbol referenced by |host_defined_option_symbol| should
|
|
* be alive, which in term would keep the settings object alive through the
|
|
* WeakMap, and in turn that keeps the referrer object alive, which would be
|
|
* passed into the callbacks.
|
|
* The reference goes like this:
|
|
* [v8::internal::Script] (via host defined options) ----1--> [idSymbol]
|
|
* [callbackReferrer] (via host_defined_option_symbol) ------2------^ |
|
|
* ^----------3---- (via WeakMap)------
|
|
* 1+3 makes sure that as long as import() can still be initiated, the
|
|
* referrer wrap is still around and can be passed into the callbacks.
|
|
* 2 is only there so that we can get the id symbol to configure the
|
|
* weak map.
|
|
* @param {ModuleWrap|ContextifyScript|Function} referrer The referrer to
|
|
* get the id symbol from. This is different from callbackReferrer which
|
|
* could be set by the caller.
|
|
* @param {ModuleRegistry} registry
|
|
*/
|
|
function registerModule(referrer, registry) {
|
|
const idSymbol = referrer[host_defined_option_symbol];
|
|
// To prevent it from being GC'ed.
|
|
registry.callbackReferrer ??= referrer;
|
|
moduleRegistries.set(idSymbol, registry);
|
|
}
|
|
|
|
// The native callback
|
|
function initializeImportMetaObject(symbol, meta) {
|
|
if (moduleRegistries.has(symbol)) {
|
|
const { initializeImportMeta, callbackReferrer } = moduleRegistries.get(symbol);
|
|
if (initializeImportMeta !== undefined) {
|
|
meta = initializeImportMeta(meta, callbackReferrer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The native callback
|
|
async function importModuleDynamicallyCallback(symbol, specifier, assertions) {
|
|
if (moduleRegistries.has(symbol)) {
|
|
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
|
|
if (importModuleDynamically !== undefined) {
|
|
return importModuleDynamically(specifier, callbackReferrer, assertions);
|
|
}
|
|
}
|
|
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
|
|
}
|
|
|
|
// This is configured during pre-execution. Specifically it's set to true for
|
|
// the loader worker in internal/main/worker_thread.js.
|
|
let _isLoaderWorker = false;
|
|
function initializeESM(isLoaderWorker = false) {
|
|
_isLoaderWorker = isLoaderWorker;
|
|
initializeDefaultConditions();
|
|
// Setup per-isolate callbacks that locate data or callbacks that we keep
|
|
// track of for different ESM modules.
|
|
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
|
|
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
|
|
}
|
|
|
|
function isLoaderWorker() {
|
|
return _isLoaderWorker;
|
|
}
|
|
|
|
async function initializeHooks() {
|
|
const customLoaderURLs = getOptionValue('--experimental-loader');
|
|
|
|
let cwd;
|
|
try {
|
|
// `process.cwd()` can fail if the parent directory is deleted while the process runs.
|
|
cwd = process.cwd() + '/';
|
|
} catch {
|
|
cwd = '/';
|
|
}
|
|
|
|
|
|
const { Hooks } = require('internal/modules/esm/hooks');
|
|
const esmLoader = require('internal/process/esm_loader').esmLoader;
|
|
|
|
const hooks = new Hooks();
|
|
esmLoader.setCustomizations(hooks);
|
|
|
|
// We need the loader customizations to be set _before_ we start invoking
|
|
// `--require`, otherwise loops can happen because a `--require` script
|
|
// might call `register(...)` before we've installed ourselves. These
|
|
// global values are magically set in `setupUserModules` just for us and
|
|
// we call them in the correct order.
|
|
// N.B. This block appears here specifically in order to ensure that
|
|
// `--require` calls occur before `--loader` ones do.
|
|
loadPreloadModules();
|
|
initializeFrozenIntrinsics();
|
|
|
|
const parentURL = pathToFileURL(cwd).href;
|
|
for (let i = 0; i < customLoaderURLs.length; i++) {
|
|
await hooks.register(
|
|
customLoaderURLs[i],
|
|
parentURL,
|
|
);
|
|
}
|
|
|
|
return hooks;
|
|
}
|
|
|
|
module.exports = {
|
|
registerModule,
|
|
initializeESM,
|
|
initializeHooks,
|
|
getDefaultConditions,
|
|
getConditionsSet,
|
|
loaderWorkerId: 'internal/modules/esm/worker',
|
|
isLoaderWorker,
|
|
};
|