Skip to content

Commit 4725ac6

Browse files
committed
vm: introduce vm.Context
Fixes #855 Fixes #31658 Fixes #31808
1 parent 4c746a6 commit 4725ac6

16 files changed

+336
-202
lines changed

doc/api/vm.md

+136-93
Large diffs are not rendered by default.

doc/api/worker_threads.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,13 @@ if (isMainThread) {
7676
}
7777
```
7878

79-
## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
79+
## `worker.moveMessagePortToContext(port, context)`
8080
<!-- YAML
8181
added: v11.13.0
8282
-->
8383

8484
* `port` {MessagePort} The message port which will be transferred.
85-
* `contextifiedSandbox` {Object} A [contextified][] object as returned by the
86-
`vm.createContext()` method.
85+
* `context` {vm.Context} A [`vm.Context`][] instance.
8786

8887
* Returns: {MessagePort}
8988

@@ -768,6 +767,7 @@ active handle in the event system. If the worker is already `unref()`ed calling
768767
[`trace_events`]: tracing.html
769768
[`v8.getHeapSnapshot()`]: v8.html#v8_v8_getheapsnapshot
770769
[`vm`]: vm.html
770+
[`vm.Context`]: vm.html#vm_class_vm_context
771771
[`worker.on('message')`]: #worker_threads_event_message_1
772772
[`worker.postMessage()`]: #worker_threads_worker_postmessage_value_transferlist
773773
[`worker.SHARE_ENV`]: #worker_threads_worker_share_env
@@ -780,5 +780,4 @@ active handle in the event system. If the worker is already `unref()`ed calling
780780
[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
781781
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
782782
[child processes]: child_process.html
783-
[contextified]: vm.html#vm_what_does_it_mean_to_contextify_an_object
784783
[v8.serdes]: v8.html#v8_serialization_api

lib/vm.js

+34-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ const {
4747
validateBuffer,
4848
validateObject,
4949
} = require('internal/validators');
50-
const { kVmBreakFirstLineSymbol } = require('internal/util');
50+
const {
51+
kVmBreakFirstLineSymbol,
52+
emitExperimentalWarning,
53+
} = require('internal/util');
5154
const kParsingContext = Symbol('script parsing context');
5255

5356
class Script extends ContextifyScript {
@@ -206,13 +209,11 @@ function isContext(object) {
206209
}
207210

208211
let defaultContextNameIndex = 1;
209-
function createContext(contextObject = {}, options = {}) {
210-
if (isContext(contextObject)) {
211-
return contextObject;
212+
function validateContextOptions(options) {
213+
if (typeof options !== 'object' || options === null) {
214+
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
212215
}
213216

214-
validateObject(options, 'options');
215-
216217
const {
217218
name = `VM Context ${defaultContextNameIndex++}`,
218219
origin,
@@ -233,10 +234,35 @@ function createContext(contextObject = {}, options = {}) {
233234
validateBoolean(wasm, 'options.codeGeneration.wasm');
234235
}
235236

236-
makeContext(contextObject, name, origin, strings, wasm);
237+
return { name, origin, strings, wasm };
238+
}
239+
240+
function createContext(contextObject = {}, options = {}) {
241+
if (isContext(contextObject)) {
242+
return contextObject;
243+
}
244+
245+
const { name, origin, strings, wasm } = validateContextOptions(options);
246+
247+
makeContext(contextObject, name, origin, strings, wasm, undefined);
237248
return contextObject;
238249
}
239250

251+
class Context {
252+
#global;
253+
254+
constructor(options = {}) {
255+
emitExperimentalWarning('vm.Context');
256+
257+
const { name, origin, strings, wasm } = validateContextOptions(options);
258+
this.#global = makeContext(undefined, name, origin, strings, wasm, this);
259+
}
260+
261+
get global() {
262+
return this.#global;
263+
}
264+
}
265+
240266
function createScript(code, options) {
241267
return new Script(code, options);
242268
}
@@ -357,6 +383,7 @@ function compileFunction(code, params, options = {}) {
357383

358384

359385
module.exports = {
386+
Context,
360387
Script,
361388
createContext,
362389
createScript,

src/node_contextify.cc

+71-53
Original file line numberDiff line numberDiff line change
@@ -151,40 +151,44 @@ MaybeLocal<Context> ContextifyContext::CreateV8Context(
151151
Local<Object> sandbox_obj,
152152
const ContextOptions& options) {
153153
EscapableHandleScope scope(env->isolate());
154-
Local<FunctionTemplate> function_template =
155-
FunctionTemplate::New(env->isolate());
156-
157-
function_template->SetClassName(sandbox_obj->GetConstructorName());
158-
159-
Local<ObjectTemplate> object_template =
160-
function_template->InstanceTemplate();
161154

162-
Local<Object> data_wrapper;
163-
if (!CreateDataWrapper(env).ToLocal(&data_wrapper))
164-
return MaybeLocal<Context>();
165-
166-
NamedPropertyHandlerConfiguration config(
167-
PropertyGetterCallback,
168-
PropertySetterCallback,
169-
PropertyDescriptorCallback,
170-
PropertyDeleterCallback,
171-
PropertyEnumeratorCallback,
172-
PropertyDefinerCallback,
173-
data_wrapper,
174-
PropertyHandlerFlags::kHasNoSideEffect);
175-
176-
IndexedPropertyHandlerConfiguration indexed_config(
177-
IndexedPropertyGetterCallback,
178-
IndexedPropertySetterCallback,
179-
IndexedPropertyDescriptorCallback,
180-
IndexedPropertyDeleterCallback,
181-
PropertyEnumeratorCallback,
182-
IndexedPropertyDefinerCallback,
183-
data_wrapper,
184-
PropertyHandlerFlags::kHasNoSideEffect);
185-
186-
object_template->SetHandler(config);
187-
object_template->SetHandler(indexed_config);
155+
Local<ObjectTemplate> object_template;
156+
157+
if (!sandbox_obj.IsEmpty()) {
158+
Local<FunctionTemplate> function_template =
159+
FunctionTemplate::New(env->isolate());
160+
161+
function_template->SetClassName(sandbox_obj->GetConstructorName());
162+
163+
object_template = function_template->InstanceTemplate();
164+
165+
Local<Object> data_wrapper;
166+
if (!CreateDataWrapper(env).ToLocal(&data_wrapper))
167+
return MaybeLocal<Context>();
168+
169+
NamedPropertyHandlerConfiguration config(
170+
PropertyGetterCallback,
171+
PropertySetterCallback,
172+
PropertyDescriptorCallback,
173+
PropertyDeleterCallback,
174+
PropertyEnumeratorCallback,
175+
PropertyDefinerCallback,
176+
data_wrapper,
177+
PropertyHandlerFlags::kHasNoSideEffect);
178+
179+
IndexedPropertyHandlerConfiguration indexed_config(
180+
IndexedPropertyGetterCallback,
181+
IndexedPropertySetterCallback,
182+
IndexedPropertyDescriptorCallback,
183+
IndexedPropertyDeleterCallback,
184+
PropertyEnumeratorCallback,
185+
IndexedPropertyDefinerCallback,
186+
data_wrapper,
187+
PropertyHandlerFlags::kHasNoSideEffect);
188+
189+
object_template->SetHandler(config);
190+
object_template->SetHandler(indexed_config);
191+
}
188192

189193
Local<Context> ctx = NewContext(env->isolate(), object_template);
190194

@@ -194,16 +198,18 @@ MaybeLocal<Context> ContextifyContext::CreateV8Context(
194198

195199
ctx->SetSecurityToken(env->context()->GetSecurityToken());
196200

197-
// We need to tie the lifetime of the sandbox object with the lifetime of
198-
// newly created context. We do this by making them hold references to each
199-
// other. The context can directly hold a reference to the sandbox as an
200-
// embedder data field. However, we cannot hold a reference to a v8::Context
201-
// directly in an Object, we instead hold onto the new context's global
202-
// object instead (which then has a reference to the context).
203-
ctx->SetEmbedderData(ContextEmbedderIndex::kSandboxObject, sandbox_obj);
204-
sandbox_obj->SetPrivate(env->context(),
205-
env->contextify_global_private_symbol(),
206-
ctx->Global());
201+
if (!sandbox_obj.IsEmpty()) {
202+
// We need to tie the lifetime of the sandbox object with the lifetime of
203+
// newly created context. We do this by making them hold references to each
204+
// other. The context can directly hold a reference to the sandbox as an
205+
// embedder data field. However, we cannot hold a reference to a v8::Context
206+
// directly in an Object, we instead hold onto the new context's global
207+
// object instead (which then has a reference to the context).
208+
ctx->SetEmbedderData(ContextEmbedderIndex::kSandboxObject, sandbox_obj);
209+
sandbox_obj->SetPrivate(env->context(),
210+
env->contextify_global_private_symbol(),
211+
ctx->Global());
212+
}
207213

208214
Utf8Value name_val(env->isolate(), options.name);
209215
ctx->AllowCodeGenerationFromStrings(options.allow_code_gen_strings->IsTrue());
@@ -236,19 +242,23 @@ void ContextifyContext::Init(Environment* env, Local<Object> target) {
236242
}
237243

238244

239-
// makeContext(sandbox, name, origin, strings, wasm);
245+
// makeContext(sandbox, name, origin, strings, wasm, instance);
240246
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
241247
Environment* env = Environment::GetCurrent(args);
242248

243-
CHECK_EQ(args.Length(), 5);
244-
CHECK(args[0]->IsObject());
245-
Local<Object> sandbox = args[0].As<Object>();
249+
CHECK_EQ(args.Length(), 6);
246250

247-
// Don't allow contextifying a sandbox multiple times.
248-
CHECK(
249-
!sandbox->HasPrivate(
250-
env->context(),
251-
env->contextify_context_private_symbol()).FromJust());
251+
Local<Object> sandbox;
252+
if (args[5]->IsUndefined()) {
253+
CHECK(args[0]->IsObject());
254+
sandbox = args[0].As<Object>();
255+
256+
// Don't allow contextifying a sandbox multiple times.
257+
CHECK(
258+
!sandbox->HasPrivate(
259+
env->context(),
260+
env->contextify_context_private_symbol()).FromJust());
261+
}
252262

253263
ContextOptions options;
254264

@@ -278,7 +288,15 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
278288
if (context_ptr->context().IsEmpty())
279289
return;
280290

281-
sandbox->SetPrivate(
291+
Local<Object> instance;
292+
if (args[5]->IsUndefined()) {
293+
instance = sandbox;
294+
} else {
295+
CHECK(args[5]->IsObject());
296+
args.GetReturnValue().Set(context_ptr->global_proxy());
297+
instance = args[5].As<Object>();
298+
}
299+
instance->SetPrivate(
282300
env->context(),
283301
env->contextify_context_private_symbol(),
284302
External::New(env->isolate(), context_ptr.release()));

test/parallel/test-new-vm-context.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { Context, Script } = require('vm');
6+
7+
[
8+
true, 0, null, '', 'hi',
9+
Symbol('symbol'),
10+
{ name: 0 },
11+
{ origin: 0 },
12+
{ codeGeneration: 0 },
13+
{ codeGeneration: { strings: 0 } },
14+
{ codeGeneration: { wasm: 0 } },
15+
].forEach((v) => {
16+
assert.throws(() => {
17+
new Context(v);
18+
}, {
19+
code: 'ERR_INVALID_ARG_TYPE',
20+
});
21+
});
22+
23+
{
24+
const ctx = new Context();
25+
ctx.global.a = 1;
26+
const script = new Script('this');
27+
assert.strictEqual(script.runInContext(ctx), ctx.global);
28+
}
29+
30+
// https://github.com/nodejs/node/issues/31808
31+
{
32+
const ctx = new Context();
33+
Object.defineProperty(ctx.global, 'x', {
34+
enumerable: true,
35+
configurable: true,
36+
get: common.mustNotCall(),
37+
set: common.mustNotCall(),
38+
});
39+
const script = new Script('function x() {}');
40+
script.runInContext(ctx);
41+
assert.strictEqual(typeof ctx.global.x, 'function');
42+
}

test/parallel/test-vm-basic.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const vm = require('vm');
8484
{
8585
const script = 'throw new Error("boom")';
8686
const filename = 'test-boom-error';
87-
const context = vm.createContext();
87+
const context = new vm.Context();
8888

8989
function checkErr(err) {
9090
return err.stack.startsWith('test-boom-error:1');

test/parallel/test-vm-codegen.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
require('../common');
44
const assert = require('assert');
55

6-
const { createContext, runInContext, runInNewContext } = require('vm');
6+
const { Context, runInContext, runInNewContext } = require('vm');
77

88
const WASM_BYTES = Buffer.from(
99
[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
1010

1111
{
12-
const ctx = createContext({ WASM_BYTES });
12+
const ctx = new Context();
13+
ctx.global.WASM_BYTES = WASM_BYTES;
1314
const test = 'eval(""); new WebAssembly.Module(WASM_BYTES);';
1415
runInContext(test, ctx);
1516

@@ -19,7 +20,7 @@ const WASM_BYTES = Buffer.from(
1920
}
2021

2122
{
22-
const ctx = createContext({}, {
23+
const ctx = new Context({
2324
codeGeneration: {
2425
strings: false,
2526
},
@@ -32,11 +33,12 @@ const WASM_BYTES = Buffer.from(
3233
}
3334

3435
{
35-
const ctx = createContext({ WASM_BYTES }, {
36+
const ctx = new Context({
3637
codeGeneration: {
3738
wasm: false,
3839
},
3940
});
41+
ctx.global.WASM_BYTES = WASM_BYTES;
4042

4143
const CompileError = runInContext('WebAssembly.CompileError', ctx);
4244
assert.throws(() => {
@@ -65,7 +67,7 @@ assert.throws(() => {
6567
});
6668

6769
assert.throws(() => {
68-
createContext({}, {
70+
new Context({
6971
codeGeneration: {
7072
strings: 0,
7173
},
@@ -85,15 +87,15 @@ assert.throws(() => {
8587
});
8688

8789
assert.throws(() => {
88-
createContext({}, {
90+
new Context({
8991
codeGeneration: 1,
9092
});
9193
}, {
9294
code: 'ERR_INVALID_ARG_TYPE',
9395
});
9496

9597
assert.throws(() => {
96-
createContext({}, {
98+
new Context({
9799
codeGeneration: null,
98100
});
99101
}, {

0 commit comments

Comments
 (0)