src: throw DataCloneError on transfering untransferable objects

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.

PR-URL: https://github.com/nodejs/node/pull/47604
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Chengzhong Wu 2023-05-05 19:22:42 +08:00 committed by GitHub
parent 3c82d48cc0
commit 64549731b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 126 additions and 24 deletions

View File

@ -130,8 +130,11 @@ added:
- v12.19.0 - v12.19.0
--> -->
* `object` {any} Any arbitrary JavaScript value.
Mark an object as not transferable. If `object` occurs in the transfer list of 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. This is a no-op if
`object` is a primitive value.
In particular, this makes sense for objects that can be cloned, rather than 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. transferred, and which are used by other objects on the sending side.
@ -150,11 +153,17 @@ const typedArray2 = new Float64Array(pooledBuffer);
markAsUntransferable(pooledBuffer); markAsUntransferable(pooledBuffer);
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
port1.postMessage(typedArray1, [ typedArray1.buffer ]); try {
// This will throw an error, because pooledBuffer is not transferable.
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
} catch (error) {
// error.name === 'DataCloneError'
}
// The following line prints the contents of typedArray1 -- it still owns // The following line prints the contents of typedArray1 -- it still owns
// its memory and has been cloned, not transferred. Without // its memory and has not been transferred. Without
// `markAsUntransferable()`, this would print an empty Uint8Array. // `markAsUntransferable()`, this would print an empty Uint8Array and the
// postMessage call would have succeeded.
// typedArray2 is intact as well. // typedArray2 is intact as well.
console.log(typedArray1); console.log(typedArray1);
console.log(typedArray2); console.log(typedArray2);
@ -162,6 +171,29 @@ console.log(typedArray2);
There is no equivalent to this API in browsers. There is no equivalent to this API in browsers.
## `worker.isMarkedAsUntransferable(object)`
<!-- YAML
added: REPLACEME
-->
* `object` {any} Any JavaScript value.
* Returns: {boolean}
Check if an object is marked as not transferable with
[`markAsUntransferable()`][].
```js
const { markAsUntransferable, isMarkedAsUntransferable } = require('node:worker_threads');
const pooledBuffer = new ArrayBuffer(8);
markAsUntransferable(pooledBuffer);
isMarkedAsUntransferable(pooledBuffer); // Returns true.
```
There is no equivalent to this API in browsers.
## `worker.moveMessagePortToContext(port, contextifiedSandbox)` ## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
<!-- YAML <!-- YAML
@ -568,6 +600,10 @@ are part of the channel.
<!-- YAML <!-- YAML
added: v10.5.0 added: v10.5.0
changes: 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: - version:
- v15.14.0 - v15.14.0
- v14.18.0 - v14.18.0

View File

