mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 13:09:21 +01:00
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 <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
parent
dcf50f15bc
commit
d98cfcc581
166
doc/api/vm.md
166
doc/api/vm.md
@ -229,6 +229,9 @@ overhead.
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/54394
|
||||
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
|
||||
- version: v14.6.0
|
||||
pr-url: https://github.com/nodejs/node/pull/34023
|
||||
description: The `microtaskMode` option is supported now.
|
||||
@ -240,8 +243,9 @@ changes:
|
||||
description: The `breakOnSigint` option is supported now.
|
||||
-->
|
||||
|
||||
* `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
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/54394
|
||||
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
|
||||
- version:
|
||||
- v21.7.0
|
||||
- v20.12.0
|
||||
@ -1094,7 +1115,9 @@ changes:
|
||||
description: The `codeGeneration` option is supported now.
|
||||
-->
|
||||
|
||||
* `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);
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/54394
|
||||
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
|
||||
- version:
|
||||
- v21.7.0
|
||||
- v20.12.0
|
||||
@ -1356,8 +1389,9 @@ changes:
|
||||
-->
|
||||
|
||||
* `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.<anonymous>'`.
|
||||
@ -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
|
||||
|
10
lib/vm.js
10
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);
|
||||
|
@ -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") \
|
||||
|
@ -118,9 +118,15 @@ Local<Name> Uint32ToName(Local<Context> context, uint32_t index) {
|
||||
|
||||
BaseObjectPtr<ContextifyContext> ContextifyContext::New(
|
||||
Environment* env, Local<Object> sandbox_obj, ContextOptions* options) {
|
||||
Local<ObjectTemplate> object_template;
|
||||
HandleScope scope(env->isolate());
|
||||
Local<ObjectTemplate> 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<Context> ContextifyContext::CreateV8Context(
|
||||
EscapableHandleScope scope(isolate);
|
||||
|
||||
Local<Context> ctx;
|
||||
if (snapshot_data == nullptr) {
|
||||
if (object_template.IsEmpty() || snapshot_data == nullptr) {
|
||||
ctx = Context::New(
|
||||
isolate,
|
||||
nullptr, // extensions
|
||||
@ -249,6 +255,7 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
|
||||
Local<Object> 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> 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> ContextifyContext::New(
|
||||
Local<Object> wrapper;
|
||||
{
|
||||
Context::Scope context_scope(v8_context);
|
||||
Local<String> 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::PropertyAttribute>(v8::DontEnum))
|
||||
.IsNothing()) {
|
||||
return BaseObjectPtr<ContextifyContext>();
|
||||
if (!sandbox_obj.IsEmpty()) {
|
||||
Local<String> 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::PropertyAttribute>(v8::DontEnum))
|
||||
.IsNothing()) {
|
||||
return BaseObjectPtr<ContextifyContext>();
|
||||
}
|
||||
}
|
||||
|
||||
// Assign host_defined_options_id to the global object so that in the
|
||||
@ -328,23 +343,27 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
|
||||
result->MakeWeak();
|
||||
}
|
||||
|
||||
if (sandbox_obj
|
||||
Local<Object> 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<ContextifyContext>();
|
||||
}
|
||||
// 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<ContextifyContext>();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -378,18 +397,21 @@ void ContextifyContext::RegisterExternalReferences(
|
||||
// makeContext(sandbox, name, origin, strings, wasm);
|
||||
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
ContextOptions options;
|
||||
|
||||
CHECK_EQ(args.Length(), 7);
|
||||
CHECK(args[0]->IsObject());
|
||||
Local<Object> sandbox = args[0].As<Object>();
|
||||
|
||||
// Don't allow contextifying a sandbox multiple times.
|
||||
CHECK(
|
||||
!sandbox->HasPrivate(
|
||||
env->context(),
|
||||
env->contextify_context_private_symbol()).FromJust());
|
||||
|
||||
ContextOptions options;
|
||||
Local<Object> sandbox;
|
||||
if (args[0]->IsObject()) {
|
||||
sandbox = args[0].As<Object>();
|
||||
// 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<String>();
|
||||
@ -422,18 +444,23 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& 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<Object>& sandbox) {
|
||||
Local<Value> context_global;
|
||||
if (sandbox
|
||||
Environment* env, const Local<Object>& wrapper_holder) {
|
||||
Local<Value> contextify;
|
||||
if (wrapper_holder
|
||||
->GetPrivate(env->context(), env->contextify_context_private_symbol())
|
||||
.ToLocal(&context_global) &&
|
||||
context_global->IsObject()) {
|
||||
return Unwrap<ContextifyContext>(context_global.As<Object>());
|
||||
.ToLocal(&contextify) &&
|
||||
contextify->IsObject()) {
|
||||
return Unwrap<ContextifyContext>(contextify.As<Object>());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ struct ContextOptions {
|
||||
v8::Local<v8::Boolean> allow_code_gen_wasm;
|
||||
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
|
||||
v8::Local<v8::Symbol> 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<v8::Object>& sandbox);
|
||||
Environment* env, const v8::Local<v8::Object>& wrapper_holder);
|
||||
|
||||
inline v8::Local<v8::Context> context() const {
|
||||
return PersistentToLocal::Default(env()->isolate(), context_);
|
||||
@ -55,8 +55,12 @@ class ContextifyContext : public BaseObject {
|
||||
}
|
||||
|
||||
inline v8::Local<v8::Object> sandbox() const {
|
||||
return context()->GetEmbedderData(ContextEmbedderIndex::kSandboxObject)
|
||||
.As<v8::Object>();
|
||||
// Only vanilla contexts have undefined sandboxes. sandbox() is only used by
|
||||
// interceptors who are not supposed to be called on vanilla contexts.
|
||||
v8::Local<v8::Value> result =
|
||||
context()->GetEmbedderData(ContextEmbedderIndex::kSandboxObject);
|
||||
CHECK(!result->IsUndefined());
|
||||
return result.As<v8::Object>();
|
||||
}
|
||||
|
||||
inline v8::MicrotaskQueue* microtask_queue() const {
|
||||
|
185
test/parallel/test-vm-context-dont-contextify.js
Normal file
185
test/parallel/test-vm-context-dont-contextify.js
Normal file
@ -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());
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user