Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
worker: allow passing JS wrapper objects via postMessage
Enable JS wrapper objects to be used as transferable or cloneable
objects in `postMessage()` calls, by having them extend a C++-backed
class.

This requires a few internal changes:
- This commit adds the possibility for transferred objects to
  read/write JS values at the end of the serialization/deserialization
  phases.
- This commit adds the possibility for transferred objects to list
  sub-transferables, e.g. typically the public JS wrapper class
  would list its C++ handle in there.
- This commit adds usage of `BaseObject` in a few more places, because
  now during deserialization weakly held objects can also be involved,
  in addition to `MessagePort`s.
  • Loading branch information
addaleax committed Jun 11, 2020
commit 877d631c56e6b99d9af176a2287edab850ab6e55
1 change: 1 addition & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ process._exiting = false;

// process.config is serialized config.gypi
process.config = JSONParse(internalBinding('native_module').config);
require('internal/worker/js_transferable').setup();

// Bootstrappers for all threads, including worker threads and main thread
const perThreadSetup = require('internal/process/per_thread');
Expand Down
31 changes: 31 additions & 0 deletions lib/internal/worker/js_transferable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';
const {
messaging_deserialize_symbol,
messaging_transfer_symbol,
messaging_clone_symbol,
messaging_transfer_list_symbol
} = internalBinding('symbols');
const {
JSTransferable,
setDeserializerCreateObjectFunction
} = internalBinding('messaging');

function setup() {
// Register the handler that will be used when deserializing JS-based objects
// from .postMessage() calls. The format of `deserializeInfo` is generally
// 'module:Constructor', e.g. 'internal/fs/promises:FileHandle'.
setDeserializerCreateObjectFunction((deserializeInfo) => {
const [ module, ctor ] = deserializeInfo.split(':');
const Ctor = require(module)[ctor];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the module name be guarded with a whitelist here?

I don't think it should since worker threads don't provide a sandbox but I just want to make sure that this isn't exposing a new attack surface somehow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamingr Hm yeah … this isn’t extending capabilities of the calling code in any way, but I agree that it might be good to be careful here. I’ll think about this a bit more.

@addaleax addaleax Jun 7, 2020 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamingr I’ve updated this PR so that this function now verifies that the target constructor is a subclass of JSTransferable, and added tests that verify that fact + the fact that this will only load internal modules (which was already the case before).

I think that’s as “locked down” as this can get. If user code makes other internal classes inherit from JSTransferable on the receiving side, and override the kTransfer method on the sending side – which is totally doable in most situations – then that means that you can already make arbitrary changes to Node.js internals and won’t even need to do this.

The one thing I could see happen would be side effects from require()ing the internal module itself, which the sending code can still trigger on the receiving side (and which would still typically result in an uncaught exception).

What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing I could see happen would be side effects from require()ing the internal module itself, which the sending code can still trigger on the receiving side (and which would still typically result in an uncaught exception).

Fwiw that's the part that initially bugged me, but like we both said it doesn't increase the attack surface at all from what I can tell.

return new Ctor();
});
}

module.exports = {
setup,
JSTransferable,
kClone: messaging_clone_symbol,
kDeserialize: messaging_deserialize_symbol,
kTransfer: messaging_transfer_symbol,
kTransferList: messaging_transfer_list_symbol
};
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
'lib/internal/vm/module.js',
'lib/internal/worker.js',
'lib/internal/worker/io.js',
'lib/internal/worker/js_transferable.js',
'lib/internal/watchdog.js',
'lib/internal/streams/lazy_transform.js',
'lib/internal/streams/async_iterator.js',
Expand Down
9 changes: 9 additions & 0 deletions src/base_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,17 @@ class BaseObject : public MemoryRetainer {
// make sure that they are not accidentally destroyed on the sending side.
// TransferForMessaging() will be called to get a representation of the
// object that is used for subsequent deserialization.
// The NestedTransferables() method can be used to transfer other objects
// along with this one, if a situation requires it.
// - kCloneable:
// This object can be cloned without being modified.
// CloneForMessaging() will be called to get a representation of the
// object that is used for subsequent deserialization, unless the
// object is listed in transferList, in which case TransferForMessaging()
// is attempted first.
// After a successful clone, FinalizeTransferRead() is called on the receiving
// end, and can read deserialize JS data possibly serialized by a previous
// FinalizeTransferWrite() call.
enum class TransferMode {
kUntransferable,
kTransferable,
Expand All @@ -137,6 +142,10 @@ class BaseObject : public MemoryRetainer {
virtual TransferMode GetTransferMode() const;
virtual std::unique_ptr<worker::TransferData> TransferForMessaging();
virtual std::unique_ptr<worker::TransferData> CloneForMessaging() const;
virtual v8::Maybe<std::vector<BaseObjectPtrImpl<BaseObject, false>>>
NestedTransferables() const;
virtual v8::Maybe<bool> FinalizeTransferRead(
v8::Local<v8::Context> context, v8::ValueDeserializer* deserializer);

virtual inline void OnGCCollect();

Expand Down
6 changes: 6 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ constexpr size_t kFsStatsBufferLength =
V(async_id_symbol, "async_id_symbol") \
V(handle_onclose_symbol, "handle_onclose") \
V(no_message_symbol, "no_message_symbol") \
V(messaging_deserialize_symbol, "messaging_deserialize_symbol") \
V(messaging_transfer_symbol, "messaging_transfer_symbol") \
V(messaging_clone_symbol, "messaging_clone_symbol") \
V(messaging_transfer_list_symbol, "messaging_transfer_list_symbol") \
V(oninit_symbol, "oninit") \
V(owner_symbol, "owner_symbol") \
V(onpskexchange_symbol, "onpskexchange") \
Expand Down Expand Up @@ -201,6 +205,7 @@ constexpr size_t kFsStatsBufferLength =
V(crypto_rsa_pss_string, "rsa-pss") \
V(cwd_string, "cwd") \
V(data_string, "data") \
V(deserialize_info_string, "deserializeInfo") \
V(dest_string, "dest") \
V(destroyed_string, "destroyed") \
V(detached_string, "detached") \
Expand Down Expand Up @@ -453,6 +458,7 @@ constexpr size_t kFsStatsBufferLength =
V(internal_binding_loader, v8::Function) \
V(immediate_callback_function, v8::Function) \
V(inspector_console_extension_installer, v8::Function) \
V(messaging_deserialize_create_object, v8::Function) \
V(message_port, v8::Object) \
V(native_module_require, v8::Function) \
V(performance_entry_callback, v8::Function) \
Expand Down
3 changes: 2 additions & 1 deletion src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ void OnFatalError(const char* location, const char* message);
V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \
V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \
V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, \
"MessagePort was found in message but not listed in transferList") \
"Object that needs transfer was found in message but not listed " \
"in transferList") \
V(ERR_MISSING_PLATFORM_FOR_WORKER, \
"The V8 platform used by this instance of Node does not support " \
"creating Workers") \
Expand Down
Loading