Skip to content

Commit 139442c

Browse files
committed
worker: add public method for marking objects as untransferable
We currently mark a number of `ArrayBuffer`s as not transferable, including the `Buffer` pool and ones with finalizers provided by C++ addons. There is no good reason to assume that userland code might not encounter similar problems, for example when doing `ArrayBuffer` pooling similar to ours. Therefore, provide an API that lets userland code also mark objects as not transferable. PR-URL: #33979 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Gus Caplan <[email protected]>
1 parent 44864d7 commit 139442c

9 files changed

+122
-20
lines changed

doc/api/worker_threads.md

+40
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,42 @@ if (isMainThread) {
8282
}
8383
```
8484

85+
## `worker.markAsUntransferable(object)`
86+
<!-- YAML
87+
added: REPLACEME
88+
-->
89+
90+
Mark an object as not transferable. If `object` occurs in the transfer list of
91+
a [`port.postMessage()`][] call, it will be ignored.
92+
93+
In particular, this makes sense for objects that can be cloned, rather than
94+
transferred, and which are used by other objects on the sending side.
95+
For example, Node.js marks the `ArrayBuffer`s it uses for its
96+
[`Buffer` pool][`Buffer.allocUnsafe()`] with this.
97+
98+
This operation cannot be undone.
99+
100+
```js
101+
const { MessageChannel, markAsUntransferable } = require('worker_threads');
102+
103+
const pooledBuffer = new ArrayBuffer(8);
104+
const typedArray1 = new Uint8Array(pooledBuffer);
105+
const typedArray2 = new Float64Array(pooledBuffer);
106+
107+
markAsUntransferable(pooledBuffer);
108+
109+
const { port1 } = new MessageChannel();
110+
port1.postMessage(typedArray1, [ typedArray1.buffer ]);
111+
112+
// The following line prints the contents of typedArray1 -- it still owns its
113+
// memory and has been cloned, not transfered. Without `markAsUntransferable()`,
114+
// this would print an empty Uint8Array. typedArray2 is intact as well.
115+
console.log(typedArray1);
116+
console.log(typedArray2);
117+
```
118+
119+
There is no equivalent to this API in browsers.
120+
85121
## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
86122
<!-- YAML
87123
added: v11.13.0
@@ -442,6 +478,9 @@ For `Buffer` instances, specifically, whether the underlying
442478
`ArrayBuffer` can be transferred or cloned depends entirely on how
443479
instances were created, which often cannot be reliably determined.
444480

481+
An `ArrayBuffer` can be marked with [`markAsUntransferable()`][] to indicate
482+
that it should always be cloned and never transferred.
483+
445484
Depending on how a `Buffer` instance was created, it may or may
446485
not own its underlying `ArrayBuffer`. An `ArrayBuffer` must not
447486
be transferred unless it is known that the `Buffer` instance
@@ -858,6 +897,7 @@ active handle in the event system. If the worker is already `unref()`ed calling
858897
[`cluster` module]: cluster.html
859898
[`fs.open()`]: fs.html#fs_fs_open_path_flags_mode_callback
860899
[`fs.close()`]: fs.html#fs_fs_close_fd_callback
900+
[`markAsUntransferable()`]: #worker_threads_worker_markasuntransferable_object
861901
[`port.on('message')`]: #worker_threads_event_message
862902
[`port.onmessage()`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/onmessage
863903
[`port.postMessage()`]: #worker_threads_port_postmessage_value_transferlist

lib/buffer.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,11 @@ const {
5959
zeroFill: bindingZeroFill
6060
} = internalBinding('buffer');
6161
const {
62-
arraybuffer_untransferable_private_symbol,
6362
getOwnNonIndexProperties,
6463
propertyFilter: {
6564
ALL_PROPERTIES,
6665
ONLY_ENUMERABLE
6766
},
68-
setHiddenValue,
6967
} = internalBinding('util');
7068
const {
7169
customInspectSymbol,
@@ -83,7 +81,6 @@ const {
8381
} = require('internal/util/inspect');
8482
const { encodings } = internalBinding('string_decoder');
8583

86-
8784
const {
8885
codes: {
8986
ERR_BUFFER_OUT_OF_BOUNDS,
@@ -104,6 +101,7 @@ const {
104101

105102
const {
106103
FastBuffer,
104+
markAsUntransferable,
107105
addBufferPrototypeMethods
108106
} = require('internal/buffer');
109107

@@ -156,7 +154,7 @@ function createUnsafeBuffer(size) {
156154
function createPool() {
157155
poolSize = Buffer.poolSize;
158156
allocPool = createUnsafeBuffer(poolSize).buffer;
159-
setHiddenValue(allocPool, arraybuffer_untransferable_private_symbol, true);
157+
markAsUntransferable(allocPool);
160158
poolOffset = 0;
161159
}
162160
createPool();

lib/internal/buffer.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const {
2727
ucs2Write,
2828
utf8Write
2929
} = internalBinding('buffer');
30+
const {
31+
untransferable_object_private_symbol,
32+
setHiddenValue,
33+
} = internalBinding('util');
3034

3135
// Temporary buffers to convert numbers.
3236
const float32Array = new Float32Array(1);
@@ -1021,7 +1025,16 @@ function addBufferPrototypeMethods(proto) {
10211025
proto.utf8Write = utf8Write;
10221026
}
10231027

1028+
// This would better be placed in internal/worker/io.js, but that doesn't work
1029+
// because Buffer needs this and that would introduce a cyclic dependency.
1030+
function markAsUntransferable(obj) {
1031+
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null)
1032+
return; // This object is a primitive and therefore already untransferable.
1033+
setHiddenValue(obj, untransferable_object_private_symbol, true);
1034+
}
1035+
10241036
module.exports = {
10251037
FastBuffer,
1026-
addBufferPrototypeMethods
1038+
addBufferPrototypeMethods,
1039+
markAsUntransferable,
10271040
};

lib/worker_threads.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ const {
1212
MessagePort,
1313
MessageChannel,
1414
moveMessagePortToContext,
15-
receiveMessageOnPort
15+
receiveMessageOnPort,
1616
} = require('internal/worker/io');
1717

18+
const {
19+
markAsUntransferable,
20+
} = require('internal/buffer');
21+
1822
module.exports = {
1923
isMainThread,
2024
MessagePort,
2125
MessageChannel,
26+
markAsUntransferable,
2227
moveMessagePortToContext,
2328
receiveMessageOnPort,
2429
resourceLimits,

src/env.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,14 @@ constexpr size_t kFsStatsBufferLength =
153153
// "node:" prefix to avoid name clashes with third-party code.
154154
#define PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(V) \
155155
V(alpn_buffer_private_symbol, "node:alpnBuffer") \
156-
V(arraybuffer_untransferable_private_symbol, "node:untransferableBuffer") \
157156
V(arrow_message_private_symbol, "node:arrowMessage") \
158157
V(contextify_context_private_symbol, "node:contextify:context") \
159158
V(contextify_global_private_symbol, "node:contextify:global") \
160159
V(decorated_private_symbol, "node:decorated") \
161160
V(napi_type_tag, "node:napi:type_tag") \
162161
V(napi_wrapper, "node:napi:wrapper") \
163162
V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \
163+
V(untransferable_object_private_symbol, "node:untransferableObject") \
164164

165165
// Symbols are per-isolate primitives but Environment proxies them
166166
// for the sake of convenience.

src/node_api.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct node_napi_env__ : public napi_env__ {
2929
v8::Local<v8::ArrayBuffer> ab) const override {
3030
return ab->SetPrivate(
3131
context(),
32-
node_env()->arraybuffer_untransferable_private_symbol(),
32+
node_env()->untransferable_object_private_symbol(),
3333
v8::True(isolate));
3434
}
3535

src/node_buffer.cc

+5-1
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ MaybeLocal<Object> New(Environment* env,
412412

413413
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), data, length);
414414
if (ab->SetPrivate(env->context(),
415-
env->arraybuffer_untransferable_private_symbol(),
415+
env->untransferable_object_private_symbol(),
416416
True(env->isolate())).IsNothing()) {
417417
callback(data, hint);
418418
return Local<Object>();
@@ -1188,6 +1188,10 @@ void Initialize(Local<Object> target,
11881188
uint32_t* zero_fill_field = allocator->zero_fill_field();
11891189
Local<ArrayBuffer> array_buffer = ArrayBuffer::New(
11901190
env->isolate(), zero_fill_field, sizeof(*zero_fill_field));
1191+
array_buffer->SetPrivate(
1192+
env->context(),
1193+
env->untransferable_object_private_symbol(),
1194+
True(env->isolate())).Check();
11911195
CHECK(target
11921196
->Set(env->context(),
11931197
FIXED_ONE_BYTE_STRING(env->isolate(), "zeroFill"),

src/node_messaging.cc

+16-11
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,21 @@ Maybe<bool> Message::Serialize(Environment* env,
426426
std::vector<Local<ArrayBuffer>> array_buffers;
427427
for (uint32_t i = 0; i < transfer_list_v.length(); ++i) {
428428
Local<Value> entry = transfer_list_v[i];
429-
// Currently, we support ArrayBuffers and MessagePorts.
429+
if (entry->IsObject()) {
430+
// See https://github.com/nodejs/node/pull/30339#issuecomment-552225353
431+
// for details.
432+
bool untransferable;
433+
if (!entry.As<Object>()->HasPrivate(
434+
context,
435+
env->untransferable_object_private_symbol())
436+
.To(&untransferable)) {
437+
return Nothing<bool>();
438+
}
439+
if (untransferable) continue;
440+
}
441+
442+
// Currently, we support ArrayBuffers and BaseObjects for which
443+
// GetTransferMode() does not return kUntransferable.
430444
if (entry->IsArrayBuffer()) {
431445
Local<ArrayBuffer> ab = entry.As<ArrayBuffer>();
432446
// If we cannot render the ArrayBuffer unusable in this Isolate and
@@ -435,16 +449,7 @@ Maybe<bool> Message::Serialize(Environment* env,
435449
!env->isolate_data()->uses_node_allocator()) {
436450
continue;
437451
}
438-
// See https://github.com/nodejs/node/pull/30339#issuecomment-552225353
439-
// for details.
440-
bool untransferrable;
441-
if (!ab->HasPrivate(
442-
context,
443-
env->arraybuffer_untransferable_private_symbol())
444-
.To(&untransferrable)) {
445-
return Nothing<bool>();
446-
}
447-
if (untransferrable) continue;
452+
448453
if (std::find(array_buffers.begin(), array_buffers.end(), ab) !=
449454
array_buffers.end()) {
450455
ThrowDataCloneException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { MessageChannel, markAsUntransferable } = require('worker_threads');
5+
6+
{
7+
const ab = new ArrayBuffer(8);
8+
9+
markAsUntransferable(ab);
10+
assert.strictEqual(ab.byteLength, 8);
11+
12+
const { port1, port2 } = new MessageChannel();
13+
port1.postMessage(ab, [ ab ]);
14+
15+
assert.strictEqual(ab.byteLength, 8); // The AB is not detached.
16+
port2.once('message', common.mustCall());
17+
}
18+
19+
{
20+
const channel1 = new MessageChannel();
21+
const channel2 = new MessageChannel();
22+
23+
markAsUntransferable(channel2.port1);
24+
25+
assert.throws(() => {
26+
channel1.port1.postMessage(channel2.port1, [ channel2.port1 ]);
27+
}, /was found in message but not listed in transferList/);
28+
29+
channel2.port1.postMessage('still works, not closed/transferred');
30+
channel2.port2.once('message', common.mustCall());
31+
}
32+
33+
{
34+
for (const value of [0, null, false, true, undefined, [], {}]) {
35+
markAsUntransferable(value); // Has no visible effect.
36+
}
37+
}

0 commit comments

Comments
 (0)