mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
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:
parent
3c82d48cc0
commit
64549731b6
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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") \
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user