Skip to content

Commit

Permalink
src: throw DataCloneError on transfering untransferable objects
Browse files Browse the repository at this point in the history
The HTML StructuredSerializeWithTransfer algorithm defines that when
an untransferable object is in the transfer list, a DataCloneError is
thrown.
An array buffer that is already transferred is also considered as
untransferable.
  • Loading branch information
legendecas committed Apr 18, 2023
1 parent 682256c commit b59b61c
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 15 deletions.
11 changes: 10 additions & 1 deletion doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,15 @@ if (isMainThread) {
added:
- v14.5.0
- v12.19.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/47604
description: An error is thrown when the untransferable object is in the
transfer list.
-->

Mark an object as not transferable. If `object` occurs in the transfer list of
a [`port.postMessage()`][] call, it is ignored.
a [`port.postMessage()`][] call, an error is thrown.

In particular, this makes sense for objects that can be cloned, rather than
transferred, and which are used by other objects on the sending side.
Expand Down Expand Up @@ -568,6 +573,10 @@ are part of the channel.
<!-- YAML
added: v10.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/47604
description: An error is thrown when an untransferable object is in the
transfer list.
- version:
- v15.14.0
- v14.18.0
Expand Down
4 changes: 3 additions & 1 deletion src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
V(change_string, "change") \
V(channel_string, "channel") \
V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \
V(clone_unsupported_type_str, "Cannot transfer object of unsupported type.") \
V(clone_unsupported_type_str, "Cannot clone object of unsupported type.") \
V(code_string, "code") \
V(commonjs_string, "commonjs") \
V(config_string, "config") \
Expand Down Expand Up @@ -301,6 +301,8 @@
V(time_to_first_header_string, "timeToFirstHeader") \
V(tls_ticket_string, "tlsTicket") \
V(transfer_string, "transfer") \
V(transfer_unsupported_type_str, \
"Cannot transfer object of unsupported type.") \
V(ttl_string, "ttl") \
V(type_string, "type") \
V(uid_string, "uid") \
Expand Down
16 changes: 11 additions & 5 deletions src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,14 @@ Maybe<bool> Message::Serialize(Environment* env,
.To(&untransferable)) {
return Nothing<bool>();
}
if (untransferable) continue;
if (untransferable) {
ThrowDataCloneException(context, env->transfer_unsupported_type_str());
return Nothing<bool>();
}
}

// Currently, we support ArrayBuffers and BaseObjects for which
// GetTransferMode() does not return kUntransferable.
// GetTransferMode() returns kTransferable.
if (entry->IsArrayBuffer()) {
Local<ArrayBuffer> ab = entry.As<ArrayBuffer>();
// If we cannot render the ArrayBuffer unusable in this Isolate,
Expand All @@ -474,7 +477,10 @@ Maybe<bool> Message::Serialize(Environment* env,
// raw data *and* an Isolate with a non-default ArrayBuffer allocator
// is always going to outlive any Workers it creates, and so will its
// allocator along with it.
if (!ab->IsDetachable()) continue;
if (!ab->IsDetachable() || ab->WasDetached()) {
ThrowDataCloneException(context, env->transfer_unsupported_type_str());
return Nothing<bool>();
}
if (std::find(array_buffers.begin(), array_buffers.end(), ab) !=
array_buffers.end()) {
ThrowDataCloneException(
Expand Down Expand Up @@ -524,8 +530,8 @@ Maybe<bool> Message::Serialize(Environment* env,
entry.As<Object>()->GetConstructorName()));
return Nothing<bool>();
}
if (host_object && host_object->GetTransferMode() !=
BaseObject::TransferMode::kUntransferable) {
if (host_object && host_object->GetTransferMode() ==
BaseObject::TransferMode::kTransferable) {
delegate.AddHostObject(host_object);
continue;
}
Expand Down
5 changes: 4 additions & 1 deletion test/addons/worker-buffer-callback/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);

const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength;
port1.postMessage(buffer, [buffer.buffer]);
assert.throws(() => port1.postMessage(buffer, [buffer.buffer]), {
code: 25,
name: 'DataCloneError',
});

assert.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0);
5 changes: 4 additions & 1 deletion test/node-api/test_worker_buffer_callback/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);

const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength;
port1.postMessage(buffer, [buffer]);
assert.throws(() => port1.postMessage(buffer, [buffer]), {
code: 25,
name: 'DataCloneError',
});

assert.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0);
5 changes: 4 additions & 1 deletion test/parallel/test-buffer-pool-untransferable.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ assert.strictEqual(a.buffer, b.buffer);
const length = a.length;

const { port1 } = new MessageChannel();
port1.postMessage(a, [ a.buffer ]);
assert.throws(() => port1.postMessage(a, [ a.buffer ]), {
code: 25,
name: 'DataCloneError',
});

// Verify that the pool ArrayBuffer has not actually been transferred:
assert.strictEqual(a.buffer, b.buffer);
Expand Down
7 changes: 7 additions & 0 deletions test/parallel/test-worker-message-port-arraybuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const { MessageChannel } = require('worker_threads');
typedArray[0] = 0x12345678;

port1.postMessage(typedArray, [ arrayBuffer ]);
assert.strictEqual(arrayBuffer.byteLength, 0);
// Transferring again should throw a DataCloneError.
assert.throws(() => port1.postMessage(typedArray, [ arrayBuffer ]), {
code: 25,
name: 'DataCloneError',
});

port2.on('message', common.mustCall((received) => {
assert.strictEqual(received[0], 0x12345678);
port2.close(common.mustCall());
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-worker-message-port-transfer-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const { internalBinding } = require('internal/test/binding');
port1.postMessage(nativeObject);
}, {
name: 'DataCloneError',
message: /Cannot transfer object of unsupported type\.$/
message: /Cannot clone object of unsupported type\.$/
});
port1.close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ const { MessageChannel, markAsUntransferable } = require('worker_threads');
markAsUntransferable(ab);
assert.strictEqual(ab.byteLength, 8);

const { port1, port2 } = new MessageChannel();
port1.postMessage(ab, [ ab ]);
const { port1 } = new MessageChannel();
assert.throws(() => port1.postMessage(ab, [ ab ]), {
code: 25,
name: 'DataCloneError',
});

assert.strictEqual(ab.byteLength, 8); // The AB is not detached.
port2.once('message', common.mustCall());
}

{
Expand All @@ -24,7 +26,10 @@ const { MessageChannel, markAsUntransferable } = require('worker_threads');

assert.throws(() => {
channel1.port1.postMessage(channel2.port1, [ channel2.port1 ]);
}, /was found in message but not listed in transferList/);
}, {
code: 25,
name: 'DataCloneError',
});

channel2.port1.postMessage('still works, not closed/transferred');
channel2.port2.once('message', common.mustCall());
Expand Down

0 comments on commit b59b61c

Please sign in to comment.