diff --git a/doc/api/vm.md b/doc/api/vm.md index 19e8aff57ea33e..30dbf724158792 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -229,6 +229,9 @@ overhead. -* `contextObject` {Object} An object that will be [contextified][]. If - `undefined`, a new object will be created. +* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined} + Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][]. + If `undefined`, an empty contextified object will be created for backwards compatibility. * `options` {Object} * `displayErrors` {boolean} When `true`, if an [`Error`][] occurs while compiling the `code`, the line of code causing the error is attached @@ -275,9 +279,16 @@ changes: `breakOnSigint` scopes in that case. * Returns: {any} the result of the very last statement executed in the script. -First contextifies the given `contextObject`, runs the compiled code contained -by the `vm.Script` object within the created context, and returns the result. -Running code does not have access to local scope. +This method is a shortcut to `script.runInContext(vm.createContext(options), options)`. +It does several things at once: + +1. Creates a new context. +2. If `contextObject` is an object, [contextifies][contextified] it with the new context. + If `contextObject` is undefined, creates a new object and [contextifies][contextified] it. + If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything. +3. Runs the compiled code contained by the `vm.Script` object within the created context. The code + does not have access to the scope in which this method is called. +4. Returns the result. The following example compiles code that sets a global variable, then executes the code multiple times in different contexts. The globals are set on and @@ -295,6 +306,12 @@ contexts.forEach((context) => { console.log(contexts); // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }] + +// This would throw if the context is created from a contextified object. +// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary +// global objects that can be frozen. +const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;'); +const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY); ``` ### `script.runInThisContext([options])` @@ -1072,6 +1089,10 @@ For detailed information, see -* `contextObject` {Object} +* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined} + Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][]. + If `undefined`, an empty contextified object will be created for backwards compatibility. * `options` {Object} * `name` {string} Human-readable name of the newly created context. **Default:** `'VM Context i'`, where `i` is an ascending numerical index of @@ -1124,10 +1147,10 @@ changes: [Support of dynamic `import()` in compilation APIs][]. * Returns: {Object} contextified object. -If given a `contextObject`, the `vm.createContext()` method will [prepare that +If the given `contextObject` is an object, the `vm.createContext()` method will [prepare that object][contextified] and return a reference to it so that it can be used in calls to [`vm.runInContext()`][] or [`script.runInContext()`][]. Inside such -scripts, the `contextObject` will be the global object, retaining all of its +scripts, the global object will be wrapped by the `contextObject`, retaining all of its existing properties but also having the built-in objects and functions any standard [global object][] has. Outside of scripts run by the vm module, global variables will remain unchanged. @@ -1152,6 +1175,11 @@ console.log(global.globalVar); If `contextObject` is omitted (or passed explicitly as `undefined`), a new, empty [contextified][] object will be returned. +When the global object in the newly created context is [contextified][], it has some quirks +compared to ordinary global objects. For example, it cannot be frozen. To create a context +without the contextifying quirks, pass [`vm.constants.DONT_CONTEXTIFY`][] as the `contextObject` +argument. See the documentation of [`vm.constants.DONT_CONTEXTIFY`][] for details. + The `vm.createContext()` method is primarily useful for creating a single context that can be used to run multiple scripts. For instance, if emulating a web browser, the method can be used to create a single context representing a @@ -1171,7 +1199,8 @@ added: v0.11.7 * Returns: {boolean} Returns `true` if the given `object` object has been [contextified][] using -[`vm.createContext()`][]. +[`vm.createContext()`][], or if it's the global object of a context created +using [`vm.constants.DONT_CONTEXTIFY`][]. ## `vm.measureMemory([options])` @@ -1332,6 +1361,10 @@ console.log(contextObject); * `code` {string} The JavaScript code to compile and run. -* `contextObject` {Object} An object that will be [contextified][]. If - `undefined`, a new object will be created. +* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined} + Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][]. + If `undefined`, an empty contextified object will be created for backwards compatibility. * `options` {Object|string} * `filename` {string} Specifies the filename used in stack traces produced by this script. **Default:** `'evalmachine.'`. @@ -1407,13 +1441,21 @@ changes: `breakOnSigint` scopes in that case. * Returns: {any} the result of the very last statement executed in the script. -The `vm.runInNewContext()` first contextifies the given `contextObject` (or -creates a new `contextObject` if passed as `undefined`), compiles the `code`, -runs it within the created context, then returns the result. Running code -does not have access to the local scope. - +This method is a shortcut to +`(new vm.Script(code, options)).runInContext(vm.createContext(options), options)`. If `options` is a string, then it specifies the filename. +It does several things at once: + +1. Creates a new context. +2. If `contextObject` is an object, [contextifies][contextified] it with the new context. + If `contextObject` is undefined, creates a new object and [contextifies][contextified] it. + If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything. +3. Compiles the code as a`vm.Script` +4. Runs the compield code within the created context. The code does not have access to the scope in + which this method is called. +5. Returns the result. + The following example compiles and executes code that increments a global variable and sets a new one. These globals are contained in the `contextObject`. @@ -1428,6 +1470,11 @@ const contextObject = { vm.runInNewContext('count += 1; name = "kitty"', contextObject); console.log(contextObject); // Prints: { animal: 'cat', count: 3, name: 'kitty' } + +// This would throw if the context is created from a contextified object. +// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that +// can be frozen. +const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY); ``` ## `vm.runInThisContext(code[, options])` @@ -1555,13 +1602,85 @@ According to the [V8 Embedder's Guide][]: > JavaScript applications to run in a single instance of V8. You must explicitly > specify the context in which you want any JavaScript code to be run. -When the method `vm.createContext()` is called, the `contextObject` argument -(or a newly-created object if `contextObject` is `undefined`) is associated -internally with a new instance of a V8 Context. This V8 Context provides the -`code` run using the `node:vm` module's methods with an isolated global -environment within which it can operate. The process of creating the V8 Context -and associating it with the `contextObject` is what this document refers to as -"contextifying" the object. +When the method `vm.createContext()` is called with an object, the `contextObject` argument +will be used to wrap the global object of a new instance of a V8 Context +(if `contextObject` is `undefined`, a new object will be created from the current context +before its contextified). This V8 Context provides the `code` run using the `node:vm` +module's methods with an isolated global environment within which it can operate. +The process of creating the V8 Context and associating it with the `contextObject` +in the outer context is what this document refers to as "contextifying" the object. + +The contextifying would introduce some quirks to the `globalThis` value in the context. +For example, it cannot be frozen, and it is not reference equal to the `contextObject` +in the outer context. + +```js +const vm = require('node:vm'); + +// An undefined `contextObject` option makes the global object contextified. +let context = vm.createContext(); +console.log(vm.runInContext('globalThis', context) === context); // false +// A contextified global object cannot be frozen. +try { + vm.runInContext('Object.freeze(globalThis);', context); +} catch(e) { + console.log(e); // TypeError: Cannot freeze +} +console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1 +``` + +To create a context with an ordinary global object and get access to a global proxy in +the outer context with fewer quirks, specify `vm.constants.DONT_CONTEXTIFY` as the +`contextObject` argument. + +### `vm.constants.DONT_CONTEXTIFY` + +This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create +a context without wrapping its global object with another object in a Node.js-specific manner. +As a result, the `globalThis` value inside the new context would behave more closely to an ordinary +one. + +```js +const vm = require('node:vm'); + +// Use vm.constants.DONT_CONTEXTIFY to freeze the global object. +const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); +vm.runInContext('Object.freeze(globalThis);', context); +try { + vm.runInContext('bar = 1; bar;', context); +} catch(e) { + console.log(e); // Uncaught ReferenceError: bar is not defined +} +``` + +When `vm.constants.DONT_CONTEXTIFY` is used as the `contextObject` argument to [`vm.createContext()`][], +the returned object is a proxy-like object to the global object in the newly created context with +fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context, +can be modified from outside the context, and can be used to access built-ins in the new context directly. + +```js +const vm = require('node:vm'); + +const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + +// Returned object is reference equal to globalThis in the new context. +console.log(vm.runInContext('globalThis', context) === context); // true + +// Can be used to access globals in the new context directly. +console.log(context.Array); // [Function: Array] +vm.runInContext('foo = 1;', context); +console.log(context.foo); // 1 +context.bar = 1; +console.log(vm.runInContext('bar;', context)); // 1 + +// Can be frozen and it affects the inner context. +Object.freeze(context); +try { + vm.runInContext('baz = 1; baz;', context); +} catch(e) { + console.log(e); // Uncaught ReferenceError: baz is not defined +} +``` ## Timeout interactions with asynchronous tasks and Promises @@ -1851,6 +1970,7 @@ const { Script, SyntheticModule } = require('node:vm'); [`script.runInThisContext()`]: #scriptruninthiscontextoptions [`url.origin`]: url.md#urlorigin [`vm.compileFunction()`]: #vmcompilefunctioncode-params-options +[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify [`vm.createContext()`]: #vmcreatecontextcontextobject-options [`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options [`vm.runInThisContext()`]: #vmruninthiscontextcode-options diff --git a/lib/vm.js b/lib/vm.js index e1ad4e30c33b66..3eea66b3f07437 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -65,6 +65,7 @@ const { } = require('internal/vm'); const { vm_dynamic_import_main_context_default, + vm_context_no_contextify, } = internalBinding('symbols'); const kParsingContext = Symbol('script parsing context'); @@ -222,7 +223,7 @@ function getContextOptions(options) { let defaultContextNameIndex = 1; function createContext(contextObject = {}, options = kEmptyObject) { - if (isContext(contextObject)) { + if (contextObject !== vm_context_no_contextify && isContext(contextObject)) { return contextObject; } @@ -258,10 +259,10 @@ function createContext(contextObject = {}, options = kEmptyObject) { const hostDefinedOptionId = getHostDefinedOptionId(importModuleDynamically, name); - makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId); + const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId); // Register the context scope callback after the context was initialized. - registerImportModuleDynamically(contextObject, importModuleDynamically); - return contextObject; + registerImportModuleDynamically(result, importModuleDynamically); + return result; } function createScript(code, options) { @@ -394,6 +395,7 @@ function measureMemory(options = kEmptyObject) { const vmConstants = { __proto__: null, USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default, + DONT_CONTEXTIFY: vm_context_no_contextify, }; ObjectFreeze(vmConstants); diff --git a/src/env_properties.h b/src/env_properties.h index 1ed8ab4d116313..9bfe077abfb0cd 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -57,6 +57,7 @@ V(resource_symbol, "resource_symbol") \ V(trigger_async_id_symbol, "trigger_async_id_symbol") \ V(source_text_module_default_hdo, "source_text_module_default_hdo") \ + V(vm_context_no_contextify, "vm_context_no_contextify") \ V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \ V(vm_dynamic_import_main_context_default, \ "vm_dynamic_import_main_context_default") \ diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 895f7b9d096166..bc90501da0ffde 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -118,9 +118,15 @@ Local Uint32ToName(Local context, uint32_t index) { BaseObjectPtr ContextifyContext::New( Environment* env, Local sandbox_obj, ContextOptions* options) { + Local object_template; HandleScope scope(env->isolate()); - Local object_template = env->contextify_global_template(); - DCHECK(!object_template.IsEmpty()); + CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla); + if (!sandbox_obj.IsEmpty()) { + // Do not use the template with interceptors for vanilla contexts. + object_template = env->contextify_global_template(); + DCHECK(!object_template.IsEmpty()); + } + const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data(); MicrotaskQueue* queue = @@ -217,7 +223,7 @@ MaybeLocal ContextifyContext::CreateV8Context( EscapableHandleScope scope(isolate); Local ctx; - if (snapshot_data == nullptr) { + if (object_template.IsEmpty() || snapshot_data == nullptr) { ctx = Context::New( isolate, nullptr, // extensions @@ -249,6 +255,7 @@ BaseObjectPtr ContextifyContext::New( Local sandbox_obj, ContextOptions* options) { HandleScope scope(env->isolate()); + CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla); // This only initializes part of the context. The primordials are // only initialized when needed because even deserializing them slows // things down significantly and they are only needed in rare occasions @@ -267,8 +274,13 @@ BaseObjectPtr ContextifyContext::New( // embedder data field. The sandbox uses a private symbol to hold a reference // to the ContextifyContext wrapper which in turn internally references // the context from its constructor. - v8_context->SetEmbedderData(ContextEmbedderIndex::kSandboxObject, - sandbox_obj); + if (sandbox_obj.IsEmpty()) { + v8_context->SetEmbedderData(ContextEmbedderIndex::kSandboxObject, + v8::Undefined(env->isolate())); + } else { + v8_context->SetEmbedderData(ContextEmbedderIndex::kSandboxObject, + sandbox_obj); + } // Delegate the code generation validation to // node::ModifyCodeGenerationFromStrings. @@ -290,16 +302,19 @@ BaseObjectPtr ContextifyContext::New( Local wrapper; { Context::Scope context_scope(v8_context); - Local ctor_name = sandbox_obj->GetConstructorName(); - if (!ctor_name->Equals(v8_context, env->object_string()).FromMaybe(false) && - new_context_global - ->DefineOwnProperty( - v8_context, - v8::Symbol::GetToStringTag(env->isolate()), - ctor_name, - static_cast(v8::DontEnum)) - .IsNothing()) { - return BaseObjectPtr(); + if (!sandbox_obj.IsEmpty()) { + Local ctor_name = sandbox_obj->GetConstructorName(); + if (!ctor_name->Equals(v8_context, env->object_string()) + .FromMaybe(false) && + new_context_global + ->DefineOwnProperty( + v8_context, + v8::Symbol::GetToStringTag(env->isolate()), + ctor_name, + static_cast(v8::DontEnum)) + .IsNothing()) { + return BaseObjectPtr(); + } } // Assign host_defined_options_id to the global object so that in the @@ -328,23 +343,27 @@ BaseObjectPtr ContextifyContext::New( result->MakeWeak(); } - if (sandbox_obj + Local wrapper_holder = + sandbox_obj.IsEmpty() ? new_context_global : sandbox_obj; + if (!wrapper_holder.IsEmpty() && + wrapper_holder ->SetPrivate( v8_context, env->contextify_context_private_symbol(), wrapper) .IsNothing()) { return BaseObjectPtr(); } - // Assign host_defined_options_id to the sandbox object so that module - // callbacks like importModuleDynamically can be registered once back to the - // JS land. - if (sandbox_obj + + // Assign host_defined_options_id to the sandbox object or the global object + // (for vanilla contexts) so that module callbacks like + // importModuleDynamically can be registered once back to the JS land. + if (!sandbox_obj.IsEmpty() && + sandbox_obj ->SetPrivate(v8_context, env->host_defined_option_symbol(), options->host_defined_options_id) .IsNothing()) { return BaseObjectPtr(); } - return result; } @@ -378,18 +397,21 @@ void ContextifyContext::RegisterExternalReferences( // makeContext(sandbox, name, origin, strings, wasm); void ContextifyContext::MakeContext(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + ContextOptions options; CHECK_EQ(args.Length(), 7); - CHECK(args[0]->IsObject()); - Local sandbox = args[0].As(); - - // Don't allow contextifying a sandbox multiple times. - CHECK( - !sandbox->HasPrivate( - env->context(), - env->contextify_context_private_symbol()).FromJust()); - - ContextOptions options; + Local sandbox; + if (args[0]->IsObject()) { + sandbox = args[0].As(); + // Don't allow contextifying a sandbox multiple times. + CHECK(!sandbox + ->HasPrivate(env->context(), + env->contextify_context_private_symbol()) + .FromJust()); + } else { + CHECK(args[0]->IsSymbol()); + options.vanilla = true; + } CHECK(args[1]->IsString()); options.name = args[1].As(); @@ -422,18 +444,23 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo& args) { try_catch.ReThrow(); return; } + + if (sandbox.IsEmpty()) { + args.GetReturnValue().Set(context_ptr->context()->Global()); + } else { + args.GetReturnValue().Set(sandbox); + } } // static ContextifyContext* ContextifyContext::ContextFromContextifiedSandbox( - Environment* env, - const Local& sandbox) { - Local context_global; - if (sandbox + Environment* env, const Local& wrapper_holder) { + Local contextify; + if (wrapper_holder ->GetPrivate(env->context(), env->contextify_context_private_symbol()) - .ToLocal(&context_global) && - context_global->IsObject()) { - return Unwrap(context_global.As()); + .ToLocal(&contextify) && + contextify->IsObject()) { + return Unwrap(contextify.As()); } return nullptr; } diff --git a/src/node_contextify.h b/src/node_contextify.h index b9e846f70bad4f..2581db0d7df568 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -19,6 +19,7 @@ struct ContextOptions { v8::Local allow_code_gen_wasm; std::unique_ptr own_microtask_queue; v8::Local host_defined_options_id; + bool vanilla = false; }; class ContextifyContext : public BaseObject { @@ -43,8 +44,7 @@ class ContextifyContext : public BaseObject { static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static ContextifyContext* ContextFromContextifiedSandbox( - Environment* env, - const v8::Local& sandbox); + Environment* env, const v8::Local& wrapper_holder); inline v8::Local context() const { return PersistentToLocal::Default(env()->isolate(), context_); @@ -55,8 +55,12 @@ class ContextifyContext : public BaseObject { } inline v8::Local sandbox() const { - return context()->GetEmbedderData(ContextEmbedderIndex::kSandboxObject) - .As(); + // Only vanilla contexts have undefined sandboxes. sandbox() is only used by + // interceptors who are not supposed to be called on vanilla contexts. + v8::Local result = + context()->GetEmbedderData(ContextEmbedderIndex::kSandboxObject); + CHECK(!result->IsUndefined()); + return result.As(); } inline v8::MicrotaskQueue* microtask_queue() const { diff --git a/test/parallel/test-vm-context-dont-contextify.js b/test/parallel/test-vm-context-dont-contextify.js new file mode 100644 index 00000000000000..6cbd62e8947b3d --- /dev/null +++ b/test/parallel/test-vm-context-dont-contextify.js @@ -0,0 +1,185 @@ +'use strict'; + +// Check vm.constants.DONT_CONTEXTIFY works. + +const common = require('../common'); + +const assert = require('assert'); +const vm = require('vm'); +const fixtures = require('../common/fixtures'); + +{ + // Check identity of the returned object. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + // The globalThis in the new context should be reference equal to the returned object. + assert.strictEqual(vm.runInContext('globalThis', context), context); + assert(vm.isContext(context)); + assert.strictEqual(typeof context.Array, 'function'); // Can access builtins directly. + assert.deepStrictEqual(Object.keys(context), []); // Properties on the global proxy are not enumerable +} + +{ + // Check that vm.createContext can return the original context if re-passed. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const context2 = new vm.createContext(context); + assert.strictEqual(context, context2); +} + +{ + // Check that the context is vanilla and that Script.runInContext works. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const result = + new vm.Script('globalThis.hey = 1; Object.freeze(globalThis); globalThis.process') + .runInContext(context); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check Script.runInNewContext works. + const result = + new vm.Script('globalThis.hey = 1; Object.freeze(globalThis); globalThis.process') + .runInNewContext(vm.constants.DONT_CONTEXTIFY); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check that vm.runInNewContext() works + const result = vm.runInNewContext( + 'globalThis.hey = 1; Object.freeze(globalThis); globalThis.process', + vm.constants.DONT_CONTEXTIFY); + assert.strictEqual(globalThis.hey, undefined); // Should not leak into current context. + assert.strictEqual(result, undefined); // Vanilla context has no Node.js globals +} + +{ + // Check that the global object of vanilla contexts work as expected. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + + // Check mutation via globalThis. + vm.runInContext('globalThis.foo = 1;', context); + assert.strictEqual(globalThis.foo, undefined); // Should not pollute the current context. + assert.strictEqual(context.foo, 1); + assert.strictEqual(vm.runInContext('globalThis.foo', context), 1); + assert.strictEqual(vm.runInContext('foo', context), 1); + + // Check mutation from outside. + context.foo = 2; + assert.strictEqual(context.foo, 2); + assert.strictEqual(vm.runInContext('globalThis.foo', context), 2); + assert.strictEqual(vm.runInContext('foo', context), 2); + + // Check contextual mutation. + vm.runInContext('bar = 1;', context); + assert.strictEqual(globalThis.bar, undefined); // Should not pollute the current context. + assert.strictEqual(context.bar, 1); + assert.strictEqual(vm.runInContext('globalThis.bar', context), 1); + assert.strictEqual(vm.runInContext('bar', context), 1); + + // Check adding new property from outside. + context.baz = 1; + assert.strictEqual(context.baz, 1); + assert.strictEqual(vm.runInContext('globalThis.baz', context), 1); + assert.strictEqual(vm.runInContext('baz', context), 1); + + // Check mutation via Object.defineProperty(). + vm.runInContext('Object.defineProperty(globalThis, "qux", {' + + 'enumerable: false, configurable: false, get() { return 1; } })', context); + assert.strictEqual(globalThis.qux, undefined); // Should not pollute the current context. + assert.strictEqual(context.qux, 1); + assert.strictEqual(vm.runInContext('qux', context), 1); + const desc = Object.getOwnPropertyDescriptor(context, 'qux'); + assert.strictEqual(desc.enumerable, false); + assert.strictEqual(desc.configurable, false); + assert.strictEqual(typeof desc.get, 'function'); + assert.throws(() => { context.qux = 1; }, { name: 'TypeError' }); + assert.throws(() => { Object.defineProperty(context, 'qux', { value: 1 }); }, { name: 'TypeError' }); + // Setting a value without a setter fails silently. + assert.strictEqual(vm.runInContext('qux = 2; qux', context), 1); + assert.throws(() => { + vm.runInContext('Object.defineProperty(globalThis, "qux", { value: 1 });'); + }, { name: 'TypeError' }); +} + +function checkFrozen(context) { + // Check mutation via globalThis. + vm.runInContext('globalThis.foo = 1', context); // Invoking setters on freezed object fails silently. + assert.strictEqual(context.foo, undefined); + assert.strictEqual(vm.runInContext('globalThis.foo', context), undefined); + assert.throws(() => { + vm.runInContext('foo', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check mutation from outside. + assert.throws(() => { + context.foo = 2; + }, { name: 'TypeError' }); + assert.strictEqual(context.foo, undefined); + assert.strictEqual(vm.runInContext('globalThis.foo', context), undefined); + assert.throws(() => { + vm.runInContext('foo', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check contextual mutation. + vm.runInContext('bar = 1', context); // Invoking setters on freezed object fails silently. + assert.strictEqual(context.bar, undefined); + assert.strictEqual(vm.runInContext('globalThis.bar', context), undefined); + assert.throws(() => { + vm.runInContext('bar', context); // It should not be looked up contextually. + }, { + name: 'ReferenceError' + }); + + // Check mutation via Object.defineProperty(). + assert.throws(() => { + vm.runInContext('Object.defineProperty(globalThis, "qux", {' + + 'enumerable: false, configurable: false, get() { return 1; } })', context); + }, { + name: 'TypeError' + }); + assert.strictEqual(context.qux, undefined); + assert.strictEqual(vm.runInContext('globalThis.qux', context), undefined); + assert.strictEqual(Object.getOwnPropertyDescriptor(context, 'qux'), undefined); + assert.throws(() => { Object.defineProperty(context, 'qux', { value: 1 }); }, { name: 'TypeError' }); + assert.throws(() => { + vm.runInContext('qux', context); + }, { + name: 'ReferenceError' + }); +} + +{ + // Check freezing the vanilla context's global object from within the context. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + // Only vanilla contexts' globals can be freezed. Contextified global objects cannot be freezed + // due to the presence of interceptors. + vm.runInContext('Object.freeze(globalThis)', context); + checkFrozen(context); +} + +{ + // Check freezing the vanilla context's global object from outside the context. + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + Object.freeze(context); + checkFrozen(context); +} + +// Check importModuleDynamically works. +(async function() { + { + const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs'); + const namespace = await import(moduleUrl.href); + // Check dynamic import works + const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); + const script = new vm.Script(`import('${encodeURI(moduleUrl.href)}')`, { + importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, + }); + const promise = script.runInContext(context); + assert.strictEqual(await promise, namespace); + } +})().catch(common.mustNotCall()); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index c548e2ccff8276..6b94a94283ccb2 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -236,6 +236,8 @@ const customTypesMap = { 'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule', 'vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER': 'vm.html#vmconstantsuse_main_context_default_loader', + 'vm.constants.DONT_CONTEXTIFY': + 'vm.html#vmconstantsdont_contextify', 'MessagePort': 'worker_threads.html#class-messageport', 'Worker': 'worker_threads.html#class-worker',