From 9112a310123d1c6738a97190dc1f97b3eb21217f Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Tue, 18 Apr 2023 15:02:48 +0800 Subject: [PATCH 1/3] 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. --- doc/api/worker_threads.md | 46 +++++++++++++++++-- lib/internal/buffer.js | 9 ++++ lib/internal/modules/esm/worker.js | 16 +++++-- lib/worker_threads.js | 2 + src/env_properties.h | 4 +- src/node_messaging.cc | 16 +++++-- test/addons/worker-buffer-callback/test.js | 5 +- .../test_worker_buffer_callback/test.js | 5 +- .../test-buffer-pool-untransferable.js | 5 +- .../test-worker-message-port-arraybuffer.js | 7 +++ ...est-worker-message-port-transfer-native.js | 2 +- ...ge-transfer-port-mark-as-untransferable.js | 13 ++++-- 12 files changed, 109 insertions(+), 21 deletions(-) diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index f3f500c9075058..c3b99404a1d9f0 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -128,10 +128,18 @@ 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. --> +* `object` {any} Any arbitrary JavaScript value. + 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 transferred, and which are used by other objects on the sending side. @@ -150,10 +158,15 @@ const typedArray2 = new Float64Array(pooledBuffer); markAsUntransferable(pooledBuffer); 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 -// its memory and has been cloned, not transferred. Without +// its memory and has not been transferred. Without // `markAsUntransferable()`, this would print an empty Uint8Array. // typedArray2 is intact as well. console.log(typedArray1); @@ -162,6 +175,29 @@ console.log(typedArray2); There is no equivalent to this API in browsers. +## `worker.isMarkedAsUntransferable(object)` + + + +* `object` {any} Any arbitrary JavaScript value. +* Returns: {boolean} + +Check is 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)` * `object` {any} Any arbitrary JavaScript value. @@ -167,7 +162,8 @@ try { // The following line prints the contents of typedArray1 -- it still owns // 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. console.log(typedArray1); console.log(typedArray2); @@ -181,7 +177,7 @@ There is no equivalent to this API in browsers. added: REPLACEME --> -* `object` {any} Any arbitrary JavaScript value. +* `object` {any} Any JavaScript value. * Returns: {boolean} Check is an object is marked as not transferable with diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js index b8f867a9a725be..476113f3a159be 100644 --- a/lib/internal/buffer.js +++ b/lib/internal/buffer.js @@ -6,6 +6,7 @@ const { Float64Array, MathFloor, Number, + ObjectPrototypeHasOwnProperty, Uint8Array, } = primordials; @@ -1056,8 +1057,12 @@ function markAsUntransferable(obj) { // This simply checks if the object is marked as untransferable and doesn't // check the ability to be transferred. function isMarkedAsUntransferable(obj) { - if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) + if (obj == null) + return false; + // v8::Object::HasPrivate checks own properties only. + if (!ObjectPrototypeHasOwnProperty(obj, untransferable_object_private_symbol)) { return false; + } return obj[untransferable_object_private_symbol] === true; } diff --git a/test/parallel/test-worker-message-transfer-port-mark-as-untransferable.js b/test/parallel/test-worker-message-transfer-port-mark-as-untransferable.js index 426cc7a1195fdf..94ae4e8c5fd237 100644 --- a/test/parallel/test-worker-message-transfer-port-mark-as-untransferable.js +++ b/test/parallel/test-worker-message-transfer-port-mark-as-untransferable.js @@ -1,12 +1,13 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const { MessageChannel, markAsUntransferable } = require('worker_threads'); +const { MessageChannel, markAsUntransferable, isMarkedAsUntransferable } = require('worker_threads'); { const ab = new ArrayBuffer(8); markAsUntransferable(ab); + assert.ok(isMarkedAsUntransferable(ab)); assert.strictEqual(ab.byteLength, 8); const { port1 } = new MessageChannel(); @@ -23,6 +24,7 @@ const { MessageChannel, markAsUntransferable } = require('worker_threads'); const channel2 = new MessageChannel(); markAsUntransferable(channel2.port1); + assert.ok(isMarkedAsUntransferable(channel2.port1)); assert.throws(() => { channel1.port1.postMessage(channel2.port1, [ channel2.port1 ]); @@ -36,7 +38,22 @@ const { MessageChannel, markAsUntransferable } = require('worker_threads'); } { - for (const value of [0, null, false, true, undefined, [], {}]) { + for (const value of [0, null, false, true, undefined]) { 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)); } From eb3c867b0d9cd7dc3e2d7aae0d2c574649480b3c Mon Sep 17 00:00:00 2001 From: legendecas Date: Wed, 3 May 2023 19:45:40 +0800 Subject: [PATCH 3/3] fixup! src: throw DataCloneError on transfering untransferable objects --- doc/api/worker_threads.md | 2 +- lib/internal/buffer.js | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 862b49cb364d72..09b695eb96ea29 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -180,7 +180,7 @@ added: REPLACEME * `object` {any} Any JavaScript value. * Returns: {boolean} -Check is an object is marked as not transferable with +Check if an object is marked as not transferable with [`markAsUntransferable()`][]. ```js diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js index 476113f3a159be..a65dd43263714e 100644 --- a/lib/internal/buffer.js +++ b/lib/internal/buffer.js @@ -6,7 +6,6 @@ const { Float64Array, MathFloor, Number, - ObjectPrototypeHasOwnProperty, Uint8Array, } = primordials; @@ -1055,15 +1054,12 @@ function markAsUntransferable(obj) { } // This simply checks if the object is marked as untransferable and doesn't -// check the ability to be transferred. +// check whether we are able to transfer it. function isMarkedAsUntransferable(obj) { if (obj == null) return false; - // v8::Object::HasPrivate checks own properties only. - if (!ObjectPrototypeHasOwnProperty(obj, untransferable_object_private_symbol)) { - return false; - } - return obj[untransferable_object_private_symbol] === true; + // 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