Skip to content

Commit 577d73b

Browse files
committed
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.
1 parent 682256c commit 577d73b

File tree

12 files changed

+109
-21
lines changed

12 files changed

+109
-21
lines changed

doc/api/worker_threads.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,18 @@ if (isMainThread) {
128128
added:
129129
- v14.5.0
130130
- v12.19.0
131+
changes:
132+
- version: REPLACEME
133+
pr-url: https://github.com/nodejs/node/pull/47604
134+
description: An error is thrown when the untransferable object is in the
135+
transfer list.
131136
-->
132137

138+
* `object` {any} Any arbitrary JavaScript value.
139+
133140
Mark an object as not transferable. If `object` occurs in the transfer list of
134-
a [`port.postMessage()`][] call, it is ignored.
141+
a [`port.postMessage()`][] call, an error is thrown. This is a no-op if
142+
`object` is a primitive value.
135143

136144
In particular, this makes sense for objects that can be cloned, rather than
137145
transferred, and which are used by other objects on the sending side.
@@ -150,10 +158,15 @@ const typedArray2 = new Float64Array(pooledBuffer);
150158
markAsUntransferable(pooledBuffer);
151159

152160
const { port1 } = new MessageChannel();
153-
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
161+
try {
162+
// This will throw an error, because pooledBuffer is not transferable.
163+
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
164+
} catch (error) {
165+
// error.name === 'DataCloneError'
166+
}
154167

155168
// The following line prints the contents of typedArray1 -- it still owns
156-
// its memory and has been cloned, not transferred. Without
169+
// its memory and has not been transferred. Without
157170
// `markAsUntransferable()`, this would print an empty Uint8Array.
158171
// typedArray2 is intact as well.
159172
console.log(typedArray1);
@@ -162,6 +175,29 @@ console.log(typedArray2);
162175

163176
There is no equivalent to this API in browsers.
164177

178+
## `worker.isMarkedAsUntransferable(object)`
179+
180+
<!-- YAML
181+
added: REPLACEME
182+
-->
183+
184+
* `object` {any} Any arbitrary JavaScript value.
185+
* Returns: {boolean}
186+
187+
Check is an object is marked as not transferable with
188+
[`markAsUntransferable()`][].
189+
190+
```js
191+
const { markAsUntransferable, isMarkedAsUntransferable } = require('node:worker_threads');
192+
193+
const pooledBuffer = new ArrayBuffer(8);
194+
markAsUntransferable(pooledBuffer);
195+
196+
isMarkedAsUntransferable(pooledBuffer); // Returns true.
197+
```
198+
199+
There is no equivalent to this API in browsers.
200+
165201
## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
166202

167203
<!-- YAML
@@ -568,6 +604,10 @@ are part of the channel.
568604
<!-- YAML
569605
added: v10.5.0
570606
changes:
607+
- version: REPLACEME
608+
pr-url: https://github.com/nodejs/node/pull/47604
609+
description: An error is thrown when an untransferable object is in the
610+
transfer list.
571611
- version:
572612
- v15.14.0
573613
- v14.18.0

lib/internal/buffer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,14 @@ function markAsUntransferable(obj) {
10531053
obj[untransferable_object_private_symbol] = true;
10541054
}
10551055

1056+
// This simply checks if the object is marked as untransferable and doesn't
1057+
// check the ability to be transferred.
1058+
function isMarkedAsUntransferable(obj) {
1059+
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null)
1060+
return false;
1061+
return obj[untransferable_object_private_symbol] === true;
1062+
}
1063+
10561064
// A toggle used to access the zero fill setting of the array buffer allocator
10571065
// in C++.
10581066
// |zeroFill| can be undefined when running inside an isolate where we
@@ -1079,6 +1087,7 @@ module.exports = {
10791087
FastBuffer,
10801088
addBufferPrototypeMethods,
10811089
markAsUntransferable,
1090+
isMarkedAsUntransferable,
10821091
createUnsafeBuffer,
10831092
readUInt16BE,
10841093
readUInt32BE,

lib/internal/modules/esm/worker.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,21 @@ const {
3030
WORKER_TO_MAIN_THREAD_NOTIFICATION,
3131
} = require('internal/modules/esm/shared_constants');
3232
const { initializeESM, initializeHooks } = require('internal/modules/esm/utils');
33-
33+
const { isMarkedAsUntransferable } = require('internal/buffer');
3434

3535
function transferArrayBuffer(hasError, source) {
3636
if (hasError || source == null) return;
37-
if (isArrayBuffer(source)) return [source];
38-
if (isTypedArray(source)) return [TypedArrayPrototypeGetBuffer(source)];
39-
if (isDataView(source)) return [DataViewPrototypeGetBuffer(source)];
37+
let arrayBuffer;
38+
if (isArrayBuffer(source)) {
39+
arrayBuffer = source;
40+
} else if (isTypedArray(source)) {
41+
arrayBuffer = TypedArrayPrototypeGetBuffer(source);
42+
} else if (isDataView(source)) {
43+
arrayBuffer = DataViewPrototypeGetBuffer(source);
44+
}
45+
if (arrayBuffer && !isMarkedAsUntransferable(arrayBuffer)) {
46+
return [arrayBuffer];
47+
}
4048
}
4149

4250
function wrapMessage(status, body) {

lib/worker_threads.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ const {
2020

2121
const {
2222
markAsUntransferable,
23+
isMarkedAsUntransferable,
2324
} = require('internal/buffer');
2425

2526
module.exports = {
2627
isMainThread,
2728
MessagePort,
2829
MessageChannel,
2930
markAsUntransferable,
31+
isMarkedAsUntransferable,
3032
moveMessagePortToContext,
3133
receiveMessageOnPort,
3234
resourceLimits,

src/env_properties.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
V(change_string, "change") \
6767
V(channel_string, "channel") \
6868
V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \
69-
V(clone_unsupported_type_str, "Cannot transfer object of unsupported type.") \
69+
V(clone_unsupported_type_str, "Cannot clone object of unsupported type.") \
7070
V(code_string, "code") \
7171
V(commonjs_string, "commonjs") \
7272
V(config_string, "config") \
@@ -301,6 +301,8 @@
301301
V(time_to_first_header_string, "timeToFirstHeader") \
302302
V(tls_ticket_string, "tlsTicket") \
303303
V(transfer_string, "transfer") \
304+
V(transfer_unsupported_type_str, \
305+
"Cannot transfer object of unsupported type.") \
304306
V(ttl_string, "ttl") \
305307
V(type_string, "type") \
306308
V(uid_string, "uid") \

src/node_messaging.cc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,14 @@ Maybe<bool> Message::Serialize(Environment* env,
459459
.To(&untransferable)) {
460460
return Nothing<bool>();
461461
}
462-
if (untransferable) continue;
462+
if (untransferable) {
463+
ThrowDataCloneException(context, env->transfer_unsupported_type_str());
464+
return Nothing<bool>();
465+
}
463466
}
464467

465468
// Currently, we support ArrayBuffers and BaseObjects for which
466-
// GetTransferMode() does not return kUntransferable.
469+
// GetTransferMode() returns kTransferable.
467470
if (entry->IsArrayBuffer()) {
468471
Local<ArrayBuffer> ab = entry.As<ArrayBuffer>();
469472
// If we cannot render the ArrayBuffer unusable in this Isolate,
@@ -474,7 +477,10 @@ Maybe<bool> Message::Serialize(Environment* env,
474477
// raw data *and* an Isolate with a non-default ArrayBuffer allocator
475478
// is always going to outlive any Workers it creates, and so will its
476479
// allocator along with it.
477-
if (!ab->IsDetachable()) continue;
480+
if (!ab->IsDetachable() || ab->WasDetached()) {
481+
ThrowDataCloneException(context, env->transfer_unsupported_type_str());
482+
return Nothing<bool>();
483+
}
478484
if (std::find(array_buffers.begin(), array_buffers.end(), ab) !=
479485
array_buffers.end()) {
480486
ThrowDataCloneException(
@@ -524,8 +530,8 @@ Maybe<bool> Message::Serialize(Environment* env,
524530
entry.As<Object>()->GetConstructorName()));
525531
return Nothing<bool>();
526532
}
527-
if (host_object && host_object->GetTransferMode() !=
528-
BaseObject::TransferMode::kUntransferable) {
533+
if (host_object && host_object->GetTransferMode() ==
534+
BaseObject::TransferMode::kTransferable) {
529535
delegate.AddHostObject(host_object);
530536
continue;
531537
}

test/addons/worker-buffer-callback/test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);
99

1010
const { port1 } = new MessageChannel();
1111
const origByteLength = buffer.byteLength;
12-
port1.postMessage(buffer, [buffer.buffer]);
12+
assert.throws(() => port1.postMessage(buffer, [buffer.buffer]), {
13+
code: 25,
14+
name: 'DataCloneError',
15+
});
1316

1417
assert.strictEqual(buffer.byteLength, origByteLength);
1518
assert.notStrictEqual(buffer.byteLength, 0);

test/node-api/test_worker_buffer_callback/test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const { buffer } = require(`./build/${common.buildType}/binding`);
99

1010
const { port1 } = new MessageChannel();
1111
const origByteLength = buffer.byteLength;
12-
port1.postMessage(buffer, [buffer]);
12+
assert.throws(() => port1.postMessage(buffer, [buffer]), {
13+
code: 25,
14+
name: 'DataCloneError',
15+
});
1316

1417
assert.strictEqual(buffer.byteLength, origByteLength);
1518
assert.notStrictEqual(buffer.byteLength, 0);

test/parallel/test-buffer-pool-untransferable.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ assert.strictEqual(a.buffer, b.buffer);
1313
const length = a.length;
1414

1515
const { port1 } = new MessageChannel();
16-
port1.postMessage(a, [ a.buffer ]);
16+
assert.throws(() => port1.postMessage(a, [ a.buffer ]), {
17+
code: 25,
18+
name: 'DataCloneError',
19+
});
1720

1821
// Verify that the pool ArrayBuffer has not actually been transferred:
1922
assert.strictEqual(a.buffer, b.buffer);

test/parallel/test-worker-message-port-arraybuffer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ const { MessageChannel } = require('worker_threads');
1212
typedArray[0] = 0x12345678;
1313

1414
port1.postMessage(typedArray, [ arrayBuffer ]);
15+
assert.strictEqual(arrayBuffer.byteLength, 0);
16+
// Transferring again should throw a DataCloneError.
17+
assert.throws(() => port1.postMessage(typedArray, [ arrayBuffer ]), {
18+
code: 25,
19+
name: 'DataCloneError',
20+
});
21+
1522
port2.on('message', common.mustCall((received) => {
1623
assert.strictEqual(received[0], 0x12345678);
1724
port2.close(common.mustCall());

0 commit comments

Comments
 (0)