Fix stream transfer over message ports

This commit is contained in:
Basilisk-Dev 2026-05-08 23:16:23 -04:00 committed by wuggy
commit a6b18bcd14
3 changed files with 255 additions and 32 deletions

View file

@ -152,11 +152,6 @@ struct SameThreadStreamTransferData
JS::PersistentRootedObject mRecord;
};
struct MessagePortStreamTransferData
{
MessagePortIdentifier mIdentifier;
};
bool
CallStreamTransferHelper(JSContext* aCx,
const char* aHelperName,
@ -235,6 +230,7 @@ TryWriteSameThreadStreamTransfer(JSContext* aCx,
bool
TryWriteReadableStreamPortTransfer(JSContext* aCx,
JS::Handle<JSObject*> aObj,
nsTArray<MessagePortIdentifier>& aPortIdentifiers,
uint32_t* aTag,
JS::TransferableOwnership* aOwnership,
void** aContent,
@ -265,13 +261,13 @@ TryWriteReadableStreamPortTransfer(JSContext* aCx,
return false;
}
MessagePortStreamTransferData* data = new MessagePortStreamTransferData();
port->CloneAndDisentangle(data->mIdentifier);
*aExtraData = aPortIdentifiers.Length();
MessagePortIdentifier* identifier = aPortIdentifiers.AppendElement();
port->CloneAndDisentangle(*identifier);
*aTag = SCTAG_DOM_TRANSFERRED_READABLESTREAM;
*aOwnership = JS::SCTAG_TMO_CUSTOM;
*aContent = data;
*aExtraData = 0;
*aContent = nullptr;
*aHandled = true;
return true;
}
@ -304,18 +300,19 @@ ReadSameThreadStreamTransfer(JSContext* aCx,
bool
ReadReadableStreamPortTransfer(JSContext* aCx,
nsISupports* aParent,
void* aContent,
nsTArray<MessagePortIdentifier>& aPortIdentifiers,
uint64_t aExtraData,
JS::MutableHandleObject aReturnObject)
{
MOZ_ASSERT(aContent);
MessagePortStreamTransferData* data =
static_cast<MessagePortStreamTransferData*>(aContent);
if (aExtraData >= aPortIdentifiers.Length()) {
return false;
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
ErrorResult rv;
RefPtr<MessagePort> port =
MessagePort::Create(global, data->mIdentifier, rv);
MessagePort::Create(global, aPortIdentifiers[aExtraData], rv);
if (NS_WARN_IF(rv.Failed())) {
rv.SuppressException();
return false;
@ -339,7 +336,6 @@ ReadReadableStreamPortTransfer(JSContext* aCx,
}
aReturnObject.set(&result.toObject());
delete data;
return true;
}
@ -1592,11 +1588,20 @@ StructuredCloneHolder::CustomReadTransferHandler(JSContext* aCx,
"__uxpReceiveReadableStreamTransfer",
aReturnObject);
}
if (aTag == SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM) {
return ReadSameThreadStreamTransfer(aCx, aContent,
"__uxpReceiveTransformStreamTransfer",
aReturnObject);
}
}
if (mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread &&
if ((mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread ||
mStructuredCloneScope == StructuredCloneScope::DifferentProcess) &&
aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM) {
return ReadReadableStreamPortTransfer(aCx, mParent, aContent,
MOZ_ASSERT(!aContent);
return ReadReadableStreamPortTransfer(aCx, mParent, mPortIdentifiers,
aExtraData,
aReturnObject);
}
@ -1691,12 +1696,25 @@ StructuredCloneHolder::CustomWriteTransferHandler(JSContext* aCx,
if (handled) {
return true;
}
if (!TryWriteSameThreadStreamTransfer(aCx, obj,
"__uxpTransferTransformStream",
SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM,
aTag, aOwnership, aContent,
aExtraData, &handled)) {
return false;
}
if (handled) {
return true;
}
}
if (mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread) {
if (mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread ||
mStructuredCloneScope == StructuredCloneScope::DifferentProcess) {
bool handled = false;
if (!TryWriteReadableStreamPortTransfer(aCx, obj, aTag, aOwnership,
aContent, aExtraData, &handled)) {
if (!TryWriteReadableStreamPortTransfer(aCx, obj, mPortIdentifiers,
aTag, aOwnership, aContent,
aExtraData, &handled)) {
return false;
}
if (handled) {
@ -1743,7 +1761,8 @@ StructuredCloneHolder::CustomFreeTransferHandler(uint32_t aTag,
}
if ((aTag == SCTAG_DOM_TRANSFERRED_WRITABLESTREAM ||
aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM) &&
aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM ||
aTag == SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM) &&
mStructuredCloneScope == StructuredCloneScope::SameProcessSameThread) {
MOZ_ASSERT(aContent);
SameThreadStreamTransferData* data =
@ -1753,12 +1772,11 @@ StructuredCloneHolder::CustomFreeTransferHandler(uint32_t aTag,
}
if (aTag == SCTAG_DOM_TRANSFERRED_READABLESTREAM &&
mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread) {
MOZ_ASSERT(aContent);
MessagePortStreamTransferData* data =
static_cast<MessagePortStreamTransferData*>(aContent);
MessagePort::ForceClose(data->mIdentifier);
delete data;
(mStructuredCloneScope == StructuredCloneScope::SameProcessDifferentThread ||
mStructuredCloneScope == StructuredCloneScope::DifferentProcess)) {
MOZ_ASSERT(!aContent);
MOZ_ASSERT(aExtraData < mPortIdentifiers.Length());
MessagePort::ForceClose(mPortIdentifiers[aExtraData]);
return;
}
}

View file

@ -71,6 +71,7 @@ enum StructuredCloneTags {
// Same-thread transferable stream records. These are not supported by IDB.
SCTAG_DOM_TRANSFERRED_READABLESTREAM,
SCTAG_DOM_TRANSFERRED_WRITABLESTREAM,
SCTAG_DOM_TRANSFERRED_TRANSFORMSTREAM,
// When adding a new tag for IDB, please don't add it to the end of the list!
// Tags that are supported by IDB must not ever change. See the static assert

View file

@ -6735,6 +6735,163 @@ js::InitStreamExtras(JSContext* cx, HandleObject global)
return makeDOMException("The object could not be cloned.", "DataCloneError");
}
function cloneTransferReasonError(value) {
var constructors = [
global.EvalError,
global.RangeError,
global.ReferenceError,
global.SyntaxError,
global.TypeError,
global.URIError,
global.Error
];
for (var i = 0; i < constructors.length; ++i) {
var C = constructors[i];
if (typeof C !== "function")
continue;
try {
if (!(value instanceof C))
continue;
} catch (e) {
continue;
}
var message = "";
var descriptor;
try {
descriptor = Object.getOwnPropertyDescriptor(value, "message");
} catch (e) {
descriptor = undefined;
}
if (descriptor && hasOwn.call(descriptor, "value"))
message = String(descriptor.value);
return new C(message);
}
return undefined;
}
function cloneTransferReasonValue(value, memory) {
if (typeof value === "function" || typeof value === "symbol")
throw makeDataCloneError();
if (!isObject(value))
return value;
if (isUncloneableStreamChunk(value))
throw makeDataCloneError();
memory = memory || new WeakMap();
if (memory.has(value))
return memory.get(value);
if (typeof global.DOMException === "function") {
try {
if (value instanceof global.DOMException) {
return new global.DOMException(String(value.message),
String(value.name));
}
} catch (e) {}
}
var errorClone = cloneTransferReasonError(value);
if (errorClone !== undefined)
return errorClone;
if (typeof global.ArrayBuffer === "function") {
try {
if (value instanceof global.ArrayBuffer) {
var bufferClone = value.slice(0);
memory.set(value, bufferClone);
return bufferClone;
}
} catch (e) {}
}
if (global.ArrayBuffer && global.ArrayBuffer.isView &&
global.ArrayBuffer.isView(value)) {
var clonedBuffer = cloneTransferReasonValue(value.buffer, memory);
var viewClone;
if (objectToString.call(value) === "[object DataView]") {
viewClone = new global.DataView(clonedBuffer, value.byteOffset,
value.byteLength);
} else {
viewClone = new value.constructor(clonedBuffer, value.byteOffset,
value.length);
}
memory.set(value, viewClone);
return viewClone;
}
if (typeof global.Date === "function") {
try {
if (value instanceof global.Date) {
var dateClone = new global.Date(value.getTime());
memory.set(value, dateClone);
return dateClone;
}
} catch (e) {}
}
if (typeof global.RegExp === "function") {
try {
if (value instanceof global.RegExp) {
var flags = "";
flags += value.global ? "g" : "";
flags += value.ignoreCase ? "i" : "";
flags += value.multiline ? "m" : "";
flags += value.unicode ? "u" : "";
flags += value.sticky ? "y" : "";
var regexpClone = new global.RegExp(value.source, flags);
regexpClone.lastIndex = value.lastIndex;
memory.set(value, regexpClone);
return regexpClone;
}
} catch (e) {}
}
if (typeof global.Map === "function") {
try {
if (value instanceof global.Map) {
var mapClone = new global.Map();
memory.set(value, mapClone);
value.forEach(function(v, k) {
mapClone.set(cloneTransferReasonValue(k, memory),
cloneTransferReasonValue(v, memory));
});
return mapClone;
}
} catch (e) {
throw e;
}
}
if (typeof global.Set === "function") {
try {
if (value instanceof global.Set) {
var setClone = new global.Set();
memory.set(value, setClone);
value.forEach(function(v) {
setClone.add(cloneTransferReasonValue(v, memory));
});
return setClone;
}
} catch (e) {
throw e;
}
}
var clone = Array.isArray(value) ? [] : {};
memory.set(value, clone);
var keys = Object.keys(value);
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
clone[key] = cloneTransferReasonValue(value[key], memory);
}
return clone;
}
function abortTransferredWritable(record, reason) {
try {
return record.writer.abort(reason);
@ -6908,12 +7065,16 @@ js::InitStreamExtras(JSContext* cx, HandleObject global)
function cancelReadableTransfer(record, reason) {
record.canceled = true;
Promise.resolve().then(function() {
var cancelPromise = Promise.resolve().then(function() {
var clonedReason = cloneTransferReasonValue(reason);
try {
silenceRejection(record.reader.cancel(reason));
} catch (e) {}
return record.reader.cancel(clonedReason);
} catch (e) {
return Promise.reject(e);
}
});
return undefined;
silenceRejection(cancelPromise);
return cancelPromise;
}
function closeReadableTransferPort(port) {
@ -7139,13 +7300,50 @@ js::InitStreamExtras(JSContext* cx, HandleObject global)
},
cancel: function(reason) {
state.canceled = true;
tryPostReadableTransferPort(port, { type: "cancel", reason: reason });
try {
port.postMessage({ type: "cancel", reason: reason });
} catch (e) {
closeReadableTransferPort(port);
return Promise.reject(e);
}
closeReadableTransferPort(port);
return undefined;
}
});
}
function __uxpTransferTransformStream(stream) {
if (!isObject(stream) || !transformState.has(stream))
return undefined;
var state = transformState.get(stream);
if (state.readable.locked || state.writable.locked)
return null;
var readableRecord = __uxpTransferReadableStream(state.readable);
if (!readableRecord)
return null;
var writableRecord = __uxpTransferWritableStream(state.writable);
if (!writableRecord)
return null;
return {
readable: readableRecord,
writable: writableRecord
};
}
function __uxpReceiveTransformStreamTransfer(record) {
var stream = Object.create(global.TransformStream.prototype);
transformState.set(stream, {
readable: __uxpReceiveReadableStreamTransfer(record.readable),
writable: __uxpReceiveWritableStreamTransfer(record.writable),
controller: undefined
});
return stream;
}
function __uxpReceiveReadableStreamTransfer(record) {
return new ReadableStream({
start: function(controller) {
@ -7323,6 +7521,12 @@ js::InitStreamExtras(JSContext* cx, HandleObject global)
Object.defineProperty(global, "__uxpReceiveReadableStreamTransferFromPort",
{ value: __uxpReceiveReadableStreamTransferFromPort,
configurable: true });
Object.defineProperty(global, "__uxpTransferTransformStream",
{ value: __uxpTransferTransformStream,
configurable: true });
Object.defineProperty(global, "__uxpReceiveTransformStreamTransfer",
{ value: __uxpReceiveTransformStreamTransfer,
configurable: true });
if (global.ReadableStream && !global.ReadableStream.from) {
Object.defineProperty(global.ReadableStream, "from", {