Skip to content

Commit

Permalink
worker: implement vm.moveMessagePortToContext()
Browse files Browse the repository at this point in the history
This should help a lot with actual sandboxing of JS code.

Thanks to Timothy Gu, Stephen Belanger and Benjamin Gruenbaum
for reviewing this change in its original form.

Refs: ayojs/ayo#111
  • Loading branch information
addaleax committed May 18, 2018
1 parent 95ffcc1 commit 92bcd19
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 5 deletions.
43 changes: 43 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,49 @@ console.log(util.inspect(sandbox));
// { globalVar: 1024 }
```

## vm.moveMessagePortToContext(port, contextifiedSandbox)
<!-- YAML
added: REPLACEME
-->

* `port` {MessagePort}
* `contextifiedSandbox` {Object} A contextified object as returned by the
`vm.createContext()` method.
* Returns: {MessagePort}

Bind a `MessagePort` to a specific VM context. This returns a new `MessagePort`
object, whose prototype and methods act as if they were created in the passed
context. The received messages will also be emitted as objects from the passed
context.

The `port` object on which this method was called can not be used for sending
or receiving further messages.

## vm.runInDebugContext(code)
<!-- YAML
added: v0.11.14
-->

> Stability: 0 - Deprecated. An alternative is in development.
* `code` {string} The JavaScript code to compile and run.

The `vm.runInDebugContext()` method compiles and executes `code` inside the V8
debug context. The primary use case is to gain access to the V8 `Debug` object:

```js
const vm = require('vm');
const Debug = vm.runInDebugContext('Debug');
console.log(Debug.findScript(process.emit).name); // 'events.js'
console.log(Debug.findScript(process.exit).name); // 'internal/process.js'
```

*Note*: The debug context and object are intrinsically tied to V8's debugger
implementation and may change (or even be removed) without prior warning.

The `Debug` object can also be made available using the V8-specific
`--expose_debug_as=` [command line option][].

## vm.runInNewContext(code[, sandbox][, options])
<!-- YAML
added: v0.3.1
Expand Down
11 changes: 9 additions & 2 deletions doc/api/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ added: REPLACEME
Instances of the `worker.MessagePort` class represent one end of an
asynchronous, two-way communications channel. It can be used to transfer
structured data, memory regions and other `MessagePort`s between different
[`Worker`][]s.
[`Worker`][]s or [`vm` context][vm]s.

For transferring `MessagePort` instances between VM contexts, see
[`vm.moveMessagePortToContext()`][].

*Note*: With the exception of `MessagePort`s being [`EventEmitter`][]s rather
than `EventTarget`s, this implementation matches [browser `MessagePort`][]s.
Expand Down Expand Up @@ -260,7 +263,9 @@ added: REPLACEME

Starts receiving messages on this `MessagePort`. When using this port
as an event emitter, this will be called automatically once `'message'`
listeners are attached.
listeners are attached. This means that this method does not need to be used
unless you are using [`vm.moveMessagePortToContext()`][] to move this `port`
into another VM context.

## Class: Worker
<!-- YAML
Expand Down Expand Up @@ -449,6 +454,8 @@ it is available as [`require('worker').threadId`][].
[`require('worker').postMessage()`]: #worker_worker_postmessage_value_transferlist
[`require('worker').isMainThread`]: #worker_worker_is_main_thread
[`require('worker').threadId`]: #worker_worker_threadid
[`vm.moveMessagePortToContext()`]: vm.html#vm_vm_movemessageporttocontext_port_context
[vm]: vm.html#vm_vm_executing_javascript
[`cluster` module]: cluster.html
[`inspector`]: inspector.html
[v8.serdes]: v8.html#v8_serialization_api
Expand Down
3 changes: 3 additions & 0 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ const {
isContext: _isContext,
} = process.binding('contextify');

const { internalBinding } = require('internal/bootstrap/loaders');
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { isUint8Array } = require('internal/util/types');
const { validateInt32, validateUint32 } = require('internal/validators');
const { moveMessagePortToContext } = internalBinding('messaging');

class Script extends ContextifyScript {
constructor(code, options = {}) {
Expand Down Expand Up @@ -290,6 +292,7 @@ module.exports = {
Script,
createContext,
createScript,
moveMessagePortToContext,
runInContext,
runInNewContext,
runInThisContext,
Expand Down
20 changes: 17 additions & 3 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
#include "node_contextify.h"
#include "node_context_data.h"

namespace node {
namespace contextify {

using v8::Array;
using v8::ArrayBuffer;
using v8::Boolean;
Expand Down Expand Up @@ -66,6 +63,9 @@ using v8::Value;
using v8::WeakCallbackInfo;
using v8::WeakCallbackType;

namespace node {
namespace contextify {

// The vm module executes code in a sandboxed environment with a different
// global object than the rest of the code. This is achieved by applying
// every call that changes or queries a property on the global `this` in the
Expand Down Expand Up @@ -938,6 +938,20 @@ void Initialize(Local<Object> target,
}

} // namespace contextify

MaybeLocal<Context> ContextFromContextifiedSandbox(
Environment* env,
Local<Object> sandbox) {
using contextify::ContextifyContext;
ContextifyContext* contextify_context =
ContextifyContext::ContextFromContextifiedSandbox(env, sandbox);

if (contextify_context == nullptr)
return MaybeLocal<Context>();
else
return contextify_context->context();
}

} // namespace node

NODE_BUILTIN_MODULE_CONTEXT_AWARE(contextify, node::contextify::Initialize)
5 changes: 5 additions & 0 deletions src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ class ContextifyContext {
};

} // namespace contextify

v8::MaybeLocal<v8::Context> ContextFromContextifiedSandbox(
Environment* env,
v8::Local<v8::Object> sandbox);

} // namespace node

#endif // SRC_NODE_CONTEXTIFY_H_
34 changes: 34 additions & 0 deletions src/node_messaging.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "node_messaging.h"
#include "node_contextify.h"
#include "node_internals.h"
#include "node_buffer.h"
#include "node_errors.h"
Expand Down Expand Up @@ -606,6 +607,37 @@ void MessagePort::Stop(const FunctionCallbackInfo<Value>& args) {
port->Stop();
}

void MessagePort::MoveToContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
MessagePort* port;
if (!args[0]->IsObject() ||
(port = Unwrap<MessagePort>(args[0].As<Object>())) == nullptr) {
env->ThrowTypeError("First argument needs to be a MessagePort instance");
}
if (!port->data_) {
env->ThrowError("Cannot transfer a closed MessagePort");
return;
}
if (port->is_privileged_ || port->flagged_message_listener_) {
env->ThrowError("Cannot transfer MessagePort with special semantics");
return;
}
Local<Value> context_arg = args[1];
Local<Context> context;
if (!context_arg->IsObject() ||
!ContextFromContextifiedSandbox(env, context_arg.As<Object>())
.ToLocal(&context)) {
env->ThrowError("Invalid context argument");
return;
}
Context::Scope context_scope(context);
MessagePort* target =
MessagePort::New(env, context, nullptr, port->Detach());
if (target) {
args.GetReturnValue().Set(target->object());
}
}

size_t MessagePort::self_size() const {
Mutex::ScopedLock lock(data_->mutex_);
size_t sz = sizeof(*this) + sizeof(*data_);
Expand Down Expand Up @@ -689,6 +721,8 @@ static void InitMessaging(Local<Object> target,
templ->GetFunction(context).ToLocalChecked()).FromJust();
}

env->SetMethod(target, "moveMessagePortToContext",
MessagePort::MoveToContext);
target->Set(context,
env->message_port_constructor_string(),
GetMessagePortConstructor(env, context).ToLocalChecked())
Expand Down
5 changes: 5 additions & 0 deletions src/node_messaging.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,16 @@ class MessagePort : public HandleWrap {
// Stop processing messages on this port as a receiving end.
void Stop();

/* constructor */
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
/* prototype methods */
static void PostMessage(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Start(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Stop(const v8::FunctionCallbackInfo<v8::Value>& args);

/* static */
static void MoveToContext(const v8::FunctionCallbackInfo<v8::Value>& args);

// Turns `a` and `b` into siblings, i.e. connects the sending side of one
// to the receiving side of the other. This is not thread-safe.
static void Entangle(MessagePort* a, MessagePort* b);
Expand Down
45 changes: 45 additions & 0 deletions test/parallel/test-message-channel-move.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable prefer-assert-methods */
'use strict';
const common = require('../common');
const assert = require('assert');
const vm = require('vm');
const { MessageChannel } = require('worker');

{
const context = vm.createContext();
const channel = new MessageChannel();
context.port = vm.moveMessagePortToContext(channel.port1, context);
context.global = context;
const port = channel.port2;
vm.runInContext('(' + function() {
function assert(condition) { if (!condition) throw new Error(); }

{
assert(port instanceof Object);
assert.strictEqual(port.onmessage, undefined);
assert(port.postMessage instanceof Function);
port.onmessage = function(msg) {
assert(msg instanceof Object);
port.postMessage(msg);
};
port.start();
}

{
let threw = false;
try {
port.postMessage(global);
} catch (e) {
assert(e instanceof Object);
assert(e instanceof Error);
threw = true;
}
assert(threw);
}
} + ')()', context);
port.on('message', common.mustCall((msg) => {
assert(msg instanceof Object);
port.close();
}));
port.postMessage({});
}

0 comments on commit 92bcd19

Please sign in to comment.