From 11f137c3cb0e811138530f384d0c933aaa9f9969 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 29 Aug 2024 11:05:03 +0200 Subject: [PATCH] vm: introduce vanilla contexts via vm.constants.DONT_CONTEXTIFY This implements a flavor of vm.createContext() and friends that creates a context without contextifying its global object. This is suitable when users want to freeze the context (impossible when the global is contextified i.e. has interceptors installed) or speed up the global access if they don't need the interceptor behavior. ```js const vm = require('node:vm'); const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); // In contexts with contextified global objects, this is false. // In vanilla contexts this is true. console.log(vm.runInContext('globalThis', context) === context); // In contexts with contextified global objects, this would throw, // but in vanilla contexts freezing the global object works. vm.runInContext('Object.freeze(globalThis);', context); // In contexts with contextified global objects, freezing throws // and won't be effective. In vanilla contexts, freezing works // and prevents scripts from accidentally leaking globals. try { vm.runInContext('globalThis.foo = 1; foo;', context); } catch(e) { console.log(e); // Uncaught ReferenceError: foo is not defined } console.log(context.Array); // [Function: Array] ``` PR-URL: https://github.com/nodejs/node/pull/54394 Reviewed-By: James M Snell Reviewed-By: Chengzhong Wu --- doc/api/vm.md | 166 +++++++++++++--- lib/vm.js | 10 +- src/env_properties.h | 1 + src/node_contextify.cc | 103 ++++++---- src/node_contextify.h | 12 +- .../test-vm-context-dont-contextify.js | 185 ++++++++++++++++++ tools/doc/type-parser.mjs | 2 + 7 files changed, 410 insertions(+), 69 deletions(-) create mode 100644 test/parallel/test-vm-context-dont-contextify.js diff --git a/doc/api/vm.md b/doc/api/vm.md index 07f913e9b14aa1..b8356d538357f9 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -228,6 +228,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 @@ -274,9 +278,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 @@ -294,6 +305,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])` @@ -1063,6 +1080,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 @@ -1113,10 +1136,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. @@ -1141,6 +1164,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 @@ -1160,7 +1188,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])` @@ -1320,6 +1349,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.'`. @@ -1394,13 +1428,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`. @@ -1415,6 +1457,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])` @@ -1541,13 +1588,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. +const 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 @@ -1837,6 +1956,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 34814c430d3253..cb64404ef0bee2 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 178f3fdfc86beb..986cd3b4c03167 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -52,6 +52,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 e6111a0ad44e5b..8951cd378a9025 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -111,9 +111,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 = @@ -208,7 +214,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 @@ -240,6 +246,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 @@ -258,8 +265,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. @@ -281,16 +293,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 @@ -319,23 +334,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; } @@ -368,18 +387,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(); @@ -412,6 +434,12 @@ 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); + } } void ContextifyContext::WeakCallback( @@ -422,14 +450,13 @@ void ContextifyContext::WeakCallback( // 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 e96df803b7ec2a..517e3f44d32490 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 fdf70bd935623d..ba499b7b3b1939 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -234,6 +234,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',