Skip to content

Commit 6454973

Browse files
authored
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: #47604 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent 3c82d48 commit 6454973

12 files changed

+126
-24
lines changed

doc/api/worker_threads.md

+40-4
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,11 @@ added:
130130
- v12.19.0
131131
-->
132132

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

136139
In particular, this makes sense for objects that can be cloned, rather than
137140
transferred, and which are used by other objects on the sending side.
@@ -150,18 +153,47 @@ const typedArray2 = new Float64Array(pooledBuffer);
150153
markAsUntransferable(pooledBuffer);
151154

152155
const { port1 } = new MessageChannel();
153-
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
156+
try {
157+
// This will throw an error, because pooledBuffer is not transferable.
158+
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
159+
} catch (error) {
160+
// error.name === 'DataCloneError'
161+
}
154162

155163
// The following line prints the contents of typedArray1 -- it still owns
156-
// its memory and has been cloned, not transferred. Without
157-
// `markAsUntransferable()`, this would print an empty Uint8Array.
164+
// its memory and has not been transferred. Without
165+
// `markAsUntransferable()`, this would print an empty Uint8Array and the
166+
// postMessage call would have succeeded.
158167
// typedArray2 is intact as well.
159168
console.log(typedArray1);
160169
console.log(typedArray2);
161170
```
162171

163172
There is no equivalent to this API in browsers.
164173

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

167199
<!-- YAML
@@ -568,6 +600,10 @@ are part of the channel.
568600
<!-- YAML
569601
added: v10.5.0
570602
changes:
603+
- version: REPLACEME
604+
pr-url: https://github.com/nodejs/node/pull/47604
605+
description: An error is thrown when an untransferable object is in the
606+
transfer list.
571607
- version:
572608
- v15.14.0
573609
- v14.18.0

lib/internal/buffer.js

+10
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,15 @@ 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 whether we are able to transfer it.
1058+
function isMarkedAsUntransferable(obj) {
1059+
if (obj == null)
1060+
return false;
1061+
// Private symbols are not inherited.
1062+
return obj[untransferable_object_private_symbol] !== undefined;
1063+
}
1064+
10561065
// A toggle used to access the zero fill setting of the array buffer allocator
10571066
// in C++.
10581067
// |zeroFill| can be undefined when running inside an isolate where we
@@ -1079,6 +1088,7 @@ module.exports = {
10791088
FastBuffer,
10801089
addBufferPrototypeMethods,
10811090
markAsUntransferable,
1091+
isMarkedAsUntransferable,
10821092
createUnsafeBuffer,
10831093
readUInt16BE,
10841094
readUInt32BE,

lib/internal/modules/esm/worker.js

+12-4
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 { 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

+2
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

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
V(change_string, "change") \
6868
V(channel_string, "channel") \
6969
V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \
70-
V(clone_unsupported_type_str, "Cannot transfer object of unsupported type.") \
70+
V(clone_unsupported_type_str, "Cannot clone object of unsupported type.") \
7171
V(code_string, "code") \
7272
V(commonjs_string, "commonjs") \
7373
V(config_string, "config") \
@@ -302,6 +302,8 @@
302302
V(time_to_first_header_string, "timeToFirstHeader") \
303303
V(tls_ticket_string, "tlsTicket") \
304304
V(transfer_string, "transfer") \
305+
V(transfer_unsupported_type_str, \
306+
"Cannot transfer object of unsupported type.") \
305307
V(ttl_string, "ttl") \
306308
V(type_string, "type") \
307309
V(uid_string, "uid") \

src/node_messaging.cc

+11-5
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

+4-1
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

+4-1
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

+4-1
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

+7
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());

test/parallel/test-worker-message-port-transfer-native.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const { internalBinding } = require('internal/test/binding');
3131
port1.postMessage(nativeObject);
3232
}, {
3333
name: 'DataCloneError',
34-
message: /Cannot transfer object of unsupported type\.$/
34+
message: /Cannot clone object of unsupported type\.$/
3535
});
3636
port1.close();
3737
}
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,59 @@
11
'use strict';
22
const common = require('../common');
33
const assert = require('assert');
4-
const { MessageChannel, markAsUntransferable } = require('worker_threads');
4+
const { MessageChannel, markAsUntransferable, isMarkedAsUntransferable } = require('worker_threads');
55

66
{
77
const ab = new ArrayBuffer(8);
88

99
markAsUntransferable(ab);
10+
assert.ok(isMarkedAsUntransferable(ab));
1011
assert.strictEqual(ab.byteLength, 8);
1112

12-
const { port1, port2 } = new MessageChannel();
13-
port1.postMessage(ab, [ ab ]);
13+
const { port1 } = new MessageChannel();
14+
assert.throws(() => port1.postMessage(ab, [ ab ]), {
15+
code: 25,
16+
name: 'DataCloneError',
17+
});
1418

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

1922
{
2023
const channel1 = new MessageChannel();
2124
const channel2 = new MessageChannel();
2225

2326
markAsUntransferable(channel2.port1);
27+
assert.ok(isMarkedAsUntransferable(channel2.port1));
2428

2529
assert.throws(() => {
2630
channel1.port1.postMessage(channel2.port1, [ channel2.port1 ]);
27-
}, /was found in message but not listed in transferList/);
31+
}, {
32+
code: 25,
33+
name: 'DataCloneError',
34+
});
2835

2936
channel2.port1.postMessage('still works, not closed/transferred');
3037
channel2.port2.once('message', common.mustCall());
3138
}
3239

3340
{
34-
for (const value of [0, null, false, true, undefined, [], {}]) {
41+
for (const value of [0, null, false, true, undefined]) {
3542
markAsUntransferable(value); // Has no visible effect.
43+
assert.ok(!isMarkedAsUntransferable(value));
3644
}
45+
for (const value of [[], {}]) {
46+
markAsUntransferable(value);
47+
assert.ok(isMarkedAsUntransferable(value));
48+
}
49+
}
50+
51+
{
52+
// Verifies that the mark is not inherited.
53+
class Foo {}
54+
markAsUntransferable(Foo.prototype);
55+
assert.ok(isMarkedAsUntransferable(Foo.prototype));
56+
57+
const foo = new Foo();
58+
assert.ok(!isMarkedAsUntransferable(foo));
3759
}

0 commit comments

Comments
 (0)