Skip to content

Commit aa328cb

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 aa328cb

16 files changed

+150
-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

+41-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,47 @@ 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+
const moduleMap = new SafeWeakMap();
72+
73+
/**
74+
* V8 would make sure that as long as import() can still be initiated from
75+
* the referer, the symbol referenced by |host_defined_option_symbol| should
76+
* be alive, which in term would keep the settings object alive through the
77+
* WeakMap, and in turn that keeps the referer object alive, which would be
78+
* passed into the callbacks.
79+
* The reference goes like this:
80+
* [v8::internal::Script] (via host defined options) ----1--> [id_symbol]
81+
* [referer wrap] (via host_defined_option_symbol) --------2------^ |
82+
* ^---------------3-------------------
83+
* 1+3 makes sure that as long as import() can still be initiated, the
84+
* referer wrap is still around and can be passed into the callbacks.
85+
* 2 is only there so that we can get the id symbol to configure the
86+
* weak map.
87+
* @param {ModuleWrap|ContextifyScript|Function} referer
88+
* @param {object} settings
89+
*/
90+
function registerModule(referer, settings) {
91+
const id_symbol = referer[host_defined_option_symbol];
92+
settings.referer = referer; // To prevent it from being GC'ed.
93+
moduleMap.set(id_symbol, settings);
94+
}
95+
96+
// The native callback
97+
function initializeImportMetaObject(symbol, meta) {
98+
if (moduleMap.has(symbol)) {
99+
const { initializeImportMeta, referer, vmReferer } = moduleMap.get(symbol);
77100
if (initializeImportMeta !== undefined) {
78-
meta = initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap);
101+
meta = initializeImportMeta(meta, vmReferer || referer);
79102
}
80103
}
81104
}
82105

83-
async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
84-
if (callbackMap.has(wrap)) {
85-
const { importModuleDynamically } = callbackMap.get(wrap);
106+
// The native callback
107+
async function importModuleDynamicallyCallback(symbol, specifier, assertions) {
108+
if (moduleMap.has(symbol)) {
109+
const { importModuleDynamically, referer, vmReferer } = moduleMap.get(symbol);
86110
if (importModuleDynamically !== undefined) {
87-
return importModuleDynamically(
88-
specifier, getModuleFromWrap(wrap) || wrap, assertions);
111+
return importModuleDynamically(specifier, vmReferer || referer, assertions);
89112
}
90113
}
91114
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
@@ -168,7 +191,7 @@ async function initializeHooks() {
168191
}
169192

170193
module.exports = {
171-
setCallbackForWrap,
194+
registerModule,
172195
initializeESM,
173196
initializeHooks,
174197
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 settings = { __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+
settings = {
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+
settings.vmReferer = this;
142+
const { registerModule } = require('internal/modules/esm/utils');
143+
registerModule(this[kWrap], settings);
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)