Skip to content

Commit b856c83

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. PR-URL: nodejs#48510 Refs: nodejs#44211 Refs: nodejs#42080 Refs: nodejs#47096 Refs: nodejs#43205 Refs: nodejs#38695 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent 9cee8ac commit b856c83

16 files changed

+185
-188
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
@@ -180,9 +180,10 @@ class ModuleLoader {
180180
) {
181181
const evalInstance = (url) => {
182182
const { ModuleWrap } = internalBinding('module_wrap');
183-
const { setCallbackForWrap } = require('internal/modules/esm/utils');
183+
const { registerModule } = require('internal/modules/esm/utils');
184184
const module = new ModuleWrap(url, undefined, source, 0, 0);
185-
setCallbackForWrap(module, {
185+
registerModule(module, {
186+
__proto__: null,
186187
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
187188
importModuleDynamically: (specifier, { url }, importAssertions) => {
188189
return this.import(specifier, url, importAssertions);

lib/internal/modules/esm/translators.js

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

lib/internal/modules/esm/utils.js

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

10+
const {
11+
privateSymbols: {
12+
host_defined_option_symbol,
13+
},
14+
} = internalBinding('util');
1015
const {
1116
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
1217
ERR_INVALID_ARG_VALUE,
@@ -21,16 +26,8 @@ const {
2126
setImportModuleDynamicallyCallback,
2227
setInitializeImportMetaObjectCallback,
2328
} = internalBinding('module_wrap');
24-
const {
25-
getModuleFromWrap,
26-
} = require('internal/vm/module');
2729
const assert = require('internal/assert');
2830

29-
const callbackMap = new SafeWeakMap();
30-
function setCallbackForWrap(wrap, data) {
31-
callbackMap.set(wrap, data);
32-
}
33-
3431
let defaultConditions;
3532
function getDefaultConditions() {
3633
assert(defaultConditions !== undefined);
@@ -73,21 +70,75 @@ function getConditionsSet(conditions) {
7370
return getDefaultConditionsSet();
7471
}
7572

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

85-
async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
86-
if (callbackMap.has(wrap)) {
87-
const { importModuleDynamically } = callbackMap.get(wrap);
136+
// The native callback
137+
async function importModuleDynamicallyCallback(symbol, specifier, assertions) {
138+
if (moduleRegistries.has(symbol)) {
139+
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
88140
if (importModuleDynamically !== undefined) {
89-
return importModuleDynamically(
90-
specifier, getModuleFromWrap(wrap) || wrap, assertions);
141+
return importModuleDynamically(specifier, callbackReferrer, assertions);
91142
}
92143
}
93144
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
@@ -151,7 +202,7 @@ async function initializeHooks() {
151202
}
152203

153204
module.exports = {
154-
setCallbackForWrap,
205+
registerModule,
155206
initializeESM,
156207
initializeHooks,
157208
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

+9-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,30 @@ 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 = {
127+
__proto__: null,
129128
initializeImportMeta: options.initializeImportMeta,
130129
importModuleDynamically: options.importModuleDynamically ?
131130
importModuleDynamicallyWrap(options.importModuleDynamically) :
132131
undefined,
133-
});
132+
};
134133
} else {
135134
assert(syntheticEvaluationSteps);
136135
this[kWrap] = new ModuleWrap(identifier, context,
137136
syntheticExportNames,
138137
syntheticEvaluationSteps);
139138
}
140139

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

143146
this[kContext] = context;
144147
}
@@ -445,5 +448,4 @@ module.exports = {
445448
SourceTextModule,
446449
SyntheticModule,
447450
importModuleDynamicallyWrap,
448-
getModuleFromWrap: (wrap) => wrapToModuleMap.get(wrap),
449451
};

lib/vm.js

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

src/env-inl.h

-10
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,6 @@ inline AliasedInt32Array& Environment::stream_base_state() {
393393
return stream_base_state_;
394394
}
395395

396-
inline uint32_t Environment::get_next_module_id() {
397-
return module_id_counter_++;
398-
}
399-
inline uint32_t Environment::get_next_script_id() {
400-
return script_id_counter_++;
401-
}
402-
inline uint32_t Environment::get_next_function_id() {
403-
return function_id_counter_++;
404-
}
405-
406396
ShouldNotAbortOnUncaughtScope::ShouldNotAbortOnUncaughtScope(
407397
Environment* env)
408398
: env_(env) {

src/env.h

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

748748
std::unordered_multimap<int, loader::ModuleWrap*> hash_to_module_map;
749-
std::unordered_map<uint32_t, loader::ModuleWrap*> id_to_module_map;
750-
std::unordered_map<uint32_t, contextify::ContextifyScript*>
751-
id_to_script_map;
752-
std::unordered_map<uint32_t, contextify::CompiledFnEntry*> id_to_function_map;
753-
754-
inline uint32_t get_next_module_id();
755-
inline uint32_t get_next_script_id();
756-
inline uint32_t get_next_function_id();
757749

758750
EnabledDebugList* enabled_debug_list() { return &enabled_debug_list_; }
759751

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") \
@@ -338,7 +339,6 @@
338339
V(blocklist_constructor_template, v8::FunctionTemplate) \
339340
V(contextify_global_template, v8::ObjectTemplate) \
340341
V(contextify_wrapper_template, v8::ObjectTemplate) \
341-
V(compiled_fn_entry_template, v8::ObjectTemplate) \
342342
V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \
343343
V(env_proxy_template, v8::ObjectTemplate) \
344344
V(env_proxy_ctor_template, v8::FunctionTemplate) \

0 commit comments

Comments
 (0)