@ -1053,6 +1053,15 @@ function markAsUntransferable(obj) {
obj[untransferable_object_private_symbol] = true; obj[untransferable_object_private_symbol] = true;
} }
// This simply checks if the object is marked as untransferable and doesn't
// check whether we are able to transfer it.
function isMarkedAsUntransferable(obj) {
if (obj == null)
return false;
// Private symbols are not inherited.
return obj[untransferable_object_private_symbol] !== undefined;
}
// A toggle used to access the zero fill setting of the array buffer allocator // A toggle used to access the zero fill setting of the array buffer allocator
// in C++. // in C++.
// |zeroFill| can be undefined when running inside an isolate where we // |zeroFill| can be undefined when running inside an isolate where we
@ -1079,6 +1088,7 @@ module.exports = {
FastBuffer, FastBuffer,
addBufferPrototypeMethods, addBufferPrototypeMethods,
markAsUntransferable, markAsUntransferable,
isMarkedAsUntransferable,
createUnsafeBuffer, createUnsafeBuffer,
readUInt16BE, readUInt16BE,
readUInt32BE, readUInt32BE,

View File

@ -30,13 +30,21 @@ const {
WORKER_TO_MAIN_THREAD_NOTIFICATION, WORKER_TO_MAIN_THREAD_NOTIFICATION,
} = require('internal/modules/esm/shared_constants'); } = require('internal/modules/esm/shared_constants');
const { initializeHooks } = require('internal/modules/esm/utils'); const { initializeHooks } = require('internal/modules/esm/utils');
const { isMarkedAsUntransferable } = require('internal/buffer');
function transferArrayBuffer(hasError, source) { function transferArrayBuffer(hasError, source) {
if (hasError || source == null) return; if (hasError || source == null) return;
if (isArrayBuffer(source)) return [source]; let arrayBuffer;
if (isTypedArray(source)) return [TypedArrayPrototypeGetBuffer(source)]; if (isArrayBuffer(source)) {
if (isDataView(source)) return [DataViewPrototypeGetBuffer(source)]; arrayBuffer = source;
} else if (isTypedArray(source)) {
arrayBuffer = TypedArrayPrototypeGetBuffer(source);
} else if (isDataView(source)) {
arrayBuffer = DataViewPrototypeGetBuffer(source);
}
if (arrayBuffer && !isMarkedAsUntransferable(arrayBuffer)) {
return [arrayBuffer];
}
} }
function wrapMessage(status, body) { function wrapMessage(status, body) {

View File

@ -20,6 +20,7 @@ const {
const { const {
markAsUntransferable, markAsUntransferable,
isMarkedAsUntransferable,
} = require('internal/buffer'); } = require('internal/buffer');
module.exports = { module.exports = {
@ -27,6 +28,7 @@ module.exports = {
MessagePort, MessagePort,
MessageChannel, MessageChannel,
markAsUntransferable, markAsUntransferable,
isMarkedAsUntransferable,
moveMessagePortToContext, moveMessagePortToContext,
receiveMessageOnPort, receiveMessageOnPort,
resourceLimits, resourceLimits,

View File

@ -67,7 +67,7 @@
V(change_string, "change") \ V(change_string, "change") \
V(channel_string, "channel") \ V(channel_string, "channel") \
V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \ 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(code_string, "code") \
V(commonjs_string, "commonjs") \ V(commonjs_string, "commonjs") \
V(config_string, "config") \ V(config_string, "config") \
@ -302,6 +302,8 @@
V(time_to_first_header_string, "timeToFirstHeader") \ V(time_to_first_header_string, "timeToFirstHeader") \
V(tls_ticket_string, "tlsTicket") \ V(tls_ticket_string, "tlsTicket") \
V(transfer_string, "transfer") \ V(transfer_string, "transfer") \
V(transfer_unsupported_type_str, \
"Cannot transfer object of unsupported type.") \
V(ttl_string, "ttl") \ V(ttl_string, "ttl") \
V(type_string, "type") \ V(type_string, "type") \
V(uid_string, "uid") \ V(uid_string, "uid") \

View File

@ -459,11 +459,14 @@ Maybe<bool> Message::Serialize(Environment* env,
.To(&untransferable)) { .To(&untransferable)) {
return Nothing<bool>(); 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 // Currently, we support ArrayBuffers and BaseObjects for which
// GetTransferMode() does not return kUntransferable. // GetTransferMode() returns kTransferable.
if (entry->IsArrayBuffer()) { if (entry->IsArrayBuffer()) {
Local<ArrayBuffer> ab = entry.As<ArrayBuffer>(); Local<ArrayBuffer> ab = entry.As<ArrayBuffer>();
// If we cannot render the ArrayBuffer unusable in this Isolate, // If we cannot render the ArrayBuffer unusable in this Isolate,
@ -474,7 +477,10 @@ Maybe<bool> Message::Serialize(Environment* env,
// raw data *and* an Isolate with a non-default ArrayBuffer allocator // raw data *and* an Isolate with a non-default ArrayBuffer allocator
// is always going to outlive any Workers it creates, and so will its // is always going to outlive any Workers it creates, and so will its
// allocator along with it. // 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) != if (std::find(array_buffers.begin(), array_buffers.end(), ab) !=
array_buffers.end()) { array_buffers.end()) {
ThrowDataCloneException( ThrowDataCloneException(
@ -524,8 +530,8 @@ Maybe<bool> Message::Serialize(Environment* env,
entry.As<Object>()->GetConstructorName())); entry.As<Object>()->GetConstructorName()));
return Nothing<bool>(); return Nothing<bool>();
} }
if (host_object && host_object->GetTransferMode() != if (host_object && host_object->GetTransferMode() ==
BaseObject::TransferMode::kUntransferable) { BaseObject::TransferMode::kTransferable) {
delegate.AddHostObject(host_object); delegate.AddHostObject(host_object);
continue; continue;
} }

View File

@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength; 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.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0); assert.notStrictEqual(buffer.byteLength, 0);

View File

@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength; const origByteLength = buffer.byteLength;
port1.postMessage(buffer, [buffer]); assert.throws(() => port1.postMessage(buffer, [buffer]), {
code: 25,
name: 'DataCloneError',
});
assert.strictEqual(buffer.byteLength, origByteLength); assert.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0); assert.notStrictEqual(buffer.byteLength, 0);

View File

@ -13,7 +13,10 @@ assert.strictEqual(a.buffer, b.buffer);
const length = a.length; const length = a.length;
const { port1 } = new MessageChannel(); 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: // Verify that the pool ArrayBuffer has not actually been transferred:
assert.strictEqual(a.buffer, b.buffer); assert.strictEqual(a.buffer, b.buffer);

View File

@ -12,6 +12,13 @@ const { MessageChannel } = require('worker_threads');
typedArray[0] = 0x12345678; typedArray[0] = 0x12345678;
port1.postMessage(typedArray, [ arrayBuffer ]); 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) => { port2.on('message', common.mustCall((received) => {
assert.strictEqual(received[0], 0x12345678); assert.strictEqual(received[0], 0x12345678);
port2.close(common.mustCall()); port2.close(common.mustCall());

View File

@ -31,7 +31,7 @@ const { internalBinding } = require('internal/test/binding');
port1.postMessage(nativeObject); port1.postMessage(nativeObject);
}, { }, {
name: 'DataCloneError', name: 'DataCloneError',
message: /Cannot transfer object of unsupported type\.$/ message: /Cannot clone object of unsupported type\.$/
}); });
port1.close(); port1.close();
} }

View File

@ -1,19 +1,22 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const { MessageChannel, markAsUntransferable } = require('worker_threads'); const { MessageChannel, markAsUntransferable, isMarkedAsUntransferable } = require('worker_threads');
{ {
const ab = new ArrayBuffer(8); const ab = new ArrayBuffer(8);
markAsUntransferable(ab); markAsUntransferable(ab);
assert.ok(isMarkedAsUntransferable(ab));
assert.strictEqual(ab.byteLength, 8); assert.strictEqual(ab.byteLength, 8);
const { port1, port2 } = new MessageChannel(); const { port1 } = new MessageChannel();
port1.postMessage(ab, [ ab ]); assert.throws(() => port1.postMessage(ab, [ ab ]), {
code: 25,
name: 'DataCloneError',
});
assert.strictEqual(ab.byteLength, 8); // The AB is not detached. assert.strictEqual(ab.byteLength, 8); // The AB is not detached.
port2.once('message', common.mustCall());
} }
{ {
@ -21,17 +24,36 @@ const { MessageChannel, markAsUntransferable } = require('worker_threads');
const channel2 = new MessageChannel(); const channel2 = new MessageChannel();
markAsUntransferable(channel2.port1); markAsUntransferable(channel2.port1);
assert.ok(isMarkedAsUntransferable(channel2.port1));
assert.throws(() => { assert.throws(() => {
channel1.port1.postMessage(channel2.port1, [ channel2.port1 ]); 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.port1.postMessage('still works, not closed/transferred');
channel2.port2.once('message', common.mustCall()); channel2.port2.once('message', common.mustCall());
} }
{ {
for (const value of [0, null, false, true, undefined, [], {}]) { for (const value of [0, null, false, true, undefined]) {
markAsUntransferable(value); // Has no visible effect. markAsUntransferable(value); // Has no visible effect.
assert.ok(!isMarkedAsUntransferable(value));
}
for (const value of [[], {}]) {
markAsUntransferable(value);
assert.ok(isMarkedAsUntransferable(value));
} }
} }
{
// Verifies that the mark is not inherited.
class Foo {}
markAsUntransferable(Foo.prototype);
assert.ok(isMarkedAsUntransferable(Foo.prototype));
const foo = new Foo();
assert.ok(!isMarkedAsUntransferable(foo));
}