Skip to content

Commit

Permalink
vm: fix leak in vm.compileFunction when importModuleDynamically is used
Browse files Browse the repository at this point in the history
Previously in the implementation there was a cycle that V8 could not
detect:

Strong global reference to CompiledFnEntry (JS wrapper)
    -> strong reference to callback setting (through the
       callbackMap key-value pair)
    -> importModuleDynamically (wrapper in internalCompileFunction())
    -> Strong reference to the compiled function (through closure in
       internalCompileFunction())

The CompiledFnEntry only gets GC'ed when the compiled function is GC'ed.
Since the compiled function is always reachable as described above,
there is a leak.

We only needed the first strong global reference because we didn't want
the function to outlive the CompiledFnEntry. In this case it can be
solved by using a private symbol instead of going with the global
reference + destruction in the weak callback, which V8's GC is not
going to understand.

PR-URL: nodejs#46785
Fixes: nodejs#42080
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
joyeecheung authored and legendecas committed Mar 1, 2023
1 parent 0597f1b commit 986498b
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/env_properties.h
Expand Up @@ -20,6 +20,7 @@
#define PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(V) \
V(arrow_message_private_symbol, "node:arrowMessage") \
V(contextify_context_private_symbol, "node:contextify:context") \
V(compiled_function_entry, "node:compiled_function_entry") \
V(decorated_private_symbol, "node:decorated") \
V(napi_type_tag, "node:napi:type_tag") \
V(napi_wrapper, "node:napi:wrapper") \
Expand Down
17 changes: 5 additions & 12 deletions src/node_contextify.cc
Expand Up @@ -77,7 +77,6 @@ using v8::Uint32;
using v8::UnboundScript;
using v8::Value;
using v8::WeakCallbackInfo;
using v8::WeakCallbackType;

// The vm module executes code in a sandboxed environment with a different
// global object than the rest of the code. This is achieved by applying
Expand Down Expand Up @@ -1263,8 +1262,7 @@ void ContextifyContext::CompileFunction(
context).ToLocal(&cache_key)) {
return;
}
CompiledFnEntry* entry = new CompiledFnEntry(env, cache_key, id, fn);
env->id_to_function_map.emplace(id, entry);
new CompiledFnEntry(env, cache_key, id, fn);

Local<Object> result = Object::New(isolate);
if (result->Set(parsing_context, env->function_string(), fn).IsNothing())
Expand Down Expand Up @@ -1296,23 +1294,18 @@ void ContextifyContext::CompileFunction(
args.GetReturnValue().Set(result);
}

void CompiledFnEntry::WeakCallback(
const WeakCallbackInfo<CompiledFnEntry>& data) {
CompiledFnEntry* entry = data.GetParameter();
delete entry;
}

CompiledFnEntry::CompiledFnEntry(Environment* env,
Local<Object> object,
uint32_t id,
Local<Function> fn)
: BaseObject(env, object), id_(id), fn_(env->isolate(), fn) {
fn_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
: BaseObject(env, object), id_(id) {
MakeWeak();
fn->SetPrivate(env->context(), env->compiled_function_entry(), object);
env->id_to_function_map.emplace(id, this);
}

CompiledFnEntry::~CompiledFnEntry() {
env()->id_to_function_map.erase(id_);
fn_.ClearWeak();
}

static void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
Expand Down
3 changes: 0 additions & 3 deletions src/node_contextify.h
Expand Up @@ -194,9 +194,6 @@ class CompiledFnEntry final : public BaseObject {

private:
uint32_t id_;
v8::Global<v8::Function> fn_;

static void WeakCallback(const v8::WeakCallbackInfo<CompiledFnEntry>& data);
};

v8::Maybe<bool> StoreCodeCacheResult(
Expand Down
14 changes: 14 additions & 0 deletions test/pummel/test-vm-compile-function-leak.js
@@ -0,0 +1,14 @@
'use strict';

// Flags: --max-old-space-size=10

require('../common');
const vm = require('vm');

const code = `console.log("${'hello world '.repeat(1e5)}");`;

for (let i = 0; i < 10000; i++) {
vm.compileFunction(code, [], {
importModuleDynamically: () => {},
});
}

0 comments on commit 986498b

Please sign in to comment.