Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: introduce codeGenerationFromString event #35157

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ console.log('This message is displayed first.');
// Process exit event with code: 0
```

### Event: `'codeGenerationFromString'`

<!-- YAML
added: REPLACEME
-->

The `'codeGenerationFromString'` event is emitted when a call is made to `eval(str)`
or `new Function(str)`.

The event handler callback will be invoked with the argument passed to `eval` or the
`Function` constructor.

The call to `eval` or `new Function` will happen after the event is emitted.

```js
process.on('codeGenerationFromString', (code) => {
console.log(`Generating code from string '${code}'`);
});
const item = { foo: 0 };
eval('item.foo++');
```

will log

```text
Generating code from string 'item.foo++'
```

### Event: `'disconnect'`

<!-- YAML
Expand Down
20 changes: 20 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ process.emitWarning = emitWarning;
// Note: only after this point are the timers effective
}

setupCodegenFromStringCallback(rawMethods);

// Preload modules so that they are included in the builtin snapshot.
require('fs');
require('v8');
Expand Down Expand Up @@ -477,6 +479,24 @@ function createGlobalConsole(consoleFromVM) {
return consoleFromNode;
}

function setupCodegenFromStringCallback(bindings) {
if (!bindings.codeGenerationFromStringsAllowed()) {
// Do nothing if code generation from strings is not allowed.
return;
}
const eventName = 'codeGenerationFromString';
process.on('newListener', (event) => {
if (event === eventName && process.listenerCount(eventName) === 0) {
bindings.setEmitCodeGenFromStringEvent(true);
}
});
process.on('removeListener', (event) => {
if (event === eventName && process.listenerCount(eventName) === 0) {
bindings.setEmitCodeGenFromStringEvent(false);
}
});
}

// https://heycam.github.io/webidl/#es-namespaces
function exposeNamespace(target, name, namespaceObject) {
ObjectDefineProperty(target, name, {
Expand Down
58 changes: 58 additions & 0 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ typedef int mode_t;

namespace node {

using errors::TryCatchScope;
using v8::Array;
using v8::ArrayBuffer;
using v8::CFunction;
Expand Down Expand Up @@ -416,6 +417,57 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
env->Exit(code);
}

static void CodeGenerationFromStringsAllowed(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
bool value = context->IsCodeGenerationFromStringsAllowed();
args.GetReturnValue().Set(value);
}

static v8::ModifyCodeGenerationFromStringsResult CodeGenCallback(
Local<Context> context,
Local<Value> source,
bool is_code_like) {
Environment* env = Environment::GetCurrent(context);
TryCatchScope try_catch(env);

ProcessEmit(env, "codeGenerationFromString", source);

// V8 does not expect this callback to have a scheduled exceptions once it
// returns, so we print them out in a best effort to do something about it
// without failing silently and without crashing the process.
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
Isolate* isolate = env->isolate();
fprintf(stderr, "Exception in codeGenerationFromString event callback:\n");
PrintCaughtException(isolate, env->context(), try_catch);
}

// returning {true, val} where val.IsEmpty() makes v8
// use the orignal value passed to `eval` which does not impact
// calls as `eval({})`
return {true, v8::MaybeLocal<v8::String>()};
}

static void SetEmitCodeGenFromStringEvent(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = args.GetIsolate();
Local<Context> context = env->context();

CHECK(args[0]->IsBoolean());

bool val = args[0]->BooleanValue(args.GetIsolate());
if (val) {
context->AllowCodeGenerationFromStrings(false);
isolate->SetModifyCodeGenerationFromStringsCallback(CodeGenCallback);
} else {
// This is enough to disable the handler. V8 will not call it anymore
// until set back to false
context->AllowCodeGenerationFromStrings(true);
}
}

namespace process {

constexpr FastStringKey BindingData::type_name;
Expand Down Expand Up @@ -555,6 +607,10 @@ static void Initialize(Local<Object> target,
env->SetMethod(target, "reallyExit", ReallyExit);
env->SetMethodNoSideEffect(target, "uptime", Uptime);
env->SetMethod(target, "patchProcessObject", PatchProcessObject);
env->SetMethod(target, "codeGenerationFromStringsAllowed",
CodeGenerationFromStringsAllowed);
env->SetMethod(target, "setEmitCodeGenFromStringEvent",
SetEmitCodeGenFromStringEvent);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
Expand Down Expand Up @@ -582,6 +638,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(ReallyExit);
registry->Register(Uptime);
registry->Register(PatchProcessObject);
registry->Register(CodeGenerationFromStringsAllowed);
registry->Register(SetEmitCodeGenFromStringEvent);
}

} // namespace process
Expand Down
39 changes: 39 additions & 0 deletions test/parallel/test-process-codegen-from-string.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

const common = require('../common');
vdeturckheim marked this conversation as resolved.
Show resolved Hide resolved
const assert = require('assert');

const item = { foo: 0 };
eval('++item.foo');

assert.strictEqual(item.foo, 1);

process.on('codeGenerationFromString', common.mustCall(handleError((code) => {
assert.strictEqual(code, 'item.foo++');
})));

eval('item.foo++');
assert.strictEqual(item.foo, 2);

process.removeAllListeners('codeGenerationFromString');

process.on('codeGenerationFromString', common.mustCall(handleError((code) => {
assert.strictEqual(code, '(function anonymous(a,b\n) {\nreturn a + b\n})');
})));

const fct = new Function('a', 'b', 'return a + b');
assert.strictEqual(fct(1, 2), 3);

function handleError(fn) {
return (...args) => {
try {
fn(...args);
} catch (err) {
// The C++ code will just log the error to stderr and continue with the
// flow of the program. Set the exit code manually to ensure the test
// script fails in case of an error.
process.exitCode = 1;
throw err;
}
};
}