From ed99b5c9e192aab0041cce008a50c985db70c944 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 9 Dec 2017 10:32:34 -0600 Subject: [PATCH 01/12] src: flatten ContextifyContext Flattens ContextifyContext allows the context interface to be used in other parts of the code base. --- node.gyp | 1 + src/node_contextify.cc | 852 ++++++++++++++++++++--------------------- src/node_contextify.h | 98 +++++ 3 files changed, 516 insertions(+), 435 deletions(-) create mode 100644 src/node_contextify.h diff --git a/node.gyp b/node.gyp index 6a4dfe0e434319..665673d268744b 100644 --- a/node.gyp +++ b/node.gyp @@ -348,6 +348,7 @@ 'src/node.h', 'src/node_buffer.h', 'src/node_constants.h', + 'src/node_contextify.h', 'src/node_debug_options.h', 'src/node_file.h', 'src/node_http2.h', diff --git a/src/node_contextify.cc b/src/node_contextify.cc index db907c63336cf7..c50cb19529c13d 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -22,8 +22,10 @@ #include "node_internals.h" #include "node_watchdog.h" #include "base_object-inl.h" +#include "node_contextify.h" namespace node { +namespace contextify { using v8::Array; using v8::ArrayBuffer; @@ -90,519 +92,499 @@ Local Uint32ToName(Local context, uint32_t index) { .ToLocalChecked(); } -class ContextifyContext { - protected: - // V8 reserves the first field in context objects for the debugger. We use the - // second field to hold a reference to the sandbox object. - enum { kSandboxObjectIndex = 1 }; - - Environment* const env_; - Persistent context_; - - public: - ContextifyContext(Environment* env, - Local sandbox_obj, - Local options_obj) - : env_(env) { - Local v8_context = CreateV8Context(env, sandbox_obj, options_obj); - context_.Reset(env->isolate(), v8_context); - - // Allocation failure or maximum call stack size reached - if (context_.IsEmpty()) - return; - context_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); - context_.MarkIndependent(); - } - - - ~ContextifyContext() { - context_.Reset(); - } - +} // anonymous namespace - inline Environment* env() const { - return env_; - } +ContextifyContext::ContextifyContext( + Environment* env, + Local sandbox_obj, Local options_obj) : env_(env) { + Local v8_context = CreateV8Context(env, sandbox_obj, options_obj); + context_.Reset(env->isolate(), v8_context); + + // Allocation failure or maximum call stack size reached + if (context_.IsEmpty()) + return; + context_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); + context_.MarkIndependent(); +} - inline Local context() const { - return PersistentToLocal(env()->isolate(), context_); - } +ContextifyContext::~ContextifyContext() { + context_.Reset(); +} - inline Local global_proxy() const { - return context()->Global(); - } +// This is an object that just keeps an internal pointer to this +// ContextifyContext. It's passed to the NamedPropertyHandler. If we +// pass the main JavaScript context object we're embedded in, then the +// NamedPropertyHandler will store a reference to it forever and keep it +// from getting gc'd. +Local ContextifyContext::CreateDataWrapper(Environment* env) { + EscapableHandleScope scope(env->isolate()); + Local wrapper = + env->script_data_constructor_function() + ->NewInstance(env->context()).FromMaybe(Local()); + if (wrapper.IsEmpty()) + return scope.Escape(Local::New(env->isolate(), Local())); + + Wrap(wrapper, this); + return scope.Escape(wrapper); +} - inline Local sandbox() const { - return Local::Cast(context()->GetEmbedderData(kSandboxObjectIndex)); +Local ContextifyContext::CreateV8Context( + Environment* env, + Local sandbox_obj, + Local options_obj) { + EscapableHandleScope scope(env->isolate()); + Local function_template = + FunctionTemplate::New(env->isolate()); + + function_template->SetClassName(sandbox_obj->GetConstructorName()); + + Local object_template = + function_template->InstanceTemplate(); + + NamedPropertyHandlerConfiguration config(PropertyGetterCallback, + PropertySetterCallback, + PropertyDescriptorCallback, + PropertyDeleterCallback, + PropertyEnumeratorCallback, + PropertyDefinerCallback, + CreateDataWrapper(env)); + + IndexedPropertyHandlerConfiguration indexed_config( + IndexedPropertyGetterCallback, + IndexedPropertySetterCallback, + IndexedPropertyDescriptorCallback, + IndexedPropertyDeleterCallback, + PropertyEnumeratorCallback, + IndexedPropertyDefinerCallback, + CreateDataWrapper(env)); + + object_template->SetHandler(config); + object_template->SetHandler(indexed_config); + + Local ctx = NewContext(env->isolate(), object_template); + + if (ctx.IsEmpty()) { + env->ThrowError("Could not instantiate context"); + return Local(); } - // This is an object that just keeps an internal pointer to this - // ContextifyContext. It's passed to the NamedPropertyHandler. If we - // pass the main JavaScript context object we're embedded in, then the - // NamedPropertyHandler will store a reference to it forever and keep it - // from getting gc'd. - Local CreateDataWrapper(Environment* env) { - EscapableHandleScope scope(env->isolate()); - Local wrapper = - env->script_data_constructor_function() - ->NewInstance(env->context()).FromMaybe(Local()); - if (wrapper.IsEmpty()) - return scope.Escape(Local::New(env->isolate(), Local())); - - Wrap(wrapper, this); - return scope.Escape(wrapper); + ctx->SetSecurityToken(env->context()->GetSecurityToken()); + + // We need to tie the lifetime of the sandbox object with the lifetime of + // newly created context. We do this by making them hold references to each + // other. The context can directly hold a reference to the sandbox as an + // embedder data field. However, we cannot hold a reference to a v8::Context + // directly in an Object, we instead hold onto the new context's global + // object instead (which then has a reference to the context). + ctx->SetEmbedderData(kSandboxObjectIndex, sandbox_obj); + sandbox_obj->SetPrivate(env->context(), + env->contextify_global_private_symbol(), + ctx->Global()); + + Local name = + options_obj->Get(env->context(), env->name_string()) + .ToLocalChecked(); + CHECK(name->IsString()); + Utf8Value name_val(env->isolate(), name); + + ContextInfo info(*name_val); + + Local 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); + info.origin = *origin_val; } + env->AssignToContext(ctx, info); - Local CreateV8Context(Environment* env, - Local sandbox_obj, - Local options_obj) { - EscapableHandleScope scope(env->isolate()); - Local function_template = - FunctionTemplate::New(env->isolate()); - - function_template->SetClassName(sandbox_obj->GetConstructorName()); - - Local object_template = - function_template->InstanceTemplate(); - - NamedPropertyHandlerConfiguration config(PropertyGetterCallback, - PropertySetterCallback, - PropertyDescriptorCallback, - PropertyDeleterCallback, - PropertyEnumeratorCallback, - PropertyDefinerCallback, - CreateDataWrapper(env)); - - IndexedPropertyHandlerConfiguration indexed_config( - IndexedPropertyGetterCallback, - IndexedPropertySetterCallback, - IndexedPropertyDescriptorCallback, - IndexedPropertyDeleterCallback, - PropertyEnumeratorCallback, - IndexedPropertyDefinerCallback, - CreateDataWrapper(env)); - - object_template->SetHandler(config); - object_template->SetHandler(indexed_config); - - Local ctx = NewContext(env->isolate(), object_template); - - if (ctx.IsEmpty()) { - env->ThrowError("Could not instantiate context"); - return Local(); - } + return scope.Escape(ctx); +} - ctx->SetSecurityToken(env->context()->GetSecurityToken()); - - // We need to tie the lifetime of the sandbox object with the lifetime of - // newly created context. We do this by making them hold references to each - // other. The context can directly hold a reference to the sandbox as an - // embedder data field. However, we cannot hold a reference to a v8::Context - // directly in an Object, we instead hold onto the new context's global - // object instead (which then has a reference to the context). - ctx->SetEmbedderData(kSandboxObjectIndex, sandbox_obj); - sandbox_obj->SetPrivate(env->context(), - env->contextify_global_private_symbol(), - ctx->Global()); - - Local name = - options_obj->Get(env->context(), env->name_string()) - .ToLocalChecked(); - CHECK(name->IsString()); - Utf8Value name_val(env->isolate(), name); - - ContextInfo info(*name_val); - - Local 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); - info.origin = *origin_val; - } - env->AssignToContext(ctx, info); +void ContextifyContext::Init(Environment* env, Local target) { + Local function_template = + FunctionTemplate::New(env->isolate()); + function_template->InstanceTemplate()->SetInternalFieldCount(1); + env->set_script_data_constructor_function(function_template->GetFunction()); - return scope.Escape(ctx); - } + env->SetMethod(target, "makeContext", MakeContext); + env->SetMethod(target, "isContext", IsContext); +} - static void Init(Environment* env, Local target) { - Local function_template = - FunctionTemplate::New(env->isolate()); - function_template->InstanceTemplate()->SetInternalFieldCount(1); - env->set_script_data_constructor_function(function_template->GetFunction()); +void ContextifyContext::MakeContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); - env->SetMethod(target, "makeContext", MakeContext); - env->SetMethod(target, "isContext", IsContext); + if (!args[0]->IsObject()) { + return env->ThrowTypeError("sandbox argument must be an object."); } + Local sandbox = args[0].As(); + // Don't allow contextifying a sandbox multiple times. + CHECK( + !sandbox->HasPrivate( + env->context(), + env->contextify_context_private_symbol()).FromJust()); - static void MakeContext(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); + Local options = args[1].As(); + CHECK(options->IsObject()); - if (!args[0]->IsObject()) { - return env->ThrowTypeError("sandbox argument must be an object."); - } - Local sandbox = args[0].As(); + TryCatch try_catch(env->isolate()); + ContextifyContext* context = new ContextifyContext(env, sandbox, options); - // Don't allow contextifying a sandbox multiple times. - CHECK( - !sandbox->HasPrivate( - env->context(), - env->contextify_context_private_symbol()).FromJust()); + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + return; + } - Local options = args[1].As(); - CHECK(options->IsObject()); + if (context->context().IsEmpty()) + return; - TryCatch try_catch(env->isolate()); - ContextifyContext* context = new ContextifyContext(env, sandbox, options); + sandbox->SetPrivate( + env->context(), + env->contextify_context_private_symbol(), + External::New(env->isolate(), context)); +} - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } - if (context->context().IsEmpty()) - return; +void ContextifyContext::IsContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); - sandbox->SetPrivate( - env->context(), - env->contextify_context_private_symbol(), - External::New(env->isolate(), context)); + if (!args[0]->IsObject()) { + env->ThrowTypeError("sandbox must be an object"); + return; } + Local sandbox = args[0].As(); + Maybe result = + sandbox->HasPrivate(env->context(), + env->contextify_context_private_symbol()); + args.GetReturnValue().Set(result.FromJust()); +} - static void IsContext(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - if (!args[0]->IsObject()) { - env->ThrowTypeError("sandbox must be an object"); - return; - } - Local sandbox = args[0].As(); +void ContextifyContext::WeakCallback( + const WeakCallbackInfo& data) { + ContextifyContext* context = data.GetParameter(); + delete context; +} - Maybe result = - sandbox->HasPrivate(env->context(), - env->contextify_context_private_symbol()); - args.GetReturnValue().Set(result.FromJust()); +// static +ContextifyContext* ContextifyContext::ContextFromContextifiedSandbox( + Environment* env, + const Local& sandbox) { + MaybeLocal maybe_value = + sandbox->GetPrivate(env->context(), + env->contextify_context_private_symbol()); + Local context_external_v; + if (maybe_value.ToLocal(&context_external_v) && + context_external_v->IsExternal()) { + Local context_external = context_external_v.As(); + return static_cast(context_external->Value()); } + return nullptr; +} - - static void WeakCallback(const WeakCallbackInfo& data) { - ContextifyContext* context = data.GetParameter(); - delete context; +// static +void ContextifyContext::PropertyGetterCallback( + Local property, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); + + // Still initializing + if (ctx->context_.IsEmpty()) + return; + + Local context = ctx->context(); + Local sandbox = ctx->sandbox(); + MaybeLocal maybe_rv = + sandbox->GetRealNamedProperty(context, property); + if (maybe_rv.IsEmpty()) { + maybe_rv = + ctx->global_proxy()->GetRealNamedProperty(context, property); } + Local rv; + if (maybe_rv.ToLocal(&rv)) { + if (rv == sandbox) + rv = ctx->global_proxy(); - static ContextifyContext* ContextFromContextifiedSandbox( - Environment* env, - const Local& sandbox) { - MaybeLocal maybe_value = - sandbox->GetPrivate(env->context(), - env->contextify_context_private_symbol()); - Local context_external_v; - if (maybe_value.ToLocal(&context_external_v) && - context_external_v->IsExternal()) { - Local context_external = context_external_v.As(); - return static_cast(context_external->Value()); - } - return nullptr; + args.GetReturnValue().Set(rv); } +} - - static void PropertyGetterCallback( - Local property, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - - // Still initializing - if (ctx->context_.IsEmpty()) - return; - - Local context = ctx->context(); - Local sandbox = ctx->sandbox(); - MaybeLocal maybe_rv = - sandbox->GetRealNamedProperty(context, property); - if (maybe_rv.IsEmpty()) { - maybe_rv = - ctx->global_proxy()->GetRealNamedProperty(context, property); - } - - Local rv; - if (maybe_rv.ToLocal(&rv)) { - if (rv == sandbox) - rv = ctx->global_proxy(); - - args.GetReturnValue().Set(rv); - } - } - - - static void PropertySetterCallback( - Local property, - Local value, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - - // Still initializing - if (ctx->context_.IsEmpty()) - return; - - auto attributes = PropertyAttribute::None; - bool is_declared_on_global_proxy = ctx->global_proxy() - ->GetRealNamedPropertyAttributes(ctx->context(), property) - .To(&attributes); - bool read_only = - static_cast(attributes) & - static_cast(PropertyAttribute::ReadOnly); - - bool is_declared_on_sandbox = ctx->sandbox() - ->GetRealNamedPropertyAttributes(ctx->context(), property) - .To(&attributes); - read_only = read_only || - (static_cast(attributes) & - static_cast(PropertyAttribute::ReadOnly)); - - if (read_only) - return; - - // true for x = 5 - // false for this.x = 5 - // false for Object.defineProperty(this, 'foo', ...) - // false for vmResult.x = 5 where vmResult = vm.runInContext(); - bool is_contextual_store = ctx->global_proxy() != args.This(); - - // Indicator to not return before setting (undeclared) function declarations - // on the sandbox in strict mode, i.e. args.ShouldThrowOnError() = true. - // True for 'function f() {}', 'this.f = function() {}', - // 'var f = function()'. - // In effect only for 'function f() {}' because - // var f = function(), is_declared = true - // this.f = function() {}, is_contextual_store = false. - bool is_function = value->IsFunction(); - - bool is_declared = is_declared_on_global_proxy || is_declared_on_sandbox; - if (!is_declared && args.ShouldThrowOnError() && is_contextual_store && - !is_function) - return; - - if (!is_declared_on_global_proxy && is_declared_on_sandbox && - args.ShouldThrowOnError() && is_contextual_store && !is_function) { - // The property exists on the sandbox but not on the global - // proxy. Setting it would throw because we are in strict mode. - // Don't attempt to set it by signaling that the call was - // intercepted. Only change the value on the sandbox. - args.GetReturnValue().Set(false); - } - - ctx->sandbox()->Set(property, value); +// static +void ContextifyContext::PropertySetterCallback( + Local property, + Local value, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); + + // Still initializing + if (ctx->context_.IsEmpty()) + return; + + auto attributes = PropertyAttribute::None; + bool is_declared_on_global_proxy = ctx->global_proxy() + ->GetRealNamedPropertyAttributes(ctx->context(), property) + .To(&attributes); + bool read_only = + static_cast(attributes) & + static_cast(PropertyAttribute::ReadOnly); + + bool is_declared_on_sandbox = ctx->sandbox() + ->GetRealNamedPropertyAttributes(ctx->context(), property) + .To(&attributes); + read_only = read_only || + (static_cast(attributes) & + static_cast(PropertyAttribute::ReadOnly)); + + if (read_only) + return; + + // true for x = 5 + // false for this.x = 5 + // false for Object.defineProperty(this, 'foo', ...) + // false for vmResult.x = 5 where vmResult = vm.runInContext(); + bool is_contextual_store = ctx->global_proxy() != args.This(); + + // Indicator to not return before setting (undeclared) function declarations + // on the sandbox in strict mode, i.e. args.ShouldThrowOnError() = true. + // True for 'function f() {}', 'this.f = function() {}', + // 'var f = function()'. + // In effect only for 'function f() {}' because + // var f = function(), is_declared = true + // this.f = function() {}, is_contextual_store = false. + bool is_function = value->IsFunction(); + + bool is_declared = is_declared_on_global_proxy || is_declared_on_sandbox; + if (!is_declared && args.ShouldThrowOnError() && is_contextual_store && + !is_function) + return; + + if (!is_declared_on_global_proxy && is_declared_on_sandbox && + args.ShouldThrowOnError() && is_contextual_store && !is_function) { + // The property exists on the sandbox but not on the global + // proxy. Setting it would throw because we are in strict mode. + // Don't attempt to set it by signaling that the call was + // intercepted. Only change the value on the sandbox. + args.GetReturnValue().Set(false); } + ctx->sandbox()->Set(property, value); +} - static void PropertyDescriptorCallback( - Local property, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); +// static +void ContextifyContext::PropertyDescriptorCallback( + Local property, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; + // Still initializing + if (ctx->context_.IsEmpty()) + return; - Local context = ctx->context(); + Local context = ctx->context(); - Local sandbox = ctx->sandbox(); + Local sandbox = ctx->sandbox(); - if (sandbox->HasOwnProperty(context, property).FromMaybe(false)) { - args.GetReturnValue().Set( - sandbox->GetOwnPropertyDescriptor(context, property) - .ToLocalChecked()); - } + if (sandbox->HasOwnProperty(context, property).FromMaybe(false)) { + args.GetReturnValue().Set( + sandbox->GetOwnPropertyDescriptor(context, property) + .ToLocalChecked()); } +} - - static void PropertyDefinerCallback( - Local property, - const PropertyDescriptor& desc, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - - // Still initializing - if (ctx->context_.IsEmpty()) - return; - - Local context = ctx->context(); - v8::Isolate* isolate = context->GetIsolate(); - - auto attributes = PropertyAttribute::None; - bool is_declared = - ctx->global_proxy()->GetRealNamedPropertyAttributes(ctx->context(), - property) - .To(&attributes); - bool read_only = - static_cast(attributes) & - static_cast(PropertyAttribute::ReadOnly); - - // If the property is set on the global as read_only, don't change it on - // the global or sandbox. - if (is_declared && read_only) - return; - - Local sandbox = ctx->sandbox(); - - auto define_prop_on_sandbox = - [&] (PropertyDescriptor* desc_for_sandbox) { - if (desc.has_enumerable()) { - desc_for_sandbox->set_enumerable(desc.enumerable()); - } - if (desc.has_configurable()) { - desc_for_sandbox->set_configurable(desc.configurable()); - } - // Set the property on the sandbox. - sandbox->DefineProperty(context, property, *desc_for_sandbox) - .FromJust(); - }; - - if (desc.has_get() || desc.has_set()) { - PropertyDescriptor desc_for_sandbox( - desc.has_get() ? desc.get() : v8::Undefined(isolate).As(), - desc.has_set() ? desc.set() : v8::Undefined(isolate).As()); - +// static +void ContextifyContext::PropertyDefinerCallback( + Local property, + const PropertyDescriptor& desc, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); + + // Still initializing + if (ctx->context_.IsEmpty()) + return; + + Local context = ctx->context(); + v8::Isolate* isolate = context->GetIsolate(); + + auto attributes = PropertyAttribute::None; + bool is_declared = + ctx->global_proxy()->GetRealNamedPropertyAttributes(ctx->context(), + property) + .To(&attributes); + bool read_only = + static_cast(attributes) & + static_cast(PropertyAttribute::ReadOnly); + + // If the property is set on the global as read_only, don't change it on + // the global or sandbox. + if (is_declared && read_only) + return; + + Local sandbox = ctx->sandbox(); + + auto define_prop_on_sandbox = + [&] (PropertyDescriptor* desc_for_sandbox) { + if (desc.has_enumerable()) { + desc_for_sandbox->set_enumerable(desc.enumerable()); + } + if (desc.has_configurable()) { + desc_for_sandbox->set_configurable(desc.configurable()); + } + // Set the property on the sandbox. + sandbox->DefineProperty(context, property, *desc_for_sandbox) + .FromJust(); + }; + + if (desc.has_get() || desc.has_set()) { + PropertyDescriptor desc_for_sandbox( + desc.has_get() ? desc.get() : v8::Undefined(isolate).As(), + desc.has_set() ? desc.set() : v8::Undefined(isolate).As()); + + define_prop_on_sandbox(&desc_for_sandbox); + } else { + Local value = + desc.has_value() ? desc.value() : v8::Undefined(isolate).As(); + + if (desc.has_writable()) { + PropertyDescriptor desc_for_sandbox(value, desc.writable()); define_prop_on_sandbox(&desc_for_sandbox); } else { - Local value = - desc.has_value() ? desc.value() : v8::Undefined(isolate).As(); - - if (desc.has_writable()) { - PropertyDescriptor desc_for_sandbox(value, desc.writable()); - define_prop_on_sandbox(&desc_for_sandbox); - } else { - PropertyDescriptor desc_for_sandbox(value); - define_prop_on_sandbox(&desc_for_sandbox); - } + PropertyDescriptor desc_for_sandbox(value); + define_prop_on_sandbox(&desc_for_sandbox); } } +} - static void PropertyDeleterCallback( - Local property, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - - // Still initializing - if (ctx->context_.IsEmpty()) - return; +// static +void ContextifyContext::PropertyDeleterCallback( + Local property, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - Maybe success = ctx->sandbox()->Delete(ctx->context(), property); + // Still initializing + if (ctx->context_.IsEmpty()) + return; - if (success.FromMaybe(false)) - return; + Maybe success = ctx->sandbox()->Delete(ctx->context(), property); - // Delete failed on the sandbox, intercept and do not delete on - // the global object. - args.GetReturnValue().Set(false); - } + if (success.FromMaybe(false)) + return; + // Delete failed on the sandbox, intercept and do not delete on + // the global object. + args.GetReturnValue().Set(false); +} - static void PropertyEnumeratorCallback( - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); +// static +void ContextifyContext::PropertyEnumeratorCallback( + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; + // Still initializing + if (ctx->context_.IsEmpty()) + return; - args.GetReturnValue().Set(ctx->sandbox()->GetPropertyNames()); - } - - static void IndexedPropertyGetterCallback( - uint32_t index, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); + args.GetReturnValue().Set(ctx->sandbox()->GetPropertyNames()); +} - // Still initializing - if (ctx->context_.IsEmpty()) - return; +// static +void ContextifyContext::IndexedPropertyGetterCallback( + uint32_t index, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - PropertyGetterCallback(Uint32ToName(ctx->context(), index), args); - } + // Still initializing + if (ctx->context_.IsEmpty()) + return; + ContextifyContext::PropertyGetterCallback( + Uint32ToName(ctx->context(), index), args); +} - static void IndexedPropertySetterCallback( - uint32_t index, - Local value, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; +void ContextifyContext::IndexedPropertySetterCallback( + uint32_t index, + Local value, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - PropertySetterCallback(Uint32ToName(ctx->context(), index), value, args); - } + // Still initializing + if (ctx->context_.IsEmpty()) + return; + ContextifyContext::PropertySetterCallback( + Uint32ToName(ctx->context(), index), value, args); +} - static void IndexedPropertyDescriptorCallback( - uint32_t index, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); +// static +void ContextifyContext::IndexedPropertyDescriptorCallback( + uint32_t index, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; + // Still initializing + if (ctx->context_.IsEmpty()) + return; - PropertyDescriptorCallback(Uint32ToName(ctx->context(), index), args); - } + ContextifyContext::PropertyDescriptorCallback( + Uint32ToName(ctx->context(), index), args); +} - static void IndexedPropertyDefinerCallback( - uint32_t index, - const PropertyDescriptor& desc, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); +void ContextifyContext::IndexedPropertyDefinerCallback( + uint32_t index, + const PropertyDescriptor& desc, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; + // Still initializing + if (ctx->context_.IsEmpty()) + return; - PropertyDefinerCallback(Uint32ToName(ctx->context(), index), desc, args); - } + ContextifyContext::PropertyDefinerCallback( + Uint32ToName(ctx->context(), index), desc, args); +} - static void IndexedPropertyDeleterCallback( - uint32_t index, - const PropertyCallbackInfo& args) { - ContextifyContext* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); +// static +void ContextifyContext::IndexedPropertyDeleterCallback( + uint32_t index, + const PropertyCallbackInfo& args) { + ContextifyContext* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Data().As()); - // Still initializing - if (ctx->context_.IsEmpty()) - return; + // Still initializing + if (ctx->context_.IsEmpty()) + return; - Maybe success = ctx->sandbox()->Delete(ctx->context(), index); + Maybe success = ctx->sandbox()->Delete(ctx->context(), index); - if (success.FromMaybe(false)) - return; + if (success.FromMaybe(false)) + return; - // Delete failed on the sandbox, intercept and do not delete on - // the global object. - args.GetReturnValue().Set(false); - } -}; + // Delete failed on the sandbox, intercept and do not delete on + // the global object. + args.GetReturnValue().Set(false); +} class ContextifyScript : public BaseObject { private: @@ -1161,7 +1143,7 @@ void InitContextify(Local target, ContextifyScript::Init(env, target); } -} // anonymous namespace +} // namespace contextify } // namespace node -NODE_BUILTIN_MODULE_CONTEXT_AWARE(contextify, node::InitContextify) +NODE_BUILTIN_MODULE_CONTEXT_AWARE(contextify, node::contextify::InitContextify) diff --git a/src/node_contextify.h b/src/node_contextify.h new file mode 100644 index 00000000000000..e8a54e1667cc31 --- /dev/null +++ b/src/node_contextify.h @@ -0,0 +1,98 @@ +#ifndef SRC_NODE_CONTEXTIFY_H_ +#define SRC_NODE_CONTEXTIFY_H_ + +#include "node_internals.h" +#include "node_watchdog.h" +#include "base_object-inl.h" + +namespace node { +namespace contextify { + +class ContextifyContext { + protected: + // V8 reserves the first field in context objects for the debugger. We use the + // second field to hold a reference to the sandbox object. + enum { kSandboxObjectIndex = 1 }; + + Environment* const env_; + v8::Persistent context_; + + public: + ContextifyContext(Environment* env, + v8::Local sandbox_obj, + v8::Local options_obj); + ~ContextifyContext(); + + v8::Local CreateDataWrapper(Environment* env); + v8::Local CreateV8Context(Environment* env, + v8::Local sandbox_obj, v8::Local options_obj); + static void Init(Environment* env, v8::Local target); + + static ContextifyContext* ContextFromContextifiedSandbox( + Environment* env, + const v8::Local& sandbox); + + inline Environment* env() const { + return env_; + } + + inline v8::Local context() const { + return PersistentToLocal(env()->isolate(), context_); + } + + inline v8::Local global_proxy() const { + return context()->Global(); + } + + inline v8::Local sandbox() const { + return v8::Local::Cast( + context()->GetEmbedderData(kSandboxObjectIndex)); + } + + private: + static void MakeContext(const v8::FunctionCallbackInfo& args); + static void IsContext(const v8::FunctionCallbackInfo& args); + static void WeakCallback( + const v8::WeakCallbackInfo& data); + static void PropertyGetterCallback( + v8::Local property, + const v8::PropertyCallbackInfo& args); + static void PropertySetterCallback( + v8::Local property, + v8::Local value, + const v8::PropertyCallbackInfo& args); + static void PropertyDescriptorCallback( + v8::Local property, + const v8::PropertyCallbackInfo& args); + static void PropertyDefinerCallback( + v8::Local property, + const v8::PropertyDescriptor& desc, + const v8::PropertyCallbackInfo& args); + static void PropertyDeleterCallback( + v8::Local property, + const v8::PropertyCallbackInfo& args); + static void PropertyEnumeratorCallback( + const v8::PropertyCallbackInfo& args); + static void IndexedPropertyGetterCallback( + uint32_t index, + const v8::PropertyCallbackInfo& args); + static void IndexedPropertySetterCallback( + uint32_t index, + v8::Local value, + const v8::PropertyCallbackInfo& args); + static void IndexedPropertyDescriptorCallback( + uint32_t index, + const v8::PropertyCallbackInfo& args); + static void IndexedPropertyDefinerCallback( + uint32_t index, + const v8::PropertyDescriptor& desc, + const v8::PropertyCallbackInfo& args); + static void IndexedPropertyDeleterCallback( + uint32_t index, + const v8::PropertyCallbackInfo& args); +}; + +} // namespace contextify +} // namespace node + +#endif // SRC_NODE_CONTEXTIFY_H_ From 03f57bfd741508048e2ce32f8c75c68bcabe7cc5 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 13 Jan 2018 15:37:21 -0800 Subject: [PATCH 02/12] src: factor out some common functions --- src/node_contextify.cc | 447 ++++++++++++++++++++--------------------- src/node_contextify.h | 11 + 2 files changed, 233 insertions(+), 225 deletions(-) diff --git a/src/node_contextify.cc b/src/node_contextify.cc index c50cb19529c13d..35b39d62e1fc77 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -586,6 +586,227 @@ void ContextifyContext::IndexedPropertyDeleterCallback( args.GetReturnValue().Set(false); } +Maybe GetBreakOnSigintArg(Environment* env, + Local options) { + if (options->IsUndefined() || options->IsString()) { + return Just(false); + } + if (!options->IsObject()) { + env->ThrowTypeError("options must be an object"); + return Nothing(); + } + + Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "breakOnSigint"); + MaybeLocal maybe_value = + options.As()->Get(env->context(), key); + if (maybe_value.IsEmpty()) + return Nothing(); + + Local value = maybe_value.ToLocalChecked(); + return Just(value->IsTrue()); +} + +Maybe GetTimeoutArg(Environment* env, Local options) { + if (options->IsUndefined() || options->IsString()) { + return Just(-1); + } + if (!options->IsObject()) { + env->ThrowTypeError("options must be an object"); + return Nothing(); + } + + MaybeLocal maybe_value = + options.As()->Get(env->context(), env->timeout_string()); + if (maybe_value.IsEmpty()) + return Nothing(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) { + return Just(-1); + } + + Maybe timeout = value->IntegerValue(env->context()); + + if (timeout.IsJust() && timeout.ToChecked() <= 0) { + env->ThrowRangeError("timeout must be a positive number"); + return Nothing(); + } + + return timeout; +} + +MaybeLocal GetLineOffsetArg(Environment* env, + Local options) { + Local defaultLineOffset = Integer::New(env->isolate(), 0); + + if (!options->IsObject()) { + return defaultLineOffset; + } + + Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "lineOffset"); + MaybeLocal maybe_value = + options.As()->Get(env->context(), key); + if (maybe_value.IsEmpty()) + return MaybeLocal(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) + return defaultLineOffset; + + return value->ToInteger(env->context()); +} + +MaybeLocal GetColumnOffsetArg(Environment* env, + Local options) { + Local defaultColumnOffset = Integer::New(env->isolate(), 0); + + if (!options->IsObject()) { + return defaultColumnOffset; + } + + Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "columnOffset"); + MaybeLocal maybe_value = + options.As()->Get(env->context(), key); + if (maybe_value.IsEmpty()) + return MaybeLocal(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) + return defaultColumnOffset; + + return value->ToInteger(env->context()); +} + +MaybeLocal GetContextArg(Environment* env, + Local options) { + if (!options->IsObject()) + return MaybeLocal(); + + MaybeLocal maybe_value = + options.As()->Get(env->context(), + env->vm_parsing_context_symbol()); + Local value; + if (!maybe_value.ToLocal(&value)) + return MaybeLocal(); + + if (!value->IsObject()) { + if (!value->IsNullOrUndefined()) { + env->ThrowTypeError( + "contextifiedSandbox argument must be an object."); + } + return MaybeLocal(); + } + + ContextifyContext* sandbox = + ContextifyContext::ContextFromContextifiedSandbox( + env, value.As()); + if (!sandbox) { + env->ThrowTypeError( + "sandbox argument must have been converted to a context."); + return MaybeLocal(); + } + + Local context = sandbox->context(); + if (context.IsEmpty()) + return MaybeLocal(); + return context; +} + +namespace { + +Maybe GetDisplayErrorsArg(Environment* env, + Local options) { + if (options->IsUndefined() || options->IsString()) { + return Just(true); + } + if (!options->IsObject()) { + env->ThrowTypeError("options must be an object"); + return Nothing(); + } + + Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "displayErrors"); + MaybeLocal maybe_value = + options.As()->Get(env->context(), key); + if (maybe_value.IsEmpty()) + return Nothing(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) + return Just(true); + + return value->BooleanValue(env->context()); +} + +MaybeLocal GetFilenameArg(Environment* env, + Local options) { + Local defaultFilename = + FIXED_ONE_BYTE_STRING(env->isolate(), "evalmachine."); + + if (options->IsUndefined()) { + return defaultFilename; + } + if (options->IsString()) { + return options.As(); + } + if (!options->IsObject()) { + env->ThrowTypeError("options must be an object"); + return Local(); + } + + Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "filename"); + MaybeLocal maybe_value = + options.As()->Get(env->context(), key); + if (maybe_value.IsEmpty()) + return MaybeLocal(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) + return defaultFilename; + return value->ToString(env->context()); +} + +MaybeLocal GetCachedData(Environment* env, + Local options) { + if (!options->IsObject()) { + return MaybeLocal(); + } + + MaybeLocal maybe_value = + options.As()->Get(env->context(), env->cached_data_string()); + if (maybe_value.IsEmpty()) + return MaybeLocal(); + + Local value = maybe_value.ToLocalChecked(); + if (value->IsUndefined()) { + return MaybeLocal(); + } + + if (!value->IsUint8Array()) { + env->ThrowTypeError("options.cachedData must be a Buffer instance"); + return MaybeLocal(); + } + + return value.As(); +} + +Maybe GetProduceCachedData(Environment* env, + Local options) { + if (!options->IsObject()) { + return Just(false); + } + + MaybeLocal maybe_value = + options.As()->Get(env->context(), + env->produce_cached_data_string()); + if (maybe_value.IsEmpty()) + return Nothing(); + + Local value = maybe_value.ToLocalChecked(); + return Just(value->IsTrue()); +} + +} // anonymous namespace + class ContextifyScript : public BaseObject { private: Persistent script_; @@ -639,7 +860,7 @@ class ContextifyScript : public BaseObject { MaybeLocal columnOffset = GetColumnOffsetArg(env, options); MaybeLocal cached_data_buf = GetCachedData(env, options); Maybe maybe_produce_cached_data = GetProduceCachedData(env, options); - MaybeLocal maybe_context = GetContext(env, options); + MaybeLocal maybe_context = GetContextArg(env, options); if (try_catch.HasCaught()) { no_abort_scope.Close(); try_catch.ReThrow(); @@ -831,230 +1052,6 @@ class ContextifyScript : public BaseObject { True(env->isolate())); } - static Maybe GetBreakOnSigintArg(Environment* env, - Local options) { - if (options->IsUndefined() || options->IsString()) { - return Just(false); - } - if (!options->IsObject()) { - env->ThrowTypeError("options must be an object"); - return Nothing(); - } - - Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "breakOnSigint"); - MaybeLocal maybe_value = - options.As()->Get(env->context(), key); - if (maybe_value.IsEmpty()) - return Nothing(); - - Local value = maybe_value.ToLocalChecked(); - return Just(value->IsTrue()); - } - - static Maybe GetTimeoutArg(Environment* env, Local options) { - if (options->IsUndefined() || options->IsString()) { - return Just(-1); - } - if (!options->IsObject()) { - env->ThrowTypeError("options must be an object"); - return Nothing(); - } - - MaybeLocal maybe_value = - options.As()->Get(env->context(), env->timeout_string()); - if (maybe_value.IsEmpty()) - return Nothing(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) { - return Just(-1); - } - - Maybe timeout = value->IntegerValue(env->context()); - - if (timeout.IsJust() && timeout.ToChecked() <= 0) { - env->ThrowRangeError("timeout must be a positive number"); - return Nothing(); - } - - return timeout; - } - - - static Maybe GetDisplayErrorsArg(Environment* env, - Local options) { - if (options->IsUndefined() || options->IsString()) { - return Just(true); - } - if (!options->IsObject()) { - env->ThrowTypeError("options must be an object"); - return Nothing(); - } - - Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "displayErrors"); - MaybeLocal maybe_value = - options.As()->Get(env->context(), key); - if (maybe_value.IsEmpty()) - return Nothing(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) - return Just(true); - - return value->BooleanValue(env->context()); - } - - - static MaybeLocal GetFilenameArg(Environment* env, - Local options) { - Local defaultFilename = - FIXED_ONE_BYTE_STRING(env->isolate(), "evalmachine."); - - if (options->IsUndefined()) { - return defaultFilename; - } - if (options->IsString()) { - return options.As(); - } - if (!options->IsObject()) { - env->ThrowTypeError("options must be an object"); - return Local(); - } - - Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "filename"); - MaybeLocal maybe_value = - options.As()->Get(env->context(), key); - if (maybe_value.IsEmpty()) - return MaybeLocal(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) - return defaultFilename; - return value->ToString(env->context()); - } - - - static MaybeLocal GetCachedData(Environment* env, - Local options) { - if (!options->IsObject()) { - return MaybeLocal(); - } - - MaybeLocal maybe_value = - options.As()->Get(env->context(), env->cached_data_string()); - if (maybe_value.IsEmpty()) - return MaybeLocal(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) { - return MaybeLocal(); - } - - if (!value->IsUint8Array()) { - env->ThrowTypeError("options.cachedData must be a Buffer instance"); - return MaybeLocal(); - } - - return value.As(); - } - - - static Maybe GetProduceCachedData(Environment* env, - Local options) { - if (!options->IsObject()) { - return Just(false); - } - - MaybeLocal maybe_value = - options.As()->Get(env->context(), - env->produce_cached_data_string()); - if (maybe_value.IsEmpty()) - return Nothing(); - - Local value = maybe_value.ToLocalChecked(); - return Just(value->IsTrue()); - } - - - static MaybeLocal GetLineOffsetArg(Environment* env, - Local options) { - Local defaultLineOffset = Integer::New(env->isolate(), 0); - - if (!options->IsObject()) { - return defaultLineOffset; - } - - Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "lineOffset"); - MaybeLocal maybe_value = - options.As()->Get(env->context(), key); - if (maybe_value.IsEmpty()) - return MaybeLocal(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) - return defaultLineOffset; - - return value->ToInteger(env->context()); - } - - - static MaybeLocal GetColumnOffsetArg(Environment* env, - Local options) { - Local defaultColumnOffset = Integer::New(env->isolate(), 0); - - if (!options->IsObject()) { - return defaultColumnOffset; - } - - Local key = FIXED_ONE_BYTE_STRING(env->isolate(), "columnOffset"); - MaybeLocal maybe_value = - options.As()->Get(env->context(), key); - if (maybe_value.IsEmpty()) - return MaybeLocal(); - - Local value = maybe_value.ToLocalChecked(); - if (value->IsUndefined()) - return defaultColumnOffset; - - return value->ToInteger(env->context()); - } - - static MaybeLocal GetContext(Environment* env, - Local options) { - if (!options->IsObject()) - return MaybeLocal(); - - MaybeLocal maybe_value = - options.As()->Get(env->context(), - env->vm_parsing_context_symbol()); - Local value; - if (!maybe_value.ToLocal(&value)) - return MaybeLocal(); - - if (!value->IsObject()) { - if (!value->IsNullOrUndefined()) { - env->ThrowTypeError( - "contextifiedSandbox argument must be an object."); - } - return MaybeLocal(); - } - - ContextifyContext* sandbox = - ContextifyContext::ContextFromContextifiedSandbox( - env, value.As()); - if (!sandbox) { - env->ThrowTypeError( - "sandbox argument must have been converted to a context."); - return MaybeLocal(); - } - - Local context = sandbox->context(); - if (context.IsEmpty()) - return MaybeLocal(); - return context; - } - - static bool EvalMachine(Environment* env, const int64_t timeout, const bool display_errors, diff --git a/src/node_contextify.h b/src/node_contextify.h index e8a54e1667cc31..b2f32850ac1c43 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -92,6 +92,17 @@ class ContextifyContext { const v8::PropertyCallbackInfo& args); }; +v8::Maybe GetBreakOnSigintArg( + Environment* env, v8::Local options); +v8::Maybe GetTimeoutArg( + Environment* env, v8::Local options); +v8::MaybeLocal GetLineOffsetArg( + Environment* env, v8::Local options); +v8::MaybeLocal GetColumnOffsetArg( + Environment* env, v8::Local options); +v8::MaybeLocal GetContextArg( + Environment* env, v8::Local options); + } // namespace contextify } // namespace node From 9ed3aa1aea6022f82ea27aa8da5dc453f78ca5e6 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 13 Jan 2018 23:35:51 -0800 Subject: [PATCH 03/12] vm: add modules adds vm.Module, which wraps around module_wrap to provide an interface for developers to work with modules in a more reflective manner. vm.Module.createContext yields a "module context" which includes a way to hook the loader for that context, again allowing for greater control in whats going on with these modules. --- doc/api/errors.md | 25 +++ doc/api/vm.md | 166 +++++++++++++++ lib/internal/errors.js | 6 + lib/internal/vm/Module.js | 193 ++++++++++++++++++ lib/vm.js | 5 +- node.gyp | 1 + src/module_wrap.cc | 183 ++++++++++++++--- src/module_wrap.h | 8 +- test/parallel/test-vm-module-basic.js | 54 +++++ .../parallel/test-vm-module-dynamic-import.js | 24 +++ test/parallel/test-vm-module-errors.js | 139 +++++++++++++ test/parallel/test-vm-module-link.js | 27 +++ test/parallel/test-vm-module-reevaluate.js | 45 ++++ 13 files changed, 841 insertions(+), 35 deletions(-) create mode 100644 lib/internal/vm/Module.js create mode 100644 test/parallel/test-vm-module-basic.js create mode 100644 test/parallel/test-vm-module-dynamic-import.js create mode 100644 test/parallel/test-vm-module-errors.js create mode 100644 test/parallel/test-vm-module-link.js create mode 100644 test/parallel/test-vm-module-reevaluate.js diff --git a/doc/api/errors.md b/doc/api/errors.md index 0e28b9b91d196e..cbb31d3cab98ab 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1639,6 +1639,31 @@ entry types were found. Superseded by `ERR_OUT_OF_RANGE` + +### ERR_VM_MODULE_ALREADY_LINKED + +The module has already been linked. + + +### ERR_VM_MODULE_STATUS + +You're trying to do something to a module but it isn't in the right state. + + +### ERR_VM_MODULE_MATCHING_CONTEXT + +Linked modules must share the same context. + + +### ERR_VM_MODULE_NOT_MODULE + +The fulfilled value of a linking promise was not a module (`vm.Module`). + + +### ERR_VM_MODULE_NOT_LINKED + +The module must be linked. + ### ERR_ZLIB_BINDING_CLOSED diff --git a/doc/api/vm.md b/doc/api/vm.md index 3ac70091e58552..c205ad4d32038d 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -43,6 +43,171 @@ console.log(x); // 1; y is not defined. *Note*: The vm module is not a security mechanism. **Do not use it to run untrusted code**. +## Class: vm.Module + + +Instances of the`vm.Module` class contain compiled module code that can +be evaluated in a set context. This class tries to mirror [`ES Module`][] +specification as much as possible. + +```javascript +(async () => { + const foo = new vm.Module('export default 5;'); + + const bar = new vm.Module('import five from "foo"; five'); + + await bar.link(async (specifier) => { + if (specifier === 'foo') + return foo; + throw new Error(`Unable to resolve dependency: ${specifier}`); + }); + + bar.instantiate(); + + const { result: five } = await bar.evaluate(); + + five + 5; //> 10 +})(); +``` + +### Static: vm.Module.setImportDynamicallyCallback(callback); + + +* `callback` {Function} + * `referrer` {String} url of the module which made the dynamic import request + * `specifier` {String} url of the module being requested. + +Handle dynamic import requests from vm.Module instances that are evaluating. +The callback should return a module namespace, not a module. See the example +below for more details. + +```javascript +vm.Module.setImportDynamicallyCallback(async (referrer, specifier) => { + if (specifier === 'random') { + const m = new vm.Module('export default Math.random()'); + await m.link(); + m.instantiate(); + await m.evaluate(); + return m.namespace; + } + + return null; +}); +``` + +### Constructor: new vm.Module(code[, options]) + + +* `code` {string} the JavaScript code to evaluate. +* `options` + * `url` {string} Specifies the url used in module resolving and stack traces + * `context` {vm.Context} The context to compile and evaluate this code in + * `lineOffset` {number} Specifies the line number offset that is displayed + in stack traces produced by this script. + * `columnOffset` {number} Spcifies the column number offset that is displayed + in stack traces produced by this script. + +### module.status + + +* {string} + +The current status of the module. Will be one of: +- `uninstantiated` +- `instantiating` +- `instantiated` +- `evaluating` +- `evaluated` +- `errored` + +### module.url + + +* {string} + +### module.dependencySpecifiers + + +* {Array} + +### module.namespace + + +* {object} + +The namespace of the module, only available after evaluation. + +### module.error + + +* {Error} + +### module.link([fn]) + + +* Returns: {Promise} + +Link module dependencies. The linking function may return a promise if desired. + +```javascript +(async () => { + const foo = new vm.Module('export default 5', { url: 'foo' }); + const bar = new vm.Module('import five from "foo"', { url: 'bar' }); + + await bar.link(async (specifier) => { + if (specifier === 'foo') + return foo; + throw new Error(`Unable to resolve dependency: ${specifier}`); + }); + + bar.instantiate(); + bar.evaluate(); +})(); +``` + +### module.instantiate() + + +Instantiate the module. + +### module.evaluate([options]) + + +* Returns: {Promise<{ result }>} + +* `options` + * `timeout` {number} Specifies the number of milliseconds to evaluate + before terminating execution. If execution is terminated, an [`Error`][] + will be thrown. + * `breakOnSigint`: if `true`, the execution will be terminated when + `SIGINT` (Ctrl+C) is received. Existing handlers for the + event that have been attached via `process.on("SIGINT")` will be disabled + during script execution, but will continue to work after that. + If execution is terminated, an [`Error`][] will be thrown. + + ## Class: vm.Script -Instances of the`vm.Module` class contain compiled module code that can -be evaluated in a set context. This class tries to mirror [`ES Module`][] -specification as much as possible. +The `vm.Module` class provides a way to use ECMAScript modules in VM contexts. +It is the counterpart of the `vm.Script` class that closely mirrors [Source Text +Module Record][]s as defined in the ECMAScript specification. + +Unlike `vm.Script` however, every `vm.Module` object is bound to a context from +its creation. Operations on `vm.Module` objects are intrinsically asynchronous, +in constrast with the synchronous nature of `vm.Script` objects. With the help +of async functions, however, manipulating `vm.Module` objects are fairly +straightforward. + +Using a `vm.Module` object requires four distinct steps: creation/parsing, +linking, instantiating, and evaluation. These four steps are illustrated in the +following example. + +```js +const vm = require('vm'); + +const contextifiedSandbox = vm.createContext({ secret: 42 }); -```javascript (async () => { - const foo = new vm.Module('export default 5;'); + // Step 1 + // + // Create a Module by constructing a new `vm.Module` object. This parses the + // provided source text, throwing a `SyntaxError` if anything goes wrong. By + // default, a Module is created in the top context. But here, we specify + // `contextifiedSandbox` as the context this Module belongs to. + // + // Here, we attempt to obtain the default export from the module "foo", and + // put it into local binding "secret". + + const bar = new vm.Module('import secret from "foo"; secret;', { + context: contextifiedSandbox + }); - const bar = new vm.Module('import five from "foo"; five'); + + // Step 2 + // + // "Link" the imported dependencies of this Module to it. + // + // The provided linking callback (the "linker") accepts an argument that is + // the specifier of the imported module. The callback is expected to return a + // Module that corresponds to the provided specifier with certain requirements + // documented in `module.link()`. + // + // Even Modules without dependencies must be explicitly linked. The callback + // provided would never be called, however. + // + // The link() method returns a Promise that will be resolved when all the + // Promises returned by the linker resolve. + // + // Note: This is a contrived example in that there is only one layer of + // dependency. To form a proper Module system, the linker would be fully + // recursive. await bar.link(async (specifier) => { - if (specifier === 'foo') + if (specifier === 'foo') { + // The `secret` variable refers to the global variable we added to + // `contextifiedSandbox` when creating the context. + const foo = new vm.Module('export default secret;', { + context: contextifiedSandbox + }); + await foo.link(() => {}); return foo; + } throw new Error(`Unable to resolve dependency: ${specifier}`); }); + + // Step 3 + // + // Instantiate the top-level Module. + // + // Only the top-level Module needs to be explicitly instantiated; its + // dependencies will be recursively instantiated by instantiate(). + bar.instantiate(); - const { result: five } = await bar.evaluate(); - five + 5; //> 10 -})(); -``` + // Step 4 + // + // Evaluate the Module. The evaluate() method returns a Promise with a single + // property "result" that contains the result of the very last statement + // executed in the Module. In the case of `bar`, it is `secret;`, which refers + // to the default export of the `foo` module, the `secret` we set in the + // beginning to 42. -### Static: vm.Module.setImportDynamicallyCallback(callback); - + const { result } = await bar.evaluate(); -* `callback` {Function} - * `referrer` {String} url of the module which made the dynamic import request - * `specifier` {String} url of the module being requested. - -Handle dynamic import requests from vm.Module instances that are evaluating. -The callback should return a module namespace, not a module. See the example -below for more details. - -```javascript -vm.Module.setImportDynamicallyCallback(async (referrer, specifier) => { - if (specifier === 'random') { - const m = new vm.Module('export default Math.random()'); - await m.link(); - m.instantiate(); - await m.evaluate(); - return m.namespace; - } - - return null; -}); + console.log(result); + // Prints 42. +})(); ``` ### Constructor: new vm.Module(code[, options]) - -* `code` {string} the JavaScript code to evaluate. +* `code` {string} JavaScript Module code to parse * `options` - * `url` {string} Specifies the url used in module resolving and stack traces - * `context` {vm.Context} The context to compile and evaluate this code in - * `lineOffset` {number} Specifies the line number offset that is displayed - in stack traces produced by this script. - * `columnOffset` {number} Spcifies the column number offset that is displayed - in stack traces produced by this script. + * `url` {string} URL used in module resolution and stack traces + * `context` {Object} The [contextified][] object as returned by the + `vm.createContext()` method, to compile and evaluate this Module in. + * `lineOffset` {integer} Specifies the line number offset that is displayed + in stack traces produced by this Module. + * `columnOffset` {integer} Spcifies the column number offset that is displayed + in stack traces produced by this Module. + +### module.dependencySpecifiers + +* {string[]} + +### module.error + +* {any} + +### module.linkingStatus + +* {string} + +The current linking status of `module`. It will be one of the following values: + +- `'unlinked'`: `module.link()` has not yet been called. +- `'linking'`: `module.link()` has been called, but not all Promises returned by + the linker function have been resolved yet. +- `'linked'`: `module.link()` has been called, and all its dependencies have + been successfully linked. +- `'errored'`: `module.link()` has been called, but at least one of its + dependencies failed to link, either because the callback returned a Promise + that is rejected, or because the Module the callback returned is invalid. + +### module.namespace + +* {Object} + +The namespace object of the module. This is only available after instantiation +(`module.instantiate()`) has completed. ### module.status - * {string} The current status of the module. Will be one of: -- `uninstantiated` -- `instantiating` -- `instantiated` -- `evaluating` -- `evaluated` -- `errored` -### module.url - +- `'uninstantiated'`: The module is not instantiated. It may because of any of + the following reasons: -* {string} + - The module was just created. + - `module.instantiate()` has been called on this module, but it failed for + some reason. -### module.dependencySpecifiers - + This status does not convey any information regarding if `module.link()` has + been called. See `module.linkingStatus` for that. -* {Array} +- `'instantiating'`: The module is currently being instantiated through a + `module.instantiate()` call on itself or a parent module. -### module.namespace - +- `'instantiated'`: The module has been instantiated successfully, but + `module.evaluate()` has not yet been called. -* {object} +- `'evaluating'`: The module is being evaluated through a `module.evaluate()` on + itself or a parent module. -The namespace of the module, only available after evaluation. +- `'evaluated'`: The module has been successfully evaluated. -### module.error - +- `'errored'`: The module has been evaluated, but an exception was thrown. -* {Error} +Other than `'errored'`, this status string corresponds to the specification's +[Source Text Module Record][]'s [[Status]] field. `'errored'` corresponds to +`'evaluated'` in the specification, but with [[EvaluationError]] set to a value +that is not `undefined`. -### module.link([fn]) - +### module.url + +* {string} +### module.evaluate([options]) + +* `options` {Object} + * `timeout` {number} Specifies the number of milliseconds to evaluate + before terminating execution. If execution is interrupted, an [`Error`][] + will be thrown. + * `breakOnSigint` {boolean} If `true`, the execution will be terminated when + `SIGINT` (Ctrl+C) is received. Existing handlers for the event that have + been attached via `process.on("SIGINT")` will be disabled during script + execution, but will continue to work after that. If execution is + interrupted, an [`Error`][] will be thrown. * Returns: {Promise} -Link module dependencies. The linking function may return a promise if desired. +Evaluate the module. -```javascript -(async () => { - const foo = new vm.Module('export default 5', { url: 'foo' }); - const bar = new vm.Module('import five from "foo"', { url: 'bar' }); +This must be called after the module has been instantiated; otherwise it will +throw an error. It could be called also when the module has already been +evaluated, in which case it will do one of the following two things: - await bar.link(async (specifier) => { - if (specifier === 'foo') - return foo; - throw new Error(`Unable to resolve dependency: ${specifier}`); - }); +- return `undefined` if the initial evaluation ended in success (`module.status` + is `'evaluated'`) +- rethrow the same exception the initial evaluation threw if the initial + evaluation ended in an error (`module.status` is `'errored'`) - bar.instantiate(); - bar.evaluate(); -})(); -``` +This method cannot be called while the module is being evaluated +(`module.status` is `'evaluating'`) to prevent infinite recursion. ### module.instantiate() - -Instantiate the module. +Instantiate the module. This must be called after linking has completed +(`linkingStatus` is `'linked'`); otherwise it will throw an error. It may also +throw an exception if one of the dependencies does not provide an export the +parent module requires. -### module.evaluate([options]) - +However, if this function succeeded, further calls to this function after the +initial instantiation will be no-ops, to be consistent with the ECMAScript +specification. -* Returns: {Promise<{ result }>} +Unlike other methods operating on `Module`, this function completes +synchronously and returns nothing. -* `options` - * `timeout` {number} Specifies the number of milliseconds to evaluate - before terminating execution. If execution is terminated, an [`Error`][] - will be thrown. - * `breakOnSigint`: if `true`, the execution will be terminated when - `SIGINT` (Ctrl+C) is received. Existing handlers for the - event that have been attached via `process.on("SIGINT")` will be disabled - during script execution, but will continue to work after that. - If execution is terminated, an [`Error`][] will be thrown. +### module.link(linker) + +* `linker` {Function} +* Returns: {Promise} + +Link module dependencies. This method must be called before instantiation, and +can only be called once per module. + +Two parameters will be passed to the `linker` function: + +- `referencingModule` The `Module` object `link()` is called on. +- `specifier` The specifier of the requested module: + + ```js + import foo from 'foo'; + // ^^^^^ the module specifier + ``` + +The function is expected to return a `Module` object or a `Promise` that +eventually resolves to a `Module` object. The returned `Module` must satisfy two +requirements: + +- It must belong to the same context as the parent `Module`. +- It must be already linked or currently linking (for cyclical imports). + +`link()` returns a `Promise` that will either get resolved when all linking +instances resolve to a valid `Module`, or rejected if the linker function does +any of the following: +- throws an exception +- returns a `Promise` that eventually gets rejected +- returns a `Promise` fulfilled with an invalid `Module` (or not a `Module` + object at all) ## Class: vm.Script -The `vm.Module` class provides a way to use ECMAScript modules in VM contexts. -It is the counterpart of the `vm.Script` class that closely mirrors [Source Text -Module Record][]s as defined in the ECMAScript specification. +> Stability: 1 - Experimental + +The `vm.Module` class provides a low-level interface for using ECMAScript +modules in VM contexts. It is the counterpart of the `vm.Script` class that +closely mirrors [Source Text Module Record][]s as defined in the ECMAScript +specification. Unlike `vm.Script` however, every `vm.Module` object is bound to a context from its creation. Operations on `vm.Module` objects are intrinsically asynchronous, -in constrast with the synchronous nature of `vm.Script` objects. With the help +in contrast with the synchronous nature of `vm.Script` objects. With the help of async functions, however, manipulating `vm.Module` objects are fairly straightforward. @@ -62,6 +65,9 @@ Using a `vm.Module` object requires four distinct steps: creation/parsing, linking, instantiating, and evaluation. These four steps are illustrated in the following example. +This feature is only available with the `--experimental-vm-modules` command +flag enabled. + *Note*: This implementation lies at a lower level than the [ECMAScript Module loader][]. There is also currently no way to interact with the Loader, though support is planned. @@ -85,9 +91,7 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); const bar = new vm.Module(` import secret from 'foo'; secret; - `, { - context: contextifiedSandbox - }); + `, { context: contextifiedSandbox }); // Step 2 @@ -119,9 +123,7 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); // The "secret" variable refers to the global variable we added to // "contextifiedSandbox" when creating the context. export default secret; - `, { - context: contextifiedSandbox - }); + `, { context: contextifiedSandbox }); } throw new Error(`Unable to resolve dependency: ${specifier}`); } @@ -317,6 +319,7 @@ Two parameters will be passed to the `linker` function: - `referencingModule` The `Module` object `link()` is called on. - `specifier` The specifier of the requested module: + ```js import foo from 'foo'; // ^^^^^ the module specifier diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e86447d6040bb6..942c7b1a441180 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -509,10 +509,13 @@ E('ERR_V8BREAKITERATOR', 'Full ICU data not installed. ' + 'See https://github.com/nodejs/node/wiki/Intl'); E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', 'At least one valid performance entry type is required'); -E('ERR_VM_MODULE_ALREADY_LINKED', 'Provided module has already been linked'); +E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked'); E('ERR_VM_MODULE_DIFFERENT_CONTEXT', 'Linked modules must use the same context'); -E('ERR_VM_MODULE_NOT_LINKED', 'Provided module must be linked %s'); +E('ERR_VM_MODULE_LINKING_ERRORED', + 'Linking has already failed for the provided module'); +E('ERR_VM_MODULE_NOT_LINKED', + 'Module must be linked before it can be instantiated'); E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module'); E('ERR_VM_MODULE_STATUS', 'Module status %s'); E('ERR_ZLIB_BINDING_CLOSED', 'zlib binding closed'); diff --git a/lib/internal/vm/Module.js b/lib/internal/vm/Module.js index cdcdf3f078c0f6..a8625ef76be613 100644 --- a/lib/internal/vm/Module.js +++ b/lib/internal/vm/Module.js @@ -1,5 +1,6 @@ 'use strict'; +const { emitExperimentalWarning } = require('internal/util'); const { URL } = require('internal/url'); const { kParsingContext, isContext } = process.binding('contextify'); const errors = require('internal/errors'); @@ -35,6 +36,8 @@ const linkingStatusMap = new WeakMap(); class Module { constructor(src, options = {}) { + emitExperimentalWarning('vm.Module'); + if (typeof src !== 'string') throw new errors.TypeError( 'ERR_INVALID_ARG_TYPE', 'src', 'string', src); @@ -59,17 +62,15 @@ class Module { 'ERR_INVALID_ARG_TYPE', 'options.url', 'string', url); } url = new URL(url).href; + } else if (context === undefined) { + url = `vm:module(${globalModuleId++})`; + } else if (perContextModuleId.has(context)) { + const curId = perContextModuleId.get(context); + url = `vm:module(${curId})`; + perContextModuleId.set(context, curId + 1); } else { - if (context === undefined) { - url = `vm:module(${globalModuleId++})`; - } else if (perContextModuleId.has(context)) { - const curId = perContextModuleId.get(context); - url = `vm:module(${curId})`; - perContextModuleId.set(context, curId + 1); - } else { - url = 'vm:module(0)'; - perContextModuleId.set(context, 1); - } + url = 'vm:module(0)'; + perContextModuleId.set(context, 1); } const wrap = new ModuleWrap(src, url, { @@ -139,7 +140,10 @@ class Module { throw new errors.Error('ERR_VM_MODULE_NOT_MODULE'); if (m.context !== this.context) throw new errors.Error('ERR_VM_MODULE_DIFFERENT_CONTEXT'); - if (linkingStatusMap.get(m) === 'unlinked') + const childLinkingStatus = linkingStatusMap.get(m); + if (childLinkingStatus === 'errored') + throw new errors.Error('ERR_VM_MODULE_LINKING_ERRORED'); + if (childLinkingStatus === 'unlinked') await m.link(linker); return wrapMap.get(m); })(); @@ -162,7 +166,7 @@ class Module { throw new errors.Error( 'ERR_VM_MODULE_STATUS', 'must not be instantiating or evaluating'); if (linkingStatusMap.get(this) !== 'linked') - throw new errors.Error('ERR_VM_MODULE_NOT_LINKED', 'before instantiation'); + throw new errors.Error('ERR_VM_MODULE_NOT_LINKED'); wrap.instantiate(); } diff --git a/lib/vm.js b/lib/vm.js index aa16bd148b2c9a..c9bb44f05732c9 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -195,5 +195,5 @@ module.exports = { isContext, }; -if (process.binding('config').experimentalModules) +if (process.binding('config').experimentalVMModules) module.exports.Module = require('internal/vm/Module').Module; diff --git a/src/node.cc b/src/node.cc index d01276800a911f..6dc321b3ee76fc 100644 --- a/src/node.cc +++ b/src/node.cc @@ -240,6 +240,11 @@ bool config_preserve_symlinks = false; // that is used by lib/module.js bool config_experimental_modules = false; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +bool config_experimental_vm_modules = false; + // Set in node.cc by ParseArgs when --loader is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/internal/bootstrap_node.js @@ -3542,6 +3547,8 @@ static void PrintHelp() { " --preserve-symlinks preserve symbolic links when resolving\n" " --experimental-modules experimental ES Module support\n" " and caching modules\n" + " --experimental-vm-modules experimental ES Module support\n" + " in vm module\n" #endif "\n" "Environment variables:\n" @@ -3621,6 +3628,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--napi-modules", "--expose-http2", // keep as a non-op through v9.x "--experimental-modules", + "--experimental-vm-modules", "--loader", "--trace-warnings", "--redirect-warnings", @@ -3788,6 +3796,8 @@ static void ParseArgs(int* argc, config_preserve_symlinks = true; } else if (strcmp(arg, "--experimental-modules") == 0) { config_experimental_modules = true; + } else if (strcmp(arg, "--experimental-vm-modules") == 0) { + config_experimental_vm_modules = true; } else if (strcmp(arg, "--loader") == 0) { const char* module = argv[index + 1]; if (!config_experimental_modules) { diff --git a/src/node_config.cc b/src/node_config.cc index 2e9ad2ed134822..cac551ad2c410a 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -82,6 +82,9 @@ static void InitConfig(Local target, } } + if (config_experimental_vm_modules) + READONLY_BOOLEAN_PROPERTY("experimentalVMModules"); + if (config_pending_deprecation) READONLY_BOOLEAN_PROPERTY("pendingDeprecation"); diff --git a/src/node_internals.h b/src/node_internals.h index 09bcbba6e0b39f..e3a6d5fca14f3c 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -171,6 +171,11 @@ extern bool config_preserve_symlinks; // that is used by lib/module.js extern bool config_experimental_modules; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +extern bool config_experimental_vm_modules; + // Set in node.cc by ParseArgs when --loader is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/internal/bootstrap_node.js diff --git a/test/parallel/test-vm-module-basic.js b/test/parallel/test-vm-module-basic.js index f8452603ca5f34..4bbe0a95ee6724 100644 --- a/test/parallel/test-vm-module-basic.js +++ b/test/parallel/test-vm-module-basic.js @@ -1,6 +1,6 @@ 'use strict'; -// Flags: --experimental-modules +// Flags: --experimental-vm-modules const common = require('../common'); const assert = require('assert'); diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js index 4da4e787b6b741..ca4dceb5def731 100644 --- a/test/parallel/test-vm-module-dynamic-import.js +++ b/test/parallel/test-vm-module-dynamic-import.js @@ -1,6 +1,6 @@ 'use strict'; -// Flags: --experimental-modules --harmony-dynamic-import +// Flags: --experimental-vm-modules --experimental-modules --harmony-dynamic-import const common = require('../common'); common.crashOnUnhandledRejection(); diff --git a/test/parallel/test-vm-module-errors.js b/test/parallel/test-vm-module-errors.js index 7dd89749a61c59..8bcb101ccc8e6d 100644 --- a/test/parallel/test-vm-module-errors.js +++ b/test/parallel/test-vm-module-errors.js @@ -1,6 +1,6 @@ 'use strict'; -// Flags: --experimental-modules +// Flags: --experimental-vm-modules const common = require('../common'); common.crashOnUnhandledRejection(); @@ -9,82 +9,92 @@ const assert = require('assert'); const { Module, createContext } = require('vm'); -async function rejects(fn, validateError) { +async function expectsRejection(fn, settings) { + const validateError = common.expectsError(settings); + // Retain async context. + const storedError = new Error('Thrown from:'); try { await fn(); } catch (err) { - validateError(err); + try { + validateError(err); + } catch (validationError) { + console.error(validationError); + console.error('Original error:'); + console.error(err); + throw storedError; + } return; } assert.fail('Missing expected exception'); } -async function createEmptyModule() { +async function createEmptyLinkedModule() { const m = new Module(''); await m.link(common.mustNotCall()); return m; } async function checkArgType() { - assert.throws(() => { + common.expectsError(() => { new Module(); - }, common.expectsError({ + }, { code: 'ERR_INVALID_ARG_TYPE', type: TypeError - })); + }); for (const invalidOptions of [ 0, 1, null, true, 'str', () => {}, Symbol.iterator ]) { - assert.throws(() => { + common.expectsError(() => { new Module('', invalidOptions); - }, common.expectsError({ + }, { code: 'ERR_INVALID_ARG_TYPE', type: TypeError - })); + }); } for (const invalidLinker of [ 0, 1, undefined, null, true, 'str', {}, Symbol.iterator ]) { - await rejects(async () => { + await expectsRejection(async () => { const m = new Module(''); await m.link(invalidLinker); - }, common.expectsError({ + }, { code: 'ERR_INVALID_ARG_TYPE', type: TypeError - })); + }); } } // Check methods/properties can only be used under a specific state. async function checkModuleState() { - await rejects(async () => { + await expectsRejection(async () => { const m = new Module(''); await m.link(common.mustNotCall()); assert.strictEqual(m.linkingStatus, 'linked'); await m.link(common.mustNotCall()); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_ALREADY_LINKED' - })); + }); - await rejects(async () => { + await expectsRejection(async () => { const m = new Module(''); m.link(common.mustNotCall()); assert.strictEqual(m.linkingStatus, 'linking'); await m.link(common.mustNotCall()); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_ALREADY_LINKED' - })); + }); - assert.throws(() => { + common.expectsError(() => { const m = new Module(''); m.instantiate(); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_NOT_LINKED' - })); + }); - await rejects(async () => { + await expectsRejection(async () => { const m = new Module('import "foo";'); try { await m.link(common.mustCall(() => ({}))); @@ -93,9 +103,9 @@ async function checkModuleState() { m.instantiate(); } assert.fail('Unreachable'); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_NOT_LINKED' - })); + }); { const m = new Module('import "foo";'); @@ -103,71 +113,71 @@ async function checkModuleState() { assert.strictEqual(module, m); assert.strictEqual(specifier, 'foo'); assert.strictEqual(m.linkingStatus, 'linking'); - assert.throws(() => { + common.expectsError(() => { m.instantiate(); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_NOT_LINKED' - })); - return createEmptyModule(); + }); + return new Module(''); })); m.instantiate(); await m.evaluate(); } - await rejects(async () => { + await expectsRejection(async () => { const m = new Module(''); await m.evaluate(); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must be one of instantiated, evaluated, and errored' - })); + }); - await rejects(async () => { - const m = await createEmptyModule(); + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); await m.evaluate(); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must be one of instantiated, evaluated, and errored' - })); + }); - assert.throws(() => { + common.expectsError(() => { const m = new Module(''); m.error; - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must be errored' - })); + }); - await rejects(async () => { - const m = await createEmptyModule(); + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); m.instantiate(); await m.evaluate(); m.error; - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must be errored' - })); + }); - assert.throws(() => { + common.expectsError(() => { const m = new Module(''); m.namespace; - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must not be uninstantiated or instantiating' - })); + }); - await rejects(async () => { - const m = await createEmptyModule(); + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); m.namespace; - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_STATUS', message: 'Module status must not be uninstantiated or instantiating' - })); + }); } // Check link() fails when the returned module is not valid. async function checkLinking() { - await rejects(async () => { + await expectsRejection(async () => { const m = new Module('import "foo";'); try { await m.link(common.mustCall(() => ({}))); @@ -176,11 +186,11 @@ async function checkLinking() { throw err; } assert.fail('Unreachable'); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_NOT_MODULE' - })); + }); - await rejects(async () => { + await expectsRejection(async () => { const c = createContext({ a: 1 }); const foo = new Module('', { context: c }); await foo.link(common.mustNotCall()); @@ -192,23 +202,32 @@ async function checkLinking() { throw err; } assert.fail('Unreachable'); - }, common.expectsError({ + }, { code: 'ERR_VM_MODULE_DIFFERENT_CONTEXT' - })); - - // await rejects(async () => { - // const m = new Module('import "foo";'); - // await m.link(common.mustCall(() => new Module(''))); - // }, common.expectsError({ - // code: 'ERR_VM_MODULE_NOT_LINKED' - // })); + }); + + await expectsRejection(async () => { + const erroredModule = new Module('import "foo";'); + try { + await erroredModule.link(common.mustCall(() => ({}))); + } catch (err) { + // ignored + } finally { + assert.strictEqual(erroredModule.linkingStatus, 'errored'); + } + + const rootModule = new Module('import "errored";'); + await rootModule.link(common.mustCall(() => erroredModule)); + }, { + code: 'ERR_VM_MODULE_LINKING_ERRORED' + }); } // Check the JavaScript engine deals with exceptions correctly async function checkExecution() { await (async () => { const m = new Module('import { nonexistent } from "module";'); - await m.link(common.mustCall(createEmptyModule)); + await m.link(common.mustCall(() => new Module(''))); // There is no code for this exception since it is thrown by the JavaScript // engine. diff --git a/test/parallel/test-vm-module-link.js b/test/parallel/test-vm-module-link.js index 893f11e72b8d55..870427e91be4bf 100644 --- a/test/parallel/test-vm-module-link.js +++ b/test/parallel/test-vm-module-link.js @@ -1,6 +1,6 @@ 'use strict'; -// Flags: --experimental-modules +// Flags: --experimental-vm-modules const common = require('../common'); common.crashOnUnhandledRejection(); diff --git a/test/parallel/test-vm-module-reevaluate.js b/test/parallel/test-vm-module-reevaluate.js index 1b4376d6e9773f..e4f5858800e297 100644 --- a/test/parallel/test-vm-module-reevaluate.js +++ b/test/parallel/test-vm-module-reevaluate.js @@ -1,6 +1,6 @@ 'use strict'; -// Flags: --experimental-modules +// Flags: --experimental-vm-modules const common = require('../common'); common.crashOnUnhandledRejection(); From 2ee1411ff0fde4a20e8dcec9126e57fb7c87309c Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 21 Jan 2018 20:53:26 -0600 Subject: [PATCH 11/12] use safe weak maps --- lib/internal/safe_globals.js | 1 + lib/internal/vm/Module.js | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/internal/safe_globals.js b/lib/internal/safe_globals.js index ad58fa662b53ef..1241b1b3581fd6 100644 --- a/lib/internal/safe_globals.js +++ b/lib/internal/safe_globals.js @@ -24,3 +24,4 @@ const makeSafe = (unsafe, safe) => { exports.SafeMap = makeSafe(Map, class SafeMap extends Map {}); exports.SafeSet = makeSafe(Set, class SafeSet extends Set {}); exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {}); +exports.SafeWeakMap = makeSafe(WeakMap, class SafeWeakMap extends WeakMap {}); diff --git a/lib/internal/vm/Module.js b/lib/internal/vm/Module.js index a8625ef76be613..c05a7eb76833ad 100644 --- a/lib/internal/vm/Module.js +++ b/lib/internal/vm/Module.js @@ -8,6 +8,7 @@ const { getConstructorOf, customInspectSymbol, } = require('internal/util'); +const { SafeWeakMap } = require('internal/safe_globals'); const { ModuleWrap, @@ -29,10 +30,10 @@ const STATUS_MAP = { }; let globalModuleId = 0; -const perContextModuleId = new WeakMap(); -const wrapMap = new WeakMap(); -const dependencyCacheMap = new WeakMap(); -const linkingStatusMap = new WeakMap(); +const perContextModuleId = new SafeWeakMap(); +const wrapMap = new SafeWeakMap(); +const dependencyCacheMap = new SafeWeakMap(); +const linkingStatusMap = new SafeWeakMap(); class Module { constructor(src, options = {}) { From dfbd67dcccd300cbfdfbfc876b39931b299c00c4 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 21 Jan 2018 23:13:35 -0800 Subject: [PATCH 12/12] =?UTF-8?q?Address=20comments=20by=20Micha=C3=ABl=20?= =?UTF-8?q?and=20John-David?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/api/vm.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 458c304b831b03..516e18b6a0fc6c 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -50,6 +50,9 @@ added: REPLACEME > Stability: 1 - Experimental +*This feature is only available with the `--experimental-vm-modules` command +flag enabled.* + The `vm.Module` class provides a low-level interface for using ECMAScript modules in VM contexts. It is the counterpart of the `vm.Script` class that closely mirrors [Source Text Module Record][]s as defined in the ECMAScript @@ -58,16 +61,13 @@ specification. Unlike `vm.Script` however, every `vm.Module` object is bound to a context from its creation. Operations on `vm.Module` objects are intrinsically asynchronous, in contrast with the synchronous nature of `vm.Script` objects. With the help -of async functions, however, manipulating `vm.Module` objects are fairly +of async functions, however, manipulating `vm.Module` objects is fairly straightforward. Using a `vm.Module` object requires four distinct steps: creation/parsing, -linking, instantiating, and evaluation. These four steps are illustrated in the +linking, instantiation, and evaluation. These four steps are illustrated in the following example. -This feature is only available with the `--experimental-vm-modules` command -flag enabled. - *Note*: This implementation lies at a lower level than the [ECMAScript Module loader][]. There is also currently no way to interact with the Loader, though support is planned. @@ -89,8 +89,8 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); // put it into local binding "secret". const bar = new vm.Module(` - import secret from 'foo'; - secret; + import s from 'foo'; + s; `, { context: contextifiedSandbox }); @@ -123,7 +123,10 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); // The "secret" variable refers to the global variable we added to // "contextifiedSandbox" when creating the context. export default secret; - `, { context: contextifiedSandbox }); + `, { context: referencingModule.context }); + + // Using `contextifiedSandbox` instead of `referencingModule.context` + // here would work as well. } throw new Error(`Unable to resolve dependency: ${specifier}`); } @@ -144,8 +147,8 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); // // Evaluate the Module. The evaluate() method returns a Promise with a single // property "result" that contains the result of the very last statement - // executed in the Module. In the case of `bar`, it is `secret;`, which refers - // to the default export of the `foo` module, the `secret` we set in the + // executed in the Module. In the case of `bar`, it is `s;`, which refers to + // the default export of the `foo` module, the `secret` we set in the // beginning to 42. const { result } = await bar.evaluate();