Skip to content

Commit

Permalink
src,lib: mark URL/URLSearchParams as untransferrable
Browse files Browse the repository at this point in the history
  • Loading branch information
legendecas committed Apr 10, 2023
1 parent 4765038 commit 4b3bd89
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 56 deletions.
2 changes: 1 addition & 1 deletion lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const {
normalizeEncoding,
kIsEncodingSymbol,
defineLazyProperties,
markAsUntransferable,
} = require('internal/util');
const {
isAnyArrayBuffer,
Expand Down Expand Up @@ -123,7 +124,6 @@ const validateOffset = (value, name, min = 0, max = kMaxLength) =>

const {
FastBuffer,
markAsUntransferable,
addBufferPrototypeMethods,
createUnsafeBuffer,
} = require('internal/buffer');
Expand Down
15 changes: 0 additions & 15 deletions lib/internal/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ const {
getZeroFillToggle,
} = internalBinding('buffer');

const {
privateSymbols: {
untransferable_object_private_symbol,
},
} = internalBinding('util');

// Temporary buffers to convert numbers.
const float32Array = new Float32Array(1);
const uInt8Float32Array = new Uint8Array(float32Array.buffer);
Expand Down Expand Up @@ -1045,14 +1039,6 @@ function addBufferPrototypeMethods(proto) {
proto.utf8Write = utf8Write;
}

// This would better be placed in internal/worker/io.js, but that doesn't work
// because Buffer needs this and that would introduce a cyclic dependency.
function markAsUntransferable(obj) {
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null)
return; // This object is a primitive and therefore already untransferable.
obj[untransferable_object_private_symbol] = true;
}

// A toggle used to access the zero fill setting of the array buffer allocator
// in C++.
// |zeroFill| can be undefined when running inside an isolate where we
Expand All @@ -1078,7 +1064,6 @@ function reconnectZeroFillToggle() {
module.exports = {
FastBuffer,
addBufferPrototypeMethods,
markAsUntransferable,
createUnsafeBuffer,
readUInt16BE,
readUInt32BE,
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const {
toUSVString,
kEnumerableProperty,
SideEffectFreeRegExpPrototypeSymbolReplace,
markAsUncloneable,
} = require('internal/util');

const {
Expand Down Expand Up @@ -376,6 +377,8 @@ class URLSearchParams {
init = toUSVString(init);
this.#searchParams = init ? parseParams(init) : [];
}

markAsUncloneable(this);
}

[inspect.custom](recurseTimes, ctx) {
Expand Down Expand Up @@ -720,6 +723,8 @@ class URL {
}

this.#updateContext(href);

markAsUncloneable(this);
}

[inspect.custom](depth, opts) {
Expand Down
25 changes: 25 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ const {
privateSymbols: {
arrow_message_private_symbol,
decorated_private_symbol,
transfer_mode_private_symbol,
},
constants: {
kUncloneable,
kCloneable,
},
sleep: _sleep,
toUSVString: _toUSVString,
Expand Down Expand Up @@ -789,6 +794,24 @@ function setupCoverageHooks(dir) {
return coverageDirectory;
}

// This would better be placed in internal/worker/io.js, but that doesn't work
// because Buffer needs this and that would introduce a cyclic dependency.
// Mark the object as untransferable. If object occurs in the transfer list of
// a port.postMessage() call, it is ignored.
function markAsUntransferable(obj) {
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null)
return; // This value is a primitive and therefore already cloneable.
obj[transfer_mode_private_symbol] = kCloneable;
}

// Mark the object as uncloneable. If object occurs in the value of a
// port.postMessage() call, an error is thrown.
function markAsUncloneable(obj) {
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null)
return; // This value is a primitive and therefore already cloneable.
obj[transfer_mode_private_symbol] = kUncloneable;
}

module.exports = {
getLazy,
assertCrypto,
Expand Down Expand Up @@ -818,6 +841,8 @@ module.exports = {
join,
lazyDOMException,
lazyDOMExceptionClass,
markAsUntransferable,
markAsUncloneable,
normalizeEncoding,
once,
promisify,
Expand Down
2 changes: 1 addition & 1 deletion lib/worker_threads.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const {

const {
markAsUntransferable,
} = require('internal/buffer');
} = require('internal/util');

module.exports = {
isMainThread,
Expand Down
16 changes: 6 additions & 10 deletions src/base_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ class BaseObject : public MemoryRetainer {
// method of MessagePorts (and, by extension, Workers).
// GetTransferMode() returns a transfer mode that indicates how to deal with
// the current object:
// - kUntransferable:
// No transfer is possible, either because this type of BaseObject does
// not know how to be transferred, or because it is not in a state in
// which it is possible to do so (e.g. because it has already been
// transferred).
// - kUncloneable:
// No transfer or clone is possible, either because this type of
// BaseObject does not know how to be transferred, or because it is not
// in a state in which it is possible to do so (e.g. because it has
// already been transferred).
// - kTransferable:
// This object can be transferred in a destructive fashion, i.e. will be
// rendered unusable on the sending side of the channel in the process
Expand All @@ -160,11 +160,7 @@ class BaseObject : public MemoryRetainer {
// 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,
kCloneable
};
enum class TransferMode : uint8_t { kUncloneable, kTransferable, kCloneable };
virtual TransferMode GetTransferMode() const;
virtual std::unique_ptr<worker::TransferData> TransferForMessaging();
virtual std::unique_ptr<worker::TransferData> CloneForMessaging() const;
Expand Down
2 changes: 1 addition & 1 deletion src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
V(arrow_message_private_symbol, "node:arrowMessage") \
V(contextify_context_private_symbol, "node:contextify:context") \
V(decorated_private_symbol, "node:decorated") \
V(transfer_mode_private_symbol, "node:transfer_mode") \
V(napi_type_tag, "node:napi:type_tag") \
V(napi_wrapper, "node:napi:wrapper") \
V(untransferable_object_private_symbol, "node:untransferableObject") \
V(exit_info_private_symbol, "node:exit_info_private_symbol") \
V(require_private_symbol, "node:require_private_symbol")

Expand Down
10 changes: 7 additions & 3 deletions src/node_api.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "async_wrap-inl.h"
#include "env-inl.h"
#define NAPI_EXPERIMENTAL
#include "base_object.h"
#include "js_native_api_v8.h"
#include "memory_tracker-inl.h"
#include "node_api.h"
Expand Down Expand Up @@ -37,9 +38,12 @@ bool node_napi_env__::can_call_into_js() const {

v8::Maybe<bool> node_napi_env__::mark_arraybuffer_as_untransferable(
v8::Local<v8::ArrayBuffer> ab) const {
return ab->SetPrivate(context(),
node_env()->untransferable_object_private_symbol(),
v8::True(isolate));
return ab->SetPrivate(
context(),
node_env()->transfer_mode_private_symbol(),
v8::Integer::New(
isolate,
static_cast<int32_t>(node::BaseObject::TransferMode::kCloneable)));
}

void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) {
Expand Down
17 changes: 11 additions & 6 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,11 @@ MaybeLocal<Object> New(Environment* env,
Local<ArrayBuffer> ab =
CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint);
if (ab->SetPrivate(env->context(),
env->untransferable_object_private_symbol(),
True(env->isolate())).IsNothing()) {
env->transfer_mode_private_symbol(),
Integer::New(env->isolate(),
static_cast<int32_t>(
BaseObject::TransferMode::kCloneable)))
.IsNothing()) {
return Local<Object>();
}
MaybeLocal<Uint8Array> maybe_ui = Buffer::New(env, ab, 0, length);
Expand Down Expand Up @@ -1181,10 +1184,12 @@ void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
ab = ArrayBuffer::New(env->isolate(), std::move(backing));
}

ab->SetPrivate(
env->context(),
env->untransferable_object_private_symbol(),
True(env->isolate())).Check();
ab->SetPrivate(env->context(),
env->transfer_mode_private_symbol(),
Integer::New(env->isolate(),
static_cast<int32_t>(
BaseObject::TransferMode::kCloneable)))
.Check();

args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
}
Expand Down
6 changes: 3 additions & 3 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,12 @@ void FileHandle::MemoryInfo(MemoryTracker* tracker) const {
}

FileHandle::TransferMode FileHandle::GetTransferMode() const {
return reading_ || closing_ || closed_ ?
TransferMode::kUntransferable : TransferMode::kTransferable;
return reading_ || closing_ || closed_ ? TransferMode::kUncloneable
: TransferMode::kTransferable;
}

std::unique_ptr<worker::TransferData> FileHandle::TransferForMessaging() {
CHECK_NE(GetTransferMode(), TransferMode::kUntransferable);
CHECK_NE(GetTransferMode(), TransferMode::kUncloneable);
auto ret = std::make_unique<TransferData>(fd_);
closed_ = true;
return ret;
Expand Down
87 changes: 74 additions & 13 deletions src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ using BaseObjectList = std::vector<BaseObjectPtr<BaseObject>>;
static const uint32_t kNormalObject = static_cast<uint32_t>(-1);

BaseObject::TransferMode BaseObject::GetTransferMode() const {
return BaseObject::TransferMode::kUntransferable;
return BaseObject::TransferMode::kUncloneable;
}

std::unique_ptr<worker::TransferData> BaseObject::TransferForMessaging() {
Expand Down Expand Up @@ -288,6 +288,39 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
isolate->ThrowException(exception);
}

Maybe<bool> ObjectHasTransferMode(Environment* env,
Local<Context> context,
Local<Object> val) {
bool has_transfer_mode;
if (!val->HasPrivate(context, env->transfer_mode_private_symbol())
.To(&has_transfer_mode)) {
return Nothing<bool>();
}
return Just(has_transfer_mode);
}

Maybe<BaseObject::TransferMode> GetObjectTransferMode(Environment* env,
Local<Context> context,
Local<Object> val) {
DCHECK(ObjectHasTransferMode(env, context, val).FromJust());

Local<Value> transfer_mode_val;
if (!val->GetPrivate(context, env->transfer_mode_private_symbol())
.ToLocal(&transfer_mode_val)) {
return Nothing<BaseObject::TransferMode>();
}
DCHECK(transfer_mode_val->IsInt32());

int32_t transfer_mode;
if (!transfer_mode_val->Int32Value(context).To(&transfer_mode)) {
return Nothing<BaseObject::TransferMode>();
}

DCHECK(transfer_mode >= 0 &&
transfer_mode <= static_cast<int32_t>(TransferMode::kCloneable));
return Just(static_cast<BaseObject::TransferMode>(transfer_mode));
}

// This tells V8 how to serialize objects that it does not understand
// (e.g. C++ objects) into the output buffer, in a way that our own
// DeserializerDelegate understands how to unpack.
Expand All @@ -300,6 +333,16 @@ class SerializerDelegate : public ValueSerializer::Delegate {
ThrowDataCloneException(context_, message);
}

bool HasCustomHostObject(Isolate* isolate) override { return true; }

Maybe<bool> IsHostObject(Isolate* isolate, Local<Object> object) override {
if (BaseObject::IsBaseObject(object)) {
return Just(true);
}

return ObjectHasTransferMode(env_, context_, object);
}

Maybe<bool> WriteHostObject(Isolate* isolate, Local<Object> object) override {
if (BaseObject::IsBaseObject(object)) {
return WriteHostObject(
Expand All @@ -321,6 +364,14 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return serializer->WriteValue(env_->context(), normal_object);
}

BaseObject::TransferMode transfer_mode;
if (!GetObjectTransferMode(env_, context_, object).To(&transfer_mode)) {
return Nothing<bool>();
}

// TODO(legendecas): custom JS object cloning.
DCHECK(transfer_mode == BaseObject::TransferMode::kUncloneable);

ThrowDataCloneError(env_->clone_unsupported_type_str());
return Nothing<bool>();
}
Expand Down Expand Up @@ -393,7 +444,7 @@ class SerializerDelegate : public ValueSerializer::Delegate {
private:
Maybe<bool> WriteHostObject(BaseObjectPtr<BaseObject> host_object) {
BaseObject::TransferMode mode = host_object->GetTransferMode();
if (mode == BaseObject::TransferMode::kUntransferable) {
if (mode == BaseObject::TransferMode::kUncloneable) {
ThrowDataCloneError(env_->clone_unsupported_type_str());
return Nothing<bool>();
}
Expand Down Expand Up @@ -452,18 +503,29 @@ Maybe<bool> Message::Serialize(Environment* env,
if (entry->IsObject()) {
// See https://github.com/nodejs/node/pull/30339#issuecomment-552225353
// for details.
bool untransferable;
if (!entry.As<Object>()->HasPrivate(
context,
env->untransferable_object_private_symbol())
.To(&untransferable)) {
bool has_transfer_mode;
if (!ObjectHasTransferMode(env, context, entry.As<Object>())
.To(&has_transfer_mode)) {
return Nothing<bool>();
}
if (untransferable) continue;
if (has_transfer_mode) {
BaseObject::TransferMode mode;
if (!GetObjectTransferMode(env, context, entry.As<Object>())
.To(&mode)) {
return Nothing<bool>();
}
if (mode == BaseObject::TransferMode::kUncloneable) {
ThrowDataCloneException(context, env->clone_unsupported_type_str());
return Nothing<bool>();
}
if (mode == BaseObject::TransferMode::kCloneable) {
continue;
}
}
}

// Currently, we support ArrayBuffers and BaseObjects for which
// GetTransferMode() does not return kUntransferable.
// GetTransferMode() does not return kUncloneable.
if (entry->IsArrayBuffer()) {
Local<ArrayBuffer> ab = entry.As<ArrayBuffer>();
// If we cannot render the ArrayBuffer unusable in this Isolate,
Expand Down Expand Up @@ -525,7 +587,7 @@ Maybe<bool> Message::Serialize(Environment* env,
return Nothing<bool>();
}
if (host_object && host_object->GetTransferMode() !=
BaseObject::TransferMode::kUntransferable) {
BaseObject::TransferMode::kUncloneable) {
delegate.AddHostObject(host_object);
continue;
}
Expand Down Expand Up @@ -849,8 +911,7 @@ std::unique_ptr<MessagePortData> MessagePort::Detach() {
}

BaseObject::TransferMode MessagePort::GetTransferMode() const {
if (IsDetached())
return BaseObject::TransferMode::kUntransferable;
if (IsDetached()) return BaseObject::TransferMode::kUncloneable;
return BaseObject::TransferMode::kTransferable;
}

Expand Down Expand Up @@ -1174,7 +1235,7 @@ JSTransferable::TransferMode JSTransferable::GetTransferMode() const {
bool has_clone;
if (!object()->Has(env()->context(),
env()->messaging_clone_symbol()).To(&has_clone)) {
return TransferMode::kUntransferable;
return TransferMode::kUncloneable;
}

return has_clone ? TransferMode::kCloneable : TransferMode::kTransferable;
Expand Down
Loading

0 comments on commit 4b3bd89

Please sign in to comment.