diff options
-rw-r--r-- | doc/api/vm.md | 3 | ||||
-rw-r--r-- | lib/internal/bootstrap/loaders.js | 13 | ||||
-rw-r--r-- | lib/internal/vm/module.js | 4 | ||||
-rw-r--r-- | lib/vm.js | 221 | ||||
-rw-r--r-- | src/env.h | 1 | ||||
-rw-r--r-- | src/node_contextify.cc | 362 | ||||
-rw-r--r-- | src/node_contextify.h | 11 | ||||
-rw-r--r-- | test/parallel/test-vm-basic.js | 111 | ||||
-rw-r--r-- | test/parallel/test-vm-cached-data.js | 10 | ||||
-rw-r--r-- | test/parallel/test-vm-context.js | 28 | ||||
-rw-r--r-- | test/parallel/test-vm-is-context.js | 7 | ||||
-rw-r--r-- | test/parallel/test-vm-options-validation.js | 78 | ||||
-rw-r--r-- | test/parallel/test-vm-timeout.js | 20 |
13 files changed, 458 insertions, 411 deletions
diff --git a/doc/api/vm.md b/doc/api/vm.md index 25efc85cf6..3712fa4f2b 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -619,6 +619,9 @@ console.log(globalVar); 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. --> diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index fff82cd0e0..828370f6ea 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -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; diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js index 9af071ce28..a64ccfe3b2 100644 --- a/lib/internal/vm/module.js +++ b/lib/internal/vm/module.js @@ -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 { @@ -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 */ } } -} -const realRunInThisContext = Script.prototype.runInThisContext; -const realRunInContext = Script.prototype.runInContext; + runInThisContext(options) { + const { breakOnSigint, args } = getRunInContextArgs(options); + if (breakOnSigint && process.listenerCount('SIGINT') > 0) { + return sigintHandlersWrap(super.runInThisContext, this, args); + } else { + return super.runInThisContext(...args); + } + } -Script.prototype.runInThisContext = function(options) { - if (options && options.breakOnSigint && process.listenerCount('SIGINT') > 0) { - return sigintHandlersWrap(realRunInThisContext, this, [options]); - } else { - return realRunInThisContext.call(this, options); + 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); + } } -}; -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); + runInNewContext(sandbox, options) { + const context = createContext(sandbox, getContextOptions(options)); + return this.runInContext(context, options); } -}; +} -Script.prototype.runInNewContext = function(sandbox, options) { - const context = createContext(sandbox, getContextOptions(options)); - return this.runInContext(context, options); -}; +function validateContext(sandbox) { + if (typeof sandbox !== 'object' || sandbox === null) { + throw new ERR_INVALID_ARG_TYPE('contextifiedSandbox', 'Object', sandbox); + } + if (!_isContext(sandbox)) { + throw new ERR_INVALID_ARG_TYPE('contextifiedSandbox', 'vm.Context', + sandbox); + } +} + +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); } @@ -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") \ diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 42f66885bb..c71e9b5789 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -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); - - if (!args.IsConstructCall()) { - return env->ThrowError("Must call vm.Script as a constructor."); + Isolate* isolate = env->isolate(); + Local<Context> context = env->context(); + + 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); + + CHECK(args[0]->IsNumber()); + int64_t timeout = args[0]->IntegerValue(env->context()).FromJust(); + + CHECK(args[1]->IsBoolean()); + bool display_errors = args[1]->IsTrue(); - int64_t timeout = maybe_timeout.ToChecked(); - bool display_errors = maybe_display_errors.ToChecked(); - bool break_on_sigint = maybe_break_on_sigint.ToChecked(); + 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); - - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } - } + CHECK(args[1]->IsNumber()); + int64_t timeout = args[1]->IntegerValue(env->context()).FromJust(); + + 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; } diff --git a/src/node_contextify.h b/src/node_contextify.h index cf3e6452fd..1075bc5c68 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -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( diff --git a/test/parallel/test-vm-basic.js b/test/parallel/test-vm-basic.js index bf1532cacc..2dfc1e9cb7 100644 --- a/test/parallel/test-vm-basic.js +++ b/test/parallel/test-vm-basic.js @@ -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'); +} + +// 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; +} + +// vm.runInNewContext +{ + const result = vm.runInNewContext( + 'vmResult = "foo"; typeof process;' + ); + assert.strictEqual(global.vmResult, undefined); + assert.strictEqual(result, 'undefined'); +} + +// vm.createContext +{ + const sandbox = {}; + const context = vm.createContext(sandbox); + assert.strictEqual(sandbox, context); +} -// 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; +// Run script with filename +{ + const script = 'throw new Error("boom")'; + const filename = 'test-boom-error'; + const context = vm.createContext(); -// Test 4: vm.runInNewContext -result = vm.runInNewContext( - 'vmResult = "foo"; typeof process;' -); -assert.strictEqual(global.vmResult, undefined); -assert.strictEqual(result, 'undefined'); + function checkErr(err) { + return err.stack.startsWith('test-boom-error:1'); + } -// Test 5: vm.createContext -const sandbox3 = {}; -const context2 = vm.createContext(sandbox3); -assert.strictEqual(sandbox3, context2); + assert.throws(() => vm.runInContext(script, context, filename), checkErr); + assert.throws(() => vm.runInNewContext(script, context, filename), checkErr); + assert.throws(() => vm.runInThisContext(script, filename), checkErr); +} -// Test 6: invalid arguments +// 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}` }); }); diff --git a/test/parallel/test-vm-cached-data.js b/test/parallel/test-vm-cached-data.js index 5302d73c2d..e3947d1ae6 100644 --- a/test/parallel/test-vm-cached-data.js +++ b/test/parallel/test-vm-cached-data.js @@ -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/ +}); diff --git a/test/parallel/test-vm-context.js b/test/parallel/test-vm-context.js index fd6a66f392..31ed69da93 100644 --- a/test/parallel/test-vm-context.js +++ b/test/parallel/test-vm-context.js @@ -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: diff --git a/test/parallel/test-vm-is-context.js b/test/parallel/test-vm-is-context.js index a762622c67..2749a14834 100644 --- a/test/parallel/test-vm-is-context.js +++ b/test/parallel/test-vm-is-context.js @@ -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); diff --git a/test/parallel/test-vm-options-validation.js b/test/parallel/test-vm-options-validation.js new file mode 100644 index 0000000000..e00a14e4b9 --- /dev/null +++ b/test/parallel/test-vm-options-validation.js @@ -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 }); + }); +} diff --git a/test/parallel/test-vm-timeout.js b/test/parallel/test-vm-timeout.js index 51239ffed5..859992e99b 100644 --- a/test/parallel/test-vm-timeout.js +++ b/test/parallel/test-vm-timeout.js @@ -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) { |