vm: move options checks from C++ to JS

Also introduces stronger type validations for options passed to vm
functions.

PR-URL: https://github.com/nodejs/node/pull/19398
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Michaël Zasso 2018-03-16 15:23:39 +01:00
parent a820f4155b
commit 34d988f122
No known key found for this signature in database
GPG Key ID: 770F7A9A5AE15600
13 changed files with 457 additions and 410 deletions

View File

@ -618,6 +618,9 @@ console.log(globalVar);
<!-- YAML
added: v0.3.1
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19398
description: The `sandbox` option can no longer be a function.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19016
description: The `codeGeneration` option is supported now.

View File

@ -88,12 +88,7 @@
};
}
// Minimal sandbox helper
const ContextifyScript = process.binding('contextify').ContextifyScript;
function runInThisContext(code, options) {
const script = new ContextifyScript(code, options);
return script.runInThisContext();
}
// Set up NativeModule
function NativeModule(id) {
@ -205,11 +200,9 @@
this.loading = true;
try {
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
const script = new ContextifyScript(source, this.filename);
// Arguments: timeout, displayErrors, breakOnSigint
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
NativeModule.requireForDeps :
NativeModule.require;

View File

@ -59,6 +59,10 @@ class Module {
let context;
if (options.context !== undefined) {
if (typeof options.context !== 'object' || options.context === null) {
throw new ERR_INVALID_ARG_TYPE('options.context', 'object',
options.context);
}
if (isContext(options.context)) {
context = options.context;
} else {

223
lib/vm.js
View File

@ -24,63 +24,109 @@
const {
ContextifyScript,
kParsingContext,
makeContext,
isContext: _isContext,
} = process.binding('contextify');
const {
ERR_INVALID_ARG_TYPE,
ERR_MISSING_ARGS
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
// The binding provides a few useful primitives:
// - Script(code, { filename = "evalmachine.anonymous",
// displayErrors = true } = {})
// with methods:
// - runInThisContext({ displayErrors = true } = {})
// - runInContext(sandbox, { displayErrors = true, timeout = undefined } = {})
// - makeContext(sandbox)
// - isContext(sandbox)
// From this we build the entire documented API.
const { isUint8Array } = require('internal/util/types');
class Script extends ContextifyScript {
constructor(code, options) {
constructor(code, options = {}) {
code = `${code}`;
if (typeof options === 'string') {
options = { filename: options };
}
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
const {
filename = 'evalmachine.<anonymous>',
lineOffset = 0,
columnOffset = 0,
cachedData,
produceCachedData = false,
[kParsingContext]: parsingContext
} = options;
if (typeof filename !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.filename', 'string', filename);
}
validateInteger(lineOffset, 'options.lineOffset');
validateInteger(columnOffset, 'options.columnOffset');
if (cachedData !== undefined && !isUint8Array(cachedData)) {
throw new ERR_INVALID_ARG_TYPE('options.cachedData',
['Buffer', 'Uint8Array'], cachedData);
}
if (typeof produceCachedData !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE('options.produceCachedData', 'boolean',
produceCachedData);
}
// Calling `ReThrow()` on a native TryCatch does not generate a new
// abort-on-uncaught-exception check. A dummy try/catch in JS land
// protects against that.
try {
super(code, options);
super(code,
filename,
lineOffset,
columnOffset,
cachedData,
produceCachedData,
parsingContext);
} catch (e) {
throw e; /* node-do-not-add-exception-line */
}
}
runInThisContext(options) {
const { breakOnSigint, args } = getRunInContextArgs(options);
if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(super.runInThisContext, this, args);
} else {
return super.runInThisContext(...args);
}
}
runInContext(contextifiedSandbox, options) {
validateContext(contextifiedSandbox);
const { breakOnSigint, args } = getRunInContextArgs(options);
if (breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(super.runInContext, this,
[contextifiedSandbox, ...args]);
} else {
return super.runInContext(contextifiedSandbox, ...args);
}
}
runInNewContext(sandbox, options) {
const context = createContext(sandbox, getContextOptions(options));
return this.runInContext(context, options);
}
}
const realRunInThisContext = Script.prototype.runInThisContext;
const realRunInContext = Script.prototype.runInContext;
Script.prototype.runInThisContext = function(options) {
if (options && options.breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(realRunInThisContext, this, [options]);
} else {
return realRunInThisContext.call(this, options);
function validateContext(sandbox) {
if (typeof sandbox !== 'object' || sandbox === null) {
throw new ERR_INVALID_ARG_TYPE('contextifiedSandbox', 'Object', sandbox);
}
};
Script.prototype.runInContext = function(contextifiedSandbox, options) {
if (options && options.breakOnSigint && process.listenerCount('SIGINT') > 0) {
return sigintHandlersWrap(realRunInContext, this,
[contextifiedSandbox, options]);
} else {
return realRunInContext.call(this, contextifiedSandbox, options);
if (!_isContext(sandbox)) {
throw new ERR_INVALID_ARG_TYPE('contextifiedSandbox', 'vm.Context',
sandbox);
}
};
}
Script.prototype.runInNewContext = function(sandbox, options) {
const context = createContext(sandbox, getContextOptions(options));
return this.runInContext(context, options);
};
function validateInteger(prop, propName) {
if (!Number.isInteger(prop)) {
throw new ERR_INVALID_ARG_TYPE(propName, 'integer', prop);
}
if ((prop >> 0) !== prop) {
throw new ERR_OUT_OF_RANGE(propName, '32-bit integer', prop);
}
}
function validateString(prop, propName) {
if (prop !== undefined && typeof prop !== 'string')
@ -97,6 +143,39 @@ function validateObject(prop, propName) {
throw new ERR_INVALID_ARG_TYPE(propName, 'Object', prop);
}
function getRunInContextArgs(options = {}) {
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
let timeout = options.timeout;
if (timeout === undefined) {
timeout = -1;
} else if (!Number.isInteger(timeout) || timeout <= 0) {
throw new ERR_INVALID_ARG_TYPE('options.timeout', 'a positive integer',
timeout);
}
const {
displayErrors = true,
breakOnSigint = false
} = options;
if (typeof displayErrors !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE('options.displayErrors', 'boolean',
displayErrors);
}
if (typeof breakOnSigint !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE('options.breakOnSigint', 'boolean',
breakOnSigint);
}
return {
breakOnSigint,
args: [timeout, displayErrors, breakOnSigint]
};
}
function getContextOptions(options) {
if (options) {
validateObject(options.contextCodeGeneration,
@ -123,57 +202,43 @@ function getContextOptions(options) {
}
function isContext(sandbox) {
if (arguments.length < 1) {
throw new ERR_MISSING_ARGS('sandbox');
if (typeof sandbox !== 'object' || sandbox === null) {
throw new ERR_INVALID_ARG_TYPE('sandbox', 'Object', sandbox);
}
if (typeof sandbox !== 'object' && typeof sandbox !== 'function' ||
sandbox === null) {
throw new ERR_INVALID_ARG_TYPE('sandbox', 'object', sandbox);
}
return _isContext(sandbox);
}
let defaultContextNameIndex = 1;
function createContext(sandbox, options) {
if (sandbox === undefined) {
sandbox = {};
} else if (isContext(sandbox)) {
function createContext(sandbox = {}, options = {}) {
if (isContext(sandbox)) {
return sandbox;
}
if (options !== undefined) {
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
}
validateObject(options.codeGeneration, 'options.codeGeneration');
options = {
name: options.name,
origin: options.origin,
codeGeneration: typeof options.codeGeneration === 'object' ? {
strings: options.codeGeneration.strings,
wasm: options.codeGeneration.wasm,
} : undefined,
};
if (options.codeGeneration !== undefined) {
validateBool(options.codeGeneration.strings,
'options.codeGeneration.strings');
validateBool(options.codeGeneration.wasm,
'options.codeGeneration.wasm');
}
if (options.name === undefined) {
options.name = `VM Context ${defaultContextNameIndex++}`;
} else if (typeof options.name !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.name', 'string', options.name);
}
validateString(options.origin, 'options.origin');
} else {
options = {
name: `VM Context ${defaultContextNameIndex++}`
};
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
makeContext(sandbox, options);
const {
name = `VM Context ${defaultContextNameIndex++}`,
origin,
codeGeneration
} = options;
if (typeof name !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.name', 'string', options.name);
}
validateString(origin, 'options.origin');
validateObject(codeGeneration, 'options.codeGeneration');
let strings = true;
let wasm = true;
if (codeGeneration !== undefined) {
({ strings = true, wasm = true } = codeGeneration);
validateBool(strings, 'options.codeGeneration.strings');
validateBool(wasm, 'options.codeGeneration.wasm');
}
makeContext(sandbox, name, origin, strings, wasm);
return sandbox;
}
@ -200,6 +265,7 @@ function sigintHandlersWrap(fn, thisArg, argsArray) {
}
function runInContext(code, contextifiedSandbox, options) {
validateContext(contextifiedSandbox);
if (typeof options === 'string') {
options = {
filename: options,
@ -226,6 +292,9 @@ function runInNewContext(code, sandbox, options) {
}
function runInThisContext(code, options) {
if (typeof options === 'string') {
options = { filename: options };
}
return createScript(code, options).runInThisContext(options);
}

View File

@ -239,7 +239,6 @@ struct PackageConfig {
V(port_string, "port") \
V(preference_string, "preference") \
V(priority_string, "priority") \
V(produce_cached_data_string, "produceCachedData") \
V(promise_string, "promise") \
V(pubkey_string, "pubkey") \
V(query_string, "query") \

View File

@ -40,6 +40,7 @@ using v8::FunctionTemplate;
using v8::HandleScope;
using v8::IndexedPropertyHandlerConfiguration;
using v8::Integer;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Maybe;
@ -96,8 +97,8 @@ Local<Name> Uint32ToName(Local<Context> context, uint32_t index) {
ContextifyContext::ContextifyContext(
Environment* env,
Local<Object> sandbox_obj, Local<Object> options_obj) : env_(env) {
Local<Context> v8_context = CreateV8Context(env, sandbox_obj, options_obj);
Local<Object> sandbox_obj, const ContextOptions& options) : env_(env) {
Local<Context> v8_context = CreateV8Context(env, sandbox_obj, options);
context_.Reset(env->isolate(), v8_context);
// Allocation failure or maximum call stack size reached
@ -129,7 +130,7 @@ Local<Value> ContextifyContext::CreateDataWrapper(Environment* env) {
Local<Context> ContextifyContext::CreateV8Context(
Environment* env,
Local<Object> sandbox_obj,
Local<Object> options_obj) {
const ContextOptions& options) {
EscapableHandleScope scope(env->isolate());
Local<FunctionTemplate> function_template =
FunctionTemplate::New(env->isolate());
@ -179,45 +180,15 @@ Local<Context> ContextifyContext::CreateV8Context(
env->contextify_global_private_symbol(),
ctx->Global());
Local<Value> name =
options_obj->Get(env->context(), env->name_string())
.ToLocalChecked();
CHECK(name->IsString());
Utf8Value name_val(env->isolate(), name);
Local<Value> codegen = options_obj->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "codeGeneration"))
.ToLocalChecked();
if (!codegen->IsUndefined()) {
CHECK(codegen->IsObject());
Local<Object> codegen_obj = codegen.As<Object>();
Local<Value> allow_code_gen_from_strings =
codegen_obj->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "strings"))
.ToLocalChecked();
ctx->AllowCodeGenerationFromStrings(
allow_code_gen_from_strings->IsUndefined() ||
allow_code_gen_from_strings->IsTrue());
Local<Value> allow_wasm_code_gen = codegen_obj->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "wasm"))
.ToLocalChecked();
ctx->SetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration,
Boolean::New(env->isolate(), allow_wasm_code_gen->IsUndefined() ||
allow_wasm_code_gen->IsTrue()));
}
Utf8Value name_val(env->isolate(), options.name);
ctx->AllowCodeGenerationFromStrings(options.allow_code_gen_strings->IsTrue());
ctx->SetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration,
options.allow_code_gen_wasm);
ContextInfo info(*name_val);
Local<Value> origin =
options_obj->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "origin"))
.ToLocalChecked();
if (!origin->IsUndefined()) {
CHECK(origin->IsString());
Utf8Value origin_val(env->isolate(), origin);
if (!options.origin.IsEmpty()) {
Utf8Value origin_val(env->isolate(), options.origin);
info.origin = *origin_val;
}
@ -238,12 +209,12 @@ void ContextifyContext::Init(Environment* env, Local<Object> target) {
}
// makeContext(sandbox, name, origin, strings, wasm);
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsObject()) {
return env->ThrowTypeError("sandbox argument must be an object.");
}
CHECK_EQ(args.Length(), 5);
CHECK(args[0]->IsObject());
Local<Object> sandbox = args[0].As<Object>();
// Don't allow contextifying a sandbox multiple times.
@ -252,8 +223,21 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
env->context(),
env->contextify_context_private_symbol()).FromJust());
Local<Object> options = args[1].As<Object>();
CHECK(options->IsObject());
ContextOptions options;
CHECK(args[1]->IsString());
options.name = args[1].As<String>();
CHECK(args[2]->IsString() || args[2]->IsUndefined());
if (args[2]->IsString()) {
options.origin = args[2].As<String>();
}
CHECK(args[3]->IsBoolean());
options.allow_code_gen_strings = args[3].As<Boolean>();
CHECK(args[4]->IsBoolean());
options.allow_code_gen_wasm = args[4].As<Boolean>();
TryCatch try_catch(env->isolate());
ContextifyContext* context = new ContextifyContext(env, sandbox, options);
@ -277,7 +261,6 @@ void ContextifyContext::IsContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsObject());
Local<Object> sandbox = args[0].As<Object>();
Maybe<bool> result =
@ -729,101 +712,6 @@ MaybeLocal<Context> GetContextArg(Environment* env,
return context;
}
namespace {
Maybe<bool> GetDisplayErrorsArg(Environment* env,
Local<Value> options) {
if (options->IsUndefined() || options->IsString()) {
return Just(true);
}
if (!options->IsObject()) {
env->ThrowTypeError("options must be an object");
return Nothing<bool>();
}
Local<String> key = FIXED_ONE_BYTE_STRING(env->isolate(), "displayErrors");
MaybeLocal<Value> maybe_value =
options.As<Object>()->Get(env->context(), key);
if (maybe_value.IsEmpty())
return Nothing<bool>();
Local<Value> value = maybe_value.ToLocalChecked();
if (value->IsUndefined())
return Just(true);
return value->BooleanValue(env->context());
}
MaybeLocal<String> GetFilenameArg(Environment* env,
Local<Value> options) {
Local<String> defaultFilename =
FIXED_ONE_BYTE_STRING(env->isolate(), "evalmachine.<anonymous>");
if (options->IsUndefined()) {
return defaultFilename;
}
if (options->IsString()) {
return options.As<String>();
}
if (!options->IsObject()) {
env->ThrowTypeError("options must be an object");
return Local<String>();
}
Local<String> key = FIXED_ONE_BYTE_STRING(env->isolate(), "filename");
MaybeLocal<Value> maybe_value =
options.As<Object>()->Get(env->context(), key);
if (maybe_value.IsEmpty())
return MaybeLocal<String>();
Local<Value> value = maybe_value.ToLocalChecked();
if (value->IsUndefined())
return defaultFilename;
return value->ToString(env->context());
}
MaybeLocal<Uint8Array> GetCachedData(Environment* env,
Local<Value> options) {
if (!options->IsObject()) {
return MaybeLocal<Uint8Array>();
}
MaybeLocal<Value> maybe_value =
options.As<Object>()->Get(env->context(), env->cached_data_string());
if (maybe_value.IsEmpty())
return MaybeLocal<Uint8Array>();
Local<Value> value = maybe_value.ToLocalChecked();
if (value->IsUndefined()) {
return MaybeLocal<Uint8Array>();
}
if (!value->IsUint8Array()) {
env->ThrowTypeError("options.cachedData must be a Buffer instance");
return MaybeLocal<Uint8Array>();
}
return value.As<Uint8Array>();
}
Maybe<bool> GetProduceCachedData(Environment* env,
Local<Value> options) {
if (!options->IsObject()) {
return Just(false);
}
MaybeLocal<Value> maybe_value =
options.As<Object>()->Get(env->context(),
env->produce_cached_data_string());
if (maybe_value.IsEmpty())
return Nothing<bool>();
Local<Value> value = maybe_value.ToLocalChecked();
return Just(value->IsTrue());
}
} // anonymous namespace
class ContextifyScript : public BaseObject {
private:
Persistent<UnboundScript> script_;
@ -855,48 +743,67 @@ class ContextifyScript : public BaseObject {
}
// args: code, [options]
static void New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
if (!args.IsConstructCall()) {
return env->ThrowError("Must call vm.Script as a constructor.");
CHECK(args.IsConstructCall());
const int argc = args.Length();
CHECK_GE(argc, 2);
CHECK(args[0]->IsString());
Local<String> code = args[0].As<String>();
CHECK(args[1]->IsString());
Local<String> filename = args[1].As<String>();
Local<Integer> line_offset;
Local<Integer> column_offset;
Local<Uint8Array> cached_data_buf;
bool produce_cached_data = false;
Local<Context> parsing_context = context;
if (argc > 2) {
// new ContextifyScript(code, filename, lineOffset, columnOffset
// cachedData, produceCachedData, parsingContext)
CHECK_EQ(argc, 7);
CHECK(args[2]->IsNumber());
line_offset = args[2].As<Integer>();
CHECK(args[3]->IsNumber());
column_offset = args[3].As<Integer>();
if (!args[4]->IsUndefined()) {
CHECK(args[4]->IsUint8Array());
cached_data_buf = args[4].As<Uint8Array>();
}
CHECK(args[5]->IsBoolean());
produce_cached_data = args[5]->IsTrue();
if (!args[6]->IsUndefined()) {
CHECK(args[6]->IsObject());
ContextifyContext* sandbox =
ContextifyContext::ContextFromContextifiedSandbox(
env, args[6].As<Object>());
CHECK_NE(sandbox, nullptr);
parsing_context = sandbox->context();
}
} else {
line_offset = Integer::New(isolate, 0);
column_offset = Integer::New(isolate, 0);
}
ContextifyScript* contextify_script =
new ContextifyScript(env, args.This());
TryCatch try_catch(env->isolate());
Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env);
Local<String> code =
args[0]->ToString(env->context()).FromMaybe(Local<String>());
Local<Value> options = args[1];
MaybeLocal<String> filename = GetFilenameArg(env, options);
MaybeLocal<Integer> lineOffset = GetLineOffsetArg(env, options);
MaybeLocal<Integer> columnOffset = GetColumnOffsetArg(env, options);
MaybeLocal<Uint8Array> cached_data_buf = GetCachedData(env, options);
Maybe<bool> maybe_produce_cached_data = GetProduceCachedData(env, options);
MaybeLocal<Context> maybe_context = GetContextArg(env, options);
if (try_catch.HasCaught()) {
no_abort_scope.Close();
try_catch.ReThrow();
return;
}
bool produce_cached_data = maybe_produce_cached_data.ToChecked();
ScriptCompiler::CachedData* cached_data = nullptr;
Local<Uint8Array> ui8;
if (cached_data_buf.ToLocal(&ui8)) {
ArrayBuffer::Contents contents = ui8->Buffer()->GetContents();
if (!cached_data_buf.IsEmpty()) {
ArrayBuffer::Contents contents = cached_data_buf->Buffer()->GetContents();
uint8_t* data = static_cast<uint8_t*>(contents.Data());
cached_data = new ScriptCompiler::CachedData(
static_cast<uint8_t*>(contents.Data()) + ui8->ByteOffset(),
ui8->ByteLength());
data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength());
}
ScriptOrigin origin(filename.ToLocalChecked(), lineOffset.ToLocalChecked(),
columnOffset.ToLocalChecked());
ScriptOrigin origin(filename, line_offset, column_offset);
ScriptCompiler::Source source(code, origin, cached_data);
ScriptCompiler::CompileOptions compile_options =
ScriptCompiler::kNoCompileOptions;
@ -904,10 +811,12 @@ class ContextifyScript : public BaseObject {
if (source.GetCachedData() != nullptr)
compile_options = ScriptCompiler::kConsumeCodeCache;
Context::Scope scope(maybe_context.FromMaybe(env->context()));
TryCatch try_catch(isolate);
Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env);
Context::Scope scope(parsing_context);
MaybeLocal<UnboundScript> v8_script = ScriptCompiler::CompileUnboundScript(
env->isolate(),
isolate,
&source,
compile_options);
@ -917,13 +826,12 @@ class ContextifyScript : public BaseObject {
try_catch.ReThrow();
return;
}
contextify_script->script_.Reset(env->isolate(),
v8_script.ToLocalChecked());
contextify_script->script_.Reset(isolate, v8_script.ToLocalChecked());
if (compile_options == ScriptCompiler::kConsumeCodeCache) {
args.This()->Set(
env->cached_data_rejected_string(),
Boolean::New(env->isolate(), source.GetCachedData()->rejected));
Boolean::New(isolate, source.GetCachedData()->rejected));
} else if (produce_cached_data) {
const ScriptCompiler::CachedData* cached_data =
ScriptCompiler::CreateCodeCache(v8_script.ToLocalChecked(), code);
@ -937,7 +845,7 @@ class ContextifyScript : public BaseObject {
}
args.This()->Set(
env->cached_data_produced_string(),
Boolean::New(env->isolate(), cached_data_produced));
Boolean::New(isolate, cached_data_produced));
}
}
@ -948,86 +856,55 @@ class ContextifyScript : public BaseObject {
}
// args: [options]
static void RunInThisContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
// Assemble arguments
TryCatch try_catch(args.GetIsolate());
Maybe<int64_t> maybe_timeout = GetTimeoutArg(env, args[0]);
Maybe<bool> maybe_display_errors = GetDisplayErrorsArg(env, args[0]);
Maybe<bool> maybe_break_on_sigint = GetBreakOnSigintArg(env, args[0]);
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}
CHECK_EQ(args.Length(), 3);
int64_t timeout = maybe_timeout.ToChecked();
bool display_errors = maybe_display_errors.ToChecked();
bool break_on_sigint = maybe_break_on_sigint.ToChecked();
CHECK(args[0]->IsNumber());
int64_t timeout = args[0]->IntegerValue(env->context()).FromJust();
CHECK(args[1]->IsBoolean());
bool display_errors = args[1]->IsTrue();
CHECK(args[2]->IsBoolean());
bool break_on_sigint = args[2]->IsTrue();
// Do the eval within this context
EvalMachine(env, timeout, display_errors, break_on_sigint, args,
&try_catch);
EvalMachine(env, timeout, display_errors, break_on_sigint, args);
}
// args: sandbox, [options]
static void RunInContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
int64_t timeout;
bool display_errors;
bool break_on_sigint;
// Assemble arguments
if (!args[0]->IsObject()) {
return env->ThrowTypeError(
"contextifiedSandbox argument must be an object.");
}
CHECK_EQ(args.Length(), 4);
CHECK(args[0]->IsObject());
Local<Object> sandbox = args[0].As<Object>();
{
TryCatch try_catch(env->isolate());
Maybe<int64_t> maybe_timeout = GetTimeoutArg(env, args[1]);
Maybe<bool> maybe_display_errors = GetDisplayErrorsArg(env, args[1]);
Maybe<bool> maybe_break_on_sigint = GetBreakOnSigintArg(env, args[1]);
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}
timeout = maybe_timeout.ToChecked();
display_errors = maybe_display_errors.ToChecked();
break_on_sigint = maybe_break_on_sigint.ToChecked();
}
// Get the context from the sandbox
ContextifyContext* contextify_context =
ContextifyContext::ContextFromContextifiedSandbox(env, sandbox);
if (contextify_context == nullptr) {
return env->ThrowTypeError(
"sandbox argument must have been converted to a context.");
}
CHECK_NE(contextify_context, nullptr);
if (contextify_context->context().IsEmpty())
return;
{
TryCatch try_catch(env->isolate());
// Do the eval within the context
Context::Scope context_scope(contextify_context->context());
EvalMachine(contextify_context->env(),
timeout,
display_errors,
break_on_sigint,
args,
&try_catch);
CHECK(args[1]->IsNumber());
int64_t timeout = args[1]->IntegerValue(env->context()).FromJust();
if (try_catch.HasCaught()) {
try_catch.ReThrow();
return;
}
}
CHECK(args[2]->IsBoolean());
bool display_errors = args[2]->IsTrue();
CHECK(args[3]->IsBoolean());
bool break_on_sigint = args[3]->IsTrue();
// Do the eval within the context
Context::Scope context_scope(contextify_context->context());
EvalMachine(contextify_context->env(),
timeout,
display_errors,
break_on_sigint,
args);
}
static void DecorateErrorStack(Environment* env, const TryCatch& try_catch) {
@ -1072,14 +949,13 @@ class ContextifyScript : public BaseObject {
const int64_t timeout,
const bool display_errors,
const bool break_on_sigint,
const FunctionCallbackInfo<Value>& args,
TryCatch* try_catch) {
const FunctionCallbackInfo<Value>& args) {
if (!ContextifyScript::InstanceOf(env, args.Holder())) {
env->ThrowTypeError(
"Script methods can only be called on script instances.");
return false;
}
TryCatch try_catch(env->isolate());
ContextifyScript* wrapped_script;
ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder(), false);
Local<UnboundScript> unbound_script =
@ -1115,10 +991,10 @@ class ContextifyScript : public BaseObject {
env->isolate()->CancelTerminateExecution();
}
if (try_catch->HasCaught()) {
if (try_catch.HasCaught()) {
if (!timed_out && !received_signal && display_errors) {
// We should decorate non-termination exceptions
DecorateErrorStack(env, *try_catch);
DecorateErrorStack(env, try_catch);
}
// If there was an exception thrown during script execution, re-throw it.
@ -1126,7 +1002,7 @@ class ContextifyScript : public BaseObject {
// letting try_catch catch it.
// If execution has been terminated, but not by one of the watchdogs from
// this invocation, this will re-throw a `null` value.
try_catch->ReThrow();
try_catch.ReThrow();
return false;
}

View File

@ -9,6 +9,13 @@
namespace node {
namespace contextify {
struct ContextOptions {
v8::Local<v8::String> name;
v8::Local<v8::String> origin;
v8::Local<v8::Boolean> allow_code_gen_strings;
v8::Local<v8::Boolean> allow_code_gen_wasm;
};
class ContextifyContext {
protected:
Environment* const env_;
@ -17,11 +24,11 @@ class ContextifyContext {
public:
ContextifyContext(Environment* env,
v8::Local<v8::Object> sandbox_obj,
v8::Local<v8::Object> options_obj);
const ContextOptions& options);
v8::Local<v8::Value> CreateDataWrapper(Environment* env);
v8::Local<v8::Context> CreateV8Context(Environment* env,
v8::Local<v8::Object> sandbox_obj, v8::Local<v8::Object> options_obj);
v8::Local<v8::Object> sandbox_obj, const ContextOptions& options);
static void Init(Environment* env, v8::Local<v8::Object> target);
static bool AllowWasmCodeGeneration(

View File

@ -24,60 +24,85 @@ const common = require('../common');
const assert = require('assert');
const vm = require('vm');
// Test 1: vm.runInNewContext
const sandbox = {};
let result = vm.runInNewContext(
'foo = "bar"; this.typeofProcess = typeof process; typeof Object;',
sandbox
);
assert.deepStrictEqual(sandbox, {
foo: 'bar',
typeofProcess: 'undefined',
});
assert.strictEqual(result, 'function');
// vm.runInNewContext
{
const sandbox = {};
const result = vm.runInNewContext(
'foo = "bar"; this.typeofProcess = typeof process; typeof Object;',
sandbox
);
assert.deepStrictEqual(sandbox, {
foo: 'bar',
typeofProcess: 'undefined',
});
assert.strictEqual(result, 'function');
}
// Test 2: vm.runInContext
const sandbox2 = { foo: 'bar' };
const context = vm.createContext(sandbox2);
result = vm.runInContext(
'baz = foo; this.typeofProcess = typeof process; typeof Object;',
context
);
assert.deepStrictEqual(sandbox2, {
foo: 'bar',
baz: 'bar',
typeofProcess: 'undefined'
});
assert.strictEqual(result, 'function');
// vm.runInContext
{
const sandbox = { foo: 'bar' };
const context = vm.createContext(sandbox);
const result = vm.runInContext(
'baz = foo; this.typeofProcess = typeof process; typeof Object;',
context
);
assert.deepStrictEqual(sandbox, {
foo: 'bar',
baz: 'bar',
typeofProcess: 'undefined'
});
assert.strictEqual(result, 'function');
}
// Test 3: vm.runInThisContext
result = vm.runInThisContext(
'vmResult = "foo"; Object.prototype.toString.call(process);'
);
assert.strictEqual(global.vmResult, 'foo');
assert.strictEqual(result, '[object process]');
delete global.vmResult;
// vm.runInThisContext
{
const result = vm.runInThisContext(
'vmResult = "foo"; Object.prototype.toString.call(process);'
);
assert.strictEqual(global.vmResult, 'foo');
assert.strictEqual(result, '[object process]');
delete global.vmResult;
}
// Test 4: vm.runInNewContext
result = vm.runInNewContext(
'vmResult = "foo"; typeof process;'
);
assert.strictEqual(global.vmResult, undefined);
assert.strictEqual(result, 'undefined');
// vm.runInNewContext
{
const result = vm.runInNewContext(
'vmResult = "foo"; typeof process;'
);
assert.strictEqual(global.vmResult, undefined);
assert.strictEqual(result, 'undefined');
}
// Test 5: vm.createContext
const sandbox3 = {};
const context2 = vm.createContext(sandbox3);
assert.strictEqual(sandbox3, context2);
// vm.createContext
{
const sandbox = {};
const context = vm.createContext(sandbox);
assert.strictEqual(sandbox, context);
}
// Test 6: invalid arguments
// Run script with filename
{
const script = 'throw new Error("boom")';
const filename = 'test-boom-error';
const context = vm.createContext();
function checkErr(err) {
return err.stack.startsWith('test-boom-error:1');
}
assert.throws(() => vm.runInContext(script, context, filename), checkErr);
assert.throws(() => vm.runInNewContext(script, context, filename), checkErr);
assert.throws(() => vm.runInThisContext(script, filename), checkErr);
}
// Invalid arguments
[null, 'string'].forEach((input) => {
common.expectsError(() => {
vm.createContext({}, input);
}, {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "options" argument must be of type object. ' +
message: 'The "options" argument must be of type Object. ' +
`Received type ${typeof input}`
});
});

View File

@ -1,5 +1,5 @@
'use strict';
require('../common');
const common = require('../common');
const assert = require('assert');
const vm = require('vm');
const spawnSync = require('child_process').spawnSync;
@ -84,8 +84,12 @@ function testRejectSlice() {
testRejectSlice();
// It should throw on non-Buffer cachedData
assert.throws(() => {
common.expectsError(() => {
new vm.Script('function abc() {}', {
cachedData: 'ohai'
});
}, /^TypeError: options\.cachedData must be a Buffer instance$/);
}, {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: /must be one of type Buffer or Uint8Array/
});

View File

@ -64,19 +64,25 @@ try {
// This is outside of catch block to confirm catch block ran.
assert.strictEqual(gh1140Exception.toString(), 'Error');
// GH-558, non-context argument segfaults / raises assertion
const nonContextualSandboxErrorMsg =
/^TypeError: contextifiedSandbox argument must be an object\.$/;
const contextifiedSandboxErrorMsg =
/^TypeError: sandbox argument must have been converted to a context\.$/;
const nonContextualSandboxError = {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: /must be of type Object/
};
const contextifiedSandboxError = {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: /must be of type vm\.Context/
};
[
[undefined, nonContextualSandboxErrorMsg],
[null, nonContextualSandboxErrorMsg], [0, nonContextualSandboxErrorMsg],
[0.0, nonContextualSandboxErrorMsg], ['', nonContextualSandboxErrorMsg],
[{}, contextifiedSandboxErrorMsg], [[], contextifiedSandboxErrorMsg]
[undefined, nonContextualSandboxError],
[null, nonContextualSandboxError], [0, nonContextualSandboxError],
[0.0, nonContextualSandboxError], ['', nonContextualSandboxError],
[{}, contextifiedSandboxError], [[], contextifiedSandboxError]
].forEach((e) => {
assert.throws(() => { script.runInContext(e[0]); }, e[1]);
assert.throws(() => { vm.runInContext('', e[0]); }, e[1]);
common.expectsError(() => { script.runInContext(e[0]); }, e[1]);
common.expectsError(() => { vm.runInContext('', e[0]); }, e[1]);
});
// Issue GH-693:

View File

@ -35,13 +35,6 @@ for (const valToTest of [
});
}
common.expectsError(() => {
vm.isContext();
}, {
code: 'ERR_MISSING_ARGS',
type: TypeError
});
assert.strictEqual(vm.isContext({}), false);
assert.strictEqual(vm.isContext([]), false);

View File

@ -0,0 +1,78 @@
'use strict';
const common = require('../common');
const vm = require('vm');
const invalidArgType = {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE'
};
const outOfRange = {
type: RangeError,
code: 'ERR_OUT_OF_RANGE'
};
common.expectsError(() => {
new vm.Script('void 0', 42);
}, invalidArgType);
[null, {}, [1], 'bad', true, 0.1].forEach((value) => {
common.expectsError(() => {
new vm.Script('void 0', { lineOffset: value });
}, invalidArgType);
common.expectsError(() => {
new vm.Script('void 0', { columnOffset: value });
}, invalidArgType);
});
common.expectsError(() => {
new vm.Script('void 0', { lineOffset: Number.MAX_SAFE_INTEGER });
}, outOfRange);
common.expectsError(() => {
new vm.Script('void 0', { columnOffset: Number.MAX_SAFE_INTEGER });
}, outOfRange);
common.expectsError(() => {
new vm.Script('void 0', { filename: 123 });
}, invalidArgType);
common.expectsError(() => {
new vm.Script('void 0', { produceCachedData: 1 });
}, invalidArgType);
[[0], {}, true, 'bad', 42].forEach((value) => {
common.expectsError(() => {
new vm.Script('void 0', { cachedData: value });
}, invalidArgType);
});
{
const script = new vm.Script('void 0');
const sandbox = vm.createContext();
function assertErrors(options) {
common.expectsError(() => {
script.runInThisContext(options);
}, invalidArgType);
common.expectsError(() => {
script.runInContext(sandbox, options);
}, invalidArgType);
common.expectsError(() => {
script.runInNewContext({}, options);
}, invalidArgType);
}
[null, 'bad', 42].forEach(assertErrors);
[{}, [1], 'bad', null, -1, 0, NaN].forEach((value) => {
assertErrors({ timeout: value });
});
[{}, [1], 'bad', 1, null].forEach((value) => {
assertErrors({ displayErrors: value });
assertErrors({ breakOnSigint: value });
});
}

View File

@ -24,25 +24,15 @@ require('../common');
const assert = require('assert');
const vm = require('vm');
// Test 1: Timeout of 100ms executing endless loop
// Timeout of 100ms executing endless loop
assert.throws(function() {
vm.runInThisContext('while(true) {}', { timeout: 100 });
}, /^Error: Script execution timed out\.$/);
// Test 2: Timeout must be >= 0ms
assert.throws(function() {
vm.runInThisContext('', { timeout: -1 });
}, /^RangeError: timeout must be a positive number$/);
// Test 3: Timeout of 0ms
assert.throws(function() {
vm.runInThisContext('', { timeout: 0 });
}, /^RangeError: timeout must be a positive number$/);
// Test 4: Timeout of 1000ms, script finishes first
// Timeout of 1000ms, script finishes first
vm.runInThisContext('', { timeout: 1000 });
// Test 5: Nested vm timeouts, inner timeout propagates out
// Nested vm timeouts, inner timeout propagates out
assert.throws(function() {
const context = {
log: console.log,
@ -54,7 +44,7 @@ assert.throws(function() {
throw new Error('Test 5 failed');
}, /Script execution timed out\./);
// Test 6: Nested vm timeouts, outer timeout is shorter and fires first.
// Nested vm timeouts, outer timeout is shorter and fires first.
assert.throws(function() {
const context = {
runInVM: function(timeout) {
@ -65,7 +55,7 @@ assert.throws(function() {
throw new Error('Test 6 failed');
}, /Script execution timed out\./);
// Test 7: Nested vm timeouts, inner script throws an error.
// Nested vm timeouts, inner script throws an error.
assert.throws(function() {
const context = {
runInVM: function(timeout) {