Skip to content

Commit 1c1ce8d

Browse files
fix(runtime): keep clone-only message reads read-only, and see an uncloneable brand created mid-clone (#490)
A clone-only message is read concurrently by every BroadcastChannel receiver and every getEnvironmentData call, and deserializing it cleared its transfer vectors, a write. Only the single receiver of a message with transferables writes them now. The serializer cached the uncloneable brand when it was created, so the first markAsUncloneable call in an isolate, made by a getter in the graph being written, went unseen and the marked object was cloned. It now picks the brand up once it exists.
1 parent 4ae32eb commit 1c1ce8d

3 files changed

Lines changed: 50 additions & 4 deletions

File tree

‎NativeScript/runtime/StructuredSerialization.cpp‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
161161
if (object->InternalFieldCount() > 0) {
162162
return Just(true);
163163
}
164+
if (uncloneableBrand_.IsEmpty()) {
165+
// markAsUncloneable creates the brand on its first call in an isolate,
166+
// which a getter in the graph being written can make.
167+
uncloneableBrand_ = messaging::UncloneableBrandIfAny(isolate);
168+
}
164169
if (!uncloneableBrand_.IsEmpty()) {
165170
bool uncloneable = false;
166171
if (!object->HasPrivate(isolate->GetCurrentContext(), uncloneableBrand_)
@@ -622,7 +627,8 @@ MaybeLocal<Value> SerializedValue::Deserialize(Isolate* isolate,
622627
"A message carrying transferred objects can only be read once.");
623628
return MaybeLocal<Value>();
624629
}
625-
if (HasTransferables()) {
630+
const bool singleReceiver = HasTransferables();
631+
if (singleReceiver) {
626632
consumed_ = true;
627633
}
628634

@@ -716,9 +722,12 @@ MaybeLocal<Value> SerializedValue::Deserialize(Isolate* isolate,
716722
ArrayBuffer::New(isolate, std::move(transferredBuffers_[i])));
717723
}
718724
// Handed over above; the vectors would otherwise keep reporting
719-
// transferables that are no longer here.
720-
transferredBuffers_.clear();
721-
transferredPorts_.clear();
725+
// transferables that are no longer here. Only the single receiver writes
726+
// them: the other readers share this value with no lock.
727+
if (singleReceiver) {
728+
transferredBuffers_.clear();
729+
transferredPorts_.clear();
730+
}
722731

723732
Local<Value> result;
724733
{

‎TestRunner/app/tests/MessagingTests.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,24 @@ describe("Messaging runtime edges", function () {
295295
});
296296
});
297297

298+
describe("markAsUncloneable", function () {
299+
it("rejects an object marked while the clone that reaches it is written", function (done) {
300+
var worker = new Worker("./messaging/uncloneableInGetterWorker.js");
301+
worker.onmessage = function (event) {
302+
expect(event.data).toEqual({ threw: true, name: "DataCloneError" });
303+
worker.terminate();
304+
done();
305+
};
306+
// fail() throws in this runner, which would skip done() when called
307+
// from an event handler.
308+
worker.onerror = function (error) {
309+
expect("worker error: " + error.message).toBeNull();
310+
worker.terminate();
311+
done();
312+
};
313+
});
314+
});
315+
298316
describe("worker error reporting", function () {
299317
// A worker boots on its own thread, so the first error arrives whenever
300318
// the runner gets to it; specs wait for it and only then settle for
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// A fresh isolate: the markAsUncloneable call in the getter below is the first
2+
// one this isolate has seen, and it happens while the clone that reaches the
3+
// marked object is already being written.
4+
var markAsUncloneable = require("node:worker_threads").markAsUncloneable;
5+
var graph = {
6+
get inner() {
7+
var marked = { a: 1 };
8+
markAsUncloneable(marked);
9+
return marked;
10+
},
11+
};
12+
var result;
13+
try {
14+
structuredClone(graph);
15+
result = { threw: false };
16+
} catch (e) {
17+
result = { threw: true, name: e && e.name };
18+
}
19+
postMessage(result);

0 commit comments

Comments
 (0)