Skip to content

Commit f27339a

Browse files
committed
module: use symbol in WeakMap to manage host defined options
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 nodejs#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.
1 parent 3ce303c commit f27339a

16 files changed

+178
-189
lines changed

lib/internal/modules/esm/create_dynamic_module.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ import.meta.done();
4545

4646
if (imports.length)
4747
reflect.imports = { __proto__: null };
48-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
49-
setCallbackForWrap(m, {
48+
const { registerModule } = require('internal/modules/esm/utils');
49+
registerModule(m, {
50+
__proto__: null,
5051
initializeImportMeta: (meta, wrap) => {
5152
meta.exports = reflect.exports;
5253
if (reflect.imports)

lib/internal/modules/esm/loader.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ class DefaultModuleLoader {
101101
) {
102102
const evalInstance = (url) => {
103103
const { ModuleWrap } = internalBinding('module_wrap');
104-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
104+
const { registerModule } = require('internal/modules/esm/utils');
105105
const module = new ModuleWrap(url, undefined, source, 0, 0);
106-
setCallbackForWrap(module, {
106+
registerModule(module, {
107+
__proto__: null,
107108
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
108109
importModuleDynamically: (specifier, { url }, importAssertions) => {
109110
return this.import(specifier, url, importAssertions);

lib/internal/modules/esm/translators.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,9 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
114114
maybeCacheSourceMap(url, source);
115115
debug(`Translating StandardModule ${url}`);
116116
const module = new ModuleWrap(url, undefined, source, 0, 0);
117-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
118-
setCallbackForWrap(module, {
117+
const { registerModule } = require('internal/modules/esm/utils');
118+
registerModule(module, {
119+
__proto__: null,
119120
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
120121
importModuleDynamically,
121122
});

lib/internal/modules/esm/utils.js

+69-18
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const {
88
ObjectFreeze,
99
} = primordials;
1010

11+
const {
12+
privateSymbols: {
13+
host_defined_option_symbol,
14+
},
15+
} = internalBinding('util');
1116
const {
1217
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
1318
ERR_INVALID_ARG_VALUE,
@@ -19,16 +24,8 @@ const {
1924
setImportModuleDynamicallyCallback,
2025
setInitializeImportMetaObjectCallback,
2126
} = internalBinding('module_wrap');
22-
const {
23-
getModuleFromWrap,
24-
} = require('internal/vm/module');
2527
const assert = require('internal/assert');
2628

27-
const callbackMap = new SafeWeakMap();
28-
function setCallbackForWrap(wrap, data) {
29-
callbackMap.set(wrap, data);
30-
}
31-
3229
let defaultConditions;
3330
function getDefaultConditions() {
3431
assert(defaultConditions !== undefined);
@@ -71,21 +68,75 @@ function getConditionsSet(conditions) {
7168
return getDefaultConditionsSet();
7269
}
7370

74-
function initializeImportMetaObject(wrap, meta) {
75-
if (callbackMap.has(wrap)) {
76-
const { initializeImportMeta } = callbackMap.get(wrap);
71+
/**
72+
* @callback ImportModuleDynamicallyCallback
73+
* @param {string} specifier
74+
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
75+
* @param {object} assertions
76+
* @returns { Promise<void> }
77+
*/
78+
79+
/**
80+
* @callback InitializeImportMetaCallback
81+
* @param {object} meta
82+
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
83+
*/
84+
85+
/**
86+
* @typedef {{
87+
* callbackReferrer: ModuleWrap|ContextifyScript|Function|vm.Module
88+
* initializeImportMeta? : InitializeImportMetaCallback,
89+
* importModuleDynamically? : ImportModuleDynamicallyCallback
90+
* }} ModuleRegistry
91+
*/
92+
93+
/**
94+
* @type {WeakMap<symbol, ModuleRegistry>}
95+
*/
96+
const moduleRegistries = new SafeWeakMap();
97+
98+
/**
99+
* V8 would make sure that as long as import() can still be initiated from
100+
* the referrer, the symbol referenced by |host_defined_option_symbol| should
101+
* be alive, which in term would keep the settings object alive through the
102+
* WeakMap, and in turn that keeps the referrer object alive, which would be
103+
* passed into the callbacks.
104+
* The reference goes like this:
105+
* [v8::internal::Script] (via host defined options) ----1--> [idSymbol]
106+
* [callbackReferrer] (via host_defined_option_symbol) ------2------^ |
107+
* ^----------3---- (via WeakMap)------
108+
* 1+3 makes sure that as long as import() can still be initiated, the
109+
* referrer wrap is still around and can be passed into the callbacks.
110+
* 2 is only there so that we can get the id symbol to configure the
111+
* weak map.
112+
* @param {ModuleWrap|ContextifyScript|Function} referrer The referrer to
113+
* get the id symbol from. This is different from callbackReferrer which
114+
* could be set by the caller.
115+
* @param {ModuleRegistry} registry
116+
*/
117+
function registerModule(referrer, registry) {
118+
const idSymbol = referrer[host_defined_option_symbol];
119+
// To prevent it from being GC'ed. If
120+
registry.callbackReferrer ??= referrer;
121+
moduleRegistries.set(idSymbol, registry);
122+
}
123+
124+
// The native callback
125+
function initializeImportMetaObject(symbol, meta) {
126+
if (moduleRegistries.has(symbol)) {
127+
const { initializeImportMeta, callbackReferrer } = moduleRegistries.get(symbol);
77128
if (initializeImportMeta !== undefined) {
78-
meta = initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap);
129+
meta = initializeImportMeta(meta, callbackReferrer);
79130
}
80131
}
81132
}
82133

83-
async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
84-
if (callbackMap.has(wrap)) {
85-
const { importModuleDynamically } = callbackMap.get(wrap);
134+
// The native callback
135+
async function importModuleDynamicallyCallback(symbol, specifier, assertions) {
136+
if (moduleRegistries.has(symbol)) {
137+
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
86138
if (importModuleDynamically !== undefined) {
87-
return importModuleDynamically(
88-
specifier, getModuleFromWrap(wrap) || wrap, assertions);
139+
return importModuleDynamically(specifier, callbackReferrer, assertions);
89140
}
90141
}
91142
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
@@ -168,7 +219,7 @@ async function initializeHooks() {
168219
}
169220

170221
module.exports = {
171-
setCallbackForWrap,
222+
registerModule,
172223
initializeESM,
173224
initializeHooks,
174225
getDefaultConditions,

lib/internal/vm.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ function internalCompileFunction(code, params, options) {
100100
const { importModuleDynamicallyWrap } = require('internal/vm/module');
101101
const wrapped = importModuleDynamicallyWrap(importModuleDynamically);
102102
const func = result.function;
103-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
104-
setCallbackForWrap(result.cacheKey, {
105-
importModuleDynamically: (s, _k, i) => wrapped(s, func, i),
103+
const { registerModule } = require('internal/modules/esm/utils');
104+
registerModule(func, {
105+
__proto__: null,
106+
importModuleDynamically: wrapped,
106107
});
107108
}
108109

lib/internal/vm/module.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const {
1111
ObjectSetPrototypeOf,
1212
ReflectApply,
1313
SafePromiseAllReturnVoid,
14-
SafeWeakMap,
1514
Symbol,
1615
SymbolToStringTag,
1716
TypeError,
@@ -69,7 +68,6 @@ const STATUS_MAP = {
6968

7069
let globalModuleId = 0;
7170
const defaultModuleName = 'vm:module';
72-
const wrapToModuleMap = new SafeWeakMap();
7371

7472
const kWrap = Symbol('kWrap');
7573
const kContext = Symbol('kContext');
@@ -120,25 +118,29 @@ class Module {
120118
});
121119
}
122120

121+
let registry = { __proto__: null };
123122
if (sourceText !== undefined) {
124123
this[kWrap] = new ModuleWrap(identifier, context, sourceText,
125124
options.lineOffset, options.columnOffset,
126125
options.cachedData);
127-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
128-
setCallbackForWrap(this[kWrap], {
126+
registry = {
129127
initializeImportMeta: options.initializeImportMeta,
130128
importModuleDynamically: options.importModuleDynamically ?
131129
importModuleDynamicallyWrap(options.importModuleDynamically) :
132130
undefined,
133-
});
131+
};
134132
} else {
135133
assert(syntheticEvaluationSteps);
136134
this[kWrap] = new ModuleWrap(identifier, context,
137135
syntheticExportNames,
138136
syntheticEvaluationSteps);
139137
}
140138

141-
wrapToModuleMap.set(this[kWrap], this);
139+
// This will take precedence over the referrer as the object being
140+
// passed into the callbacks.
141+
registry.callbackReferrer = this;
142+
const { registerModule } = require('internal/modules/esm/utils');
143+
registerModule(this[kWrap], registry);
142144

143145
this[kContext] = context;
144146
}
@@ -445,5 +447,4 @@ module.exports = {
445447
SourceTextModule,
446448
SyntheticModule,
447449
importModuleDynamicallyWrap,
448-
getModuleFromWrap: (wrap) => wrapToModuleMap.get(wrap),
449450
};

lib/vm.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ class Script extends ContextifyScript {
106106
validateFunction(importModuleDynamically,
107107
'options.importModuleDynamically');
108108
const { importModuleDynamicallyWrap } = require('internal/vm/module');
109-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
110-
setCallbackForWrap(this, {
109+
const { registerModule } = require('internal/modules/esm/utils');
110+
registerModule(this, {
111+
__proto__: null,
111112
importModuleDynamically:
112113
importModuleDynamicallyWrap(importModuleDynamically),
113114
});

src/env-inl.h

-10
Original file line numberDiff line numberDiff line change
@@ -359,16 +359,6 @@ inline AliasedInt32Array& Environment::stream_base_state() {
359359
return stream_base_state_;
360360
}
361361

362-
inline uint32_t Environment::get_next_module_id() {
363-
return module_id_counter_++;
364-
}
365-
inline uint32_t Environment::get_next_script_id() {
366-
return script_id_counter_++;
367-
}
368-
inline uint32_t Environment::get_next_function_id() {
369-
return function_id_counter_++;
370-
}
371-
372362
ShouldNotAbortOnUncaughtScope::ShouldNotAbortOnUncaughtScope(
373363
Environment* env)
374364
: env_(env) {

src/env.h

-8
Original file line numberDiff line numberDiff line change
@@ -722,14 +722,6 @@ class Environment : public MemoryRetainer {
722722
builtins::BuiltinLoader* builtin_loader();
723723

724724
std::unordered_multimap<int, loader::ModuleWrap*> hash_to_module_map;
725-
std::unordered_map<uint32_t, loader::ModuleWrap*> id_to_module_map;
726-
std::unordered_map<uint32_t, contextify::ContextifyScript*>
727-
id_to_script_map;
728-
std::unordered_map<uint32_t, contextify::CompiledFnEntry*> id_to_function_map;
729-
730-
inline uint32_t get_next_module_id();
731-
inline uint32_t get_next_script_id();
732-
inline uint32_t get_next_function_id();
733725

734726
EnabledDebugList* enabled_debug_list() { return &enabled_debug_list_; }
735727

src/env_properties.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
V(arrow_message_private_symbol, "node:arrowMessage") \
2222
V(contextify_context_private_symbol, "node:contextify:context") \
2323
V(decorated_private_symbol, "node:decorated") \
24+
V(host_defined_option_symbol, "node:host_defined_option_symbol") \
2425
V(napi_type_tag, "node:napi:type_tag") \
2526
V(napi_wrapper, "node:napi:wrapper") \
2627
V(untransferable_object_private_symbol, "node:untransferableObject") \
@@ -339,7 +340,6 @@
339340
V(blocklist_constructor_template, v8::FunctionTemplate) \
340341
V(contextify_global_template, v8::ObjectTemplate) \
341342
V(contextify_wrapper_template, v8::ObjectTemplate) \
342-
V(compiled_fn_entry_template, v8::ObjectTemplate) \
343343
V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \
344344
V(env_proxy_template, v8::ObjectTemplate) \
345345
V(env_proxy_ctor_template, v8::FunctionTemplate) \

0 commit comments

Comments
 (0)