diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 1265dedf7cda..64a1e95e2038 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -2373,12 +2373,31 @@ const JSPropertySpec CloneBufferObject::props_[] = { JS_PS_END }; +static mozilla::Maybe +ParseCloneScope(JSContext* cx, HandleString str) +{ + mozilla::Maybe scope; + + JSAutoByteString scopeStr(cx, str); + if (!scopeStr) + return scope; + + if (strcmp(scopeStr.ptr(), "SameProcessSameThread") == 0) + scope.emplace(JS::StructuredCloneScope::SameProcessSameThread); + else if (strcmp(scopeStr.ptr(), "SameProcessDifferentThread") == 0) + scope.emplace(JS::StructuredCloneScope::SameProcessDifferentThread); + else if (strcmp(scopeStr.ptr(), "DifferentProcess") == 0) + scope.emplace(JS::StructuredCloneScope::DifferentProcess); + + return scope; +} + static bool Serialize(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - JSAutoStructuredCloneBuffer clonebuf(JS::StructuredCloneScope::SameProcessSameThread, nullptr, nullptr); + mozilla::Maybe clonebuf; JS::CloneDataPolicy policy; if (!args.get(2).isUndefined()) { @@ -2407,12 +2426,30 @@ Serialize(JSContext* cx, unsigned argc, Value* vp) return false; } } + + if (!JS_GetProperty(cx, opts, "scope", &v)) + return false; + + if (!v.isUndefined()) { + RootedString str(cx, JS::ToString(cx, v)); + if (!str) + return false; + auto scope = ParseCloneScope(cx, str); + if (!scope) { + JS_ReportErrorASCII(cx, "Invalid structured clone scope"); + return false; + } + clonebuf.emplace(*scope, nullptr, nullptr); + } } - if (!clonebuf.write(cx, args.get(0), args.get(1), policy)) + if (!clonebuf) + clonebuf.emplace(JS::StructuredCloneScope::SameProcessSameThread, nullptr, nullptr); + + if (!clonebuf->write(cx, args.get(0), args.get(1), policy)) return false; - RootedObject obj(cx, CloneBufferObject::Create(cx, &clonebuf)); + RootedObject obj(cx, CloneBufferObject::Create(cx, clonebuf.ptr())); if (!obj) return false; @@ -2425,14 +2462,33 @@ Deserialize(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - if (args.length() != 1 || !args[0].isObject()) { - JS_ReportErrorASCII(cx, "deserialize requires a single clonebuffer argument"); + if (!args.get(0).isObject() || !args[0].toObject().is()) { + JS_ReportErrorASCII(cx, "deserialize requires a clonebuffer argument"); return false; } - if (!args[0].toObject().is()) { - JS_ReportErrorASCII(cx, "deserialize requires a clonebuffer"); - return false; + JS::StructuredCloneScope scope = JS::StructuredCloneScope::SameProcessSameThread; + if (args.get(1).isObject()) { + RootedObject opts(cx, &args[1].toObject()); + if (!opts) + return false; + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "scope", &v)) + return false; + + if (!v.isUndefined()) { + RootedString str(cx, JS::ToString(cx, v)); + if (!str) + return false; + auto maybeScope = ParseCloneScope(cx, str); + if (!maybeScope) { + JS_ReportErrorASCII(cx, "Invalid structured clone scope"); + return false; + } + + scope = *maybeScope; + } } Rooted obj(cx, &args[0].toObject().as()); @@ -2451,8 +2507,9 @@ Deserialize(JSContext* cx, unsigned argc, Value* vp) RootedValue deserialized(cx); if (!JS_ReadStructuredClone(cx, *obj->data(), JS_STRUCTURED_CLONE_VERSION, - JS::StructuredCloneScope::SameProcessSameThread, - &deserialized, nullptr, nullptr)) { + scope, + &deserialized, nullptr, nullptr)) + { return false; } args.rval().set(deserialized); @@ -4495,15 +4552,22 @@ gc::ZealModeHelpText), JS_FN_HELP("serialize", Serialize, 1, 0, "serialize(data, [transferables, [policy]])", " Serialize 'data' using JS_WriteStructuredClone. Returns a structured\n" -" clone buffer object. 'policy' must be an object. The following keys'\n" -" string values will be used to determine whether the corresponding types\n" -" may be serialized (value 'allow', the default) or not (value 'deny').\n" -" If denied types are encountered a TypeError will be thrown during cloning.\n" -" Valid keys: 'SharedArrayBuffer'."), +" clone buffer object. 'policy' may be an options hash. Valid keys:\n" +" 'SharedArrayBuffer' - either 'allow' (the default) or 'deny'\n" +" to specify whether SharedArrayBuffers may be serialized.\n" +"\n" +" 'scope' - SameProcessSameThread, SameProcessDifferentThread, or\n" +" DifferentProcess. Determines how some values will be serialized.\n" +" Clone buffers may only be deserialized with a compatible scope."), JS_FN_HELP("deserialize", Deserialize, 1, 0, -"deserialize(clonebuffer)", -" Deserialize data generated by serialize."), +"deserialize(clonebuffer[, opts])", +" Deserialize data generated by serialize. 'opts' is an options hash with one\n" +" recognized key 'scope', which limits the clone buffers that are considered\n" +" valid. Allowed values: 'SameProcessSameThread', 'SameProcessDifferentThread',\n" +" and 'DifferentProcess'. So for example, a DifferentProcess clone buffer\n" +" may be deserialized in any scope, but a SameProcessSameThread clone buffer\n" +" cannot be deserialized in a DifferentProcess scope."), JS_FN_HELP("detachArrayBuffer", DetachArrayBuffer, 1, 0, "detachArrayBuffer(buffer)", diff --git a/js/src/tests/js1_8_5/extensions/clone-errors.js b/js/src/tests/js1_8_5/extensions/clone-errors.js index c1a6d0a8dcdf..f65578a06b9a 100644 --- a/js/src/tests/js1_8_5/extensions/clone-errors.js +++ b/js/src/tests/js1_8_5/extensions/clone-errors.js @@ -22,4 +22,20 @@ check(new Proxy({}, {})); // A failing getter. check({get x() { throw new Error("fail"); }}); +// Mismatched scopes. +for (let [write_scope, read_scope] of [['SameProcessSameThread', 'SameProcessDifferentThread'], + ['SameProcessSameThread', 'DifferentProcess'], + ['SameProcessDifferentThread', 'DifferentProcess']]) +{ + var ab = new ArrayBuffer(12); + var buffer = serialize(ab, [ab], { scope: write_scope }); + var caught = false; + try { + deserialize(buffer, { scope: read_scope }); + } catch (exc) { + caught = true; + } + assertEq(caught, true, `${write_scope} clone buffer should not be deserializable as ${read_scope}`); +} + reportCompare(0, 0, "ok"); diff --git a/js/src/tests/js1_8_5/extensions/clone-transferables.js b/js/src/tests/js1_8_5/extensions/clone-transferables.js index 56ebf1520470..673684b9543e 100644 --- a/js/src/tests/js1_8_5/extensions/clone-transferables.js +++ b/js/src/tests/js1_8_5/extensions/clone-transferables.js @@ -2,13 +2,22 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/licenses/publicdomain/ -function test() { - for (var size of [0, 8, 16, 200, 1000, 4096, -8, -200, -8192, -65536]) { - size = Math.abs(size); +function* buffer_options() { + for (var scope of ["SameProcessSameThread", "SameProcessDifferentThread", "DifferentProcess"]) { + for (var size of [0, 8, 16, 200, 1000, 4096, 8192, 65536]) { + yield { scope, size }; + } + } +} + +function test() { + for (var {scope, size} of buffer_options()) { var old = new ArrayBuffer(size); - var copy = deserialize(serialize(old, [old])); + var copy = deserialize(serialize([old, old], [old], { scope }), { scope }); assertEq(old.byteLength, 0); + assertEq(copy[0] === copy[1], true); + copy = copy[0]; assertEq(copy.byteLength, size); var constructors = [ Int8Array, @@ -32,7 +41,7 @@ function test() { if (!dataview) assertEq(old_arr.length, size / old_arr.BYTES_PER_ELEMENT); - var copy_arr = deserialize(serialize(old_arr, [ buf ])); + var copy_arr = deserialize(serialize(old_arr, [ buf ], { scope }), { scope }); assertEq(buf.byteLength, 0, "donor array buffer should be detached"); if (!dataview) { @@ -54,7 +63,7 @@ function test() { var buf = new ArrayBuffer(size); var old_arr = new ctor(buf); var dv = new DataView(buf); // Second view - var copy_arr = deserialize(serialize(old_arr, [ buf ])); + var copy_arr = deserialize(serialize(old_arr, [ buf ], { scope }), { scope }); assertEq(buf.byteLength, 0, "donor array buffer should be detached"); assertEq(old_arr.byteLength, 0, @@ -78,7 +87,7 @@ function test() { var view = new Int32Array(old); view[0] = 1; var mutator = { get foo() { view[0] = 2; } }; - var copy = deserialize(serialize([ old, mutator ], [old])); + var copy = deserialize(serialize([ old, mutator ], [ old ], { scope }), { scope }); var viewCopy = new Int32Array(copy[0]); assertEq(view.length, 0); // Underlying buffer now detached. assertEq(viewCopy[0], 2); @@ -90,7 +99,7 @@ function test() { old = new ArrayBuffer(size); var mutator = { get foo() { - deserialize(serialize(old, [old])); + deserialize(serialize(old, [old], { scope }), { scope }); } }; // The throw is not yet implemented, bug 919259. diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp index 437cf572a4e1..07d63133ef90 100644 --- a/js/src/vm/StructuredClone.cpp +++ b/js/src/vm/StructuredClone.cpp @@ -125,6 +125,7 @@ enum StructuredDataType : uint32_t { SCTAG_TRANSFER_MAP_HEADER = 0xFFFF0200, SCTAG_TRANSFER_MAP_PENDING_ENTRY, SCTAG_TRANSFER_MAP_ARRAY_BUFFER, + SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER, SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES, SCTAG_END_OF_BUILTIN_TYPES @@ -166,6 +167,19 @@ struct BufferIterator { JS_STATIC_ASSERT(8 % sizeof(T) == 0); } + BufferIterator(const BufferIterator& other) + : mBuffer(other.mBuffer) + , mIter(other.mIter) + { + } + + BufferIterator& operator=(const BufferIterator& other) + { + MOZ_ASSERT(&mBuffer == &other.mBuffer); + mIter = other.mIter; + return *this; + } + BufferIterator operator++(int) { BufferIterator ret = *this; if (!mIter.AdvanceAcrossSegments(mBuffer, sizeof(T))) { @@ -181,6 +195,11 @@ struct BufferIterator { return *this; } + size_t operator-(const BufferIterator& other) { + MOZ_ASSERT(&mBuffer == &other.mBuffer); + return mBuffer.RangeLength(other.mIter, mIter); + } + void next() { if (!mIter.AdvanceAcrossSegments(mBuffer, sizeof(T))) { MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete"); @@ -211,6 +230,8 @@ struct BufferIterator { struct SCOutput { public: + using Iter = BufferIterator; + explicit SCOutput(JSContext* cx); JSContext* context() const { return cx; } @@ -229,11 +250,16 @@ struct SCOutput { bool extractBuffer(JSStructuredCloneData* data); void discardTransferables(const JSStructuredCloneCallbacks* cb, void* cbClosure); + uint64_t tell() const { return buf.Size(); } uint64_t count() const { return buf.Size() / sizeof(uint64_t); } - BufferIterator iter() { + Iter iter() { return BufferIterator(buf); } + size_t offset(Iter dest) { + return dest - iter(); + } + private: JSContext* cx; mozilla::BufferList buf; @@ -262,7 +288,9 @@ class SCInput { bool get(uint64_t* p); bool getPair(uint32_t* tagp, uint32_t* datap); - BufferIterator tell() const { return point; } + const BufferIterator& tell() const { return point; } + void seekTo(const BufferIterator& pos) { point = pos; } + void seekBy(size_t pos) { point += pos; } template bool readArray(T* p, size_t nelems); @@ -290,7 +318,7 @@ struct JSStructuredCloneReader { explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope, const JSStructuredCloneCallbacks* cb, void* cbClosure) - : in(in), scope(scope), objs(in.context()), allObjs(in.context()), + : in(in), allowedScope(scope), objs(in.context()), allObjs(in.context()), callbacks(cb), closure(cbClosure) { } SCInput& input() { return in; } @@ -318,7 +346,18 @@ struct JSStructuredCloneReader { SCInput& in; - JS::StructuredCloneScope scope; + // The widest scope that the caller will accept, where + // SameProcessSameThread is the widest (it can store anything it wants) and + // DifferentProcess is the narrowest (it cannot contain pointers and must + // be valid cross-process.) + JS::StructuredCloneScope allowedScope; + + // The scope the buffer was generated for (what sort of buffer it is.) The + // scope is not just a permissions thing; it also affects the storage + // format (eg a Transferred ArrayBuffer can be stored as a pointer for + // SameProcessSameThread but must have its contents in the clone buffer for + // DifferentProcess.) + JS::StructuredCloneScope storedScope; // Stack of objects with properties remaining to be read. AutoValueVector objs; @@ -1438,7 +1477,6 @@ JSStructuredCloneWriter::writeTransferMap() RootedObject obj(context()); for (auto tr = transferableObjects.all(); !tr.empty(); tr.popFront()) { obj = tr.front(); - if (!memory.put(obj, memory.count())) { ReportOutOfMemory(context()); return false; @@ -1474,7 +1512,8 @@ JSStructuredCloneWriter::transferOwnership() MOZ_ASSERT(NativeEndian::swapFromLittleEndian(point.peek()) == transferableObjects.count()); point++; - RootedObject obj(context()); + JSContext* cx = context(); + RootedObject obj(cx); for (auto tr = transferableObjects.all(); !tr.empty(); tr.popFront()) { obj = tr.front(); @@ -1490,41 +1529,63 @@ JSStructuredCloneWriter::transferOwnership() #endif ESClass cls; - if (!GetBuiltinClass(context(), obj, &cls)) + if (!GetBuiltinClass(cx, obj, &cls)) return false; if (cls == ESClass::ArrayBuffer) { + tag = SCTAG_TRANSFER_MAP_ARRAY_BUFFER; + // The current setup of the array buffer inheritance hierarchy doesn't // lend itself well to generic manipulation via proxies. - Rooted arrayBuffer(context(), &CheckedUnwrap(obj)->as()); - JSAutoCompartment ac(context(), arrayBuffer); + Rooted arrayBuffer(cx, &CheckedUnwrap(obj)->as()); + JSAutoCompartment ac(cx, arrayBuffer); size_t nbytes = arrayBuffer->byteLength(); if (arrayBuffer->isWasm() || arrayBuffer->isPreparedForAsmJS()) { - JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, - JSMSG_WASM_NO_TRANSFER); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WASM_NO_TRANSFER); return false; } - bool hasStealableContents = arrayBuffer->hasStealableContents() && - (scope != JS::StructuredCloneScope::DifferentProcess); + if (scope == JS::StructuredCloneScope::DifferentProcess) { + // Write Transferred ArrayBuffers in DifferentProcess scope at + // the end of the clone buffer, and store the offset within the + // buffer to where the ArrayBuffer was written. Note that this + // will invalidate the current position iterator. - ArrayBufferObject::BufferContents bufContents = - ArrayBufferObject::stealContents(context(), arrayBuffer, hasStealableContents); - if (!bufContents) - return false; // already transferred data + size_t pointOffset = out.offset(point); + tag = SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER; + ownership = JS::SCTAG_TMO_UNOWNED; + content = nullptr; + extraData = out.tell() - pointOffset; // Offset from tag to current end of buffer + if (!writeArrayBuffer(arrayBuffer)) + return false; - content = bufContents.data(); - tag = SCTAG_TRANSFER_MAP_ARRAY_BUFFER; - if (bufContents.kind() == ArrayBufferObject::MAPPED) - ownership = JS::SCTAG_TMO_MAPPED_DATA; - else - ownership = JS::SCTAG_TMO_ALLOC_DATA; - extraData = nbytes; + // Must refresh the point iterator after its collection has + // been modified. + point = out.iter(); + point += pointOffset; + + if (!JS_DetachArrayBuffer(cx, arrayBuffer)) + return false; + } else { + bool hasStealableContents = arrayBuffer->hasStealableContents(); + + ArrayBufferObject::BufferContents bufContents = + ArrayBufferObject::stealContents(cx, arrayBuffer, hasStealableContents); + if (!bufContents) + return false; // already transferred data + + content = bufContents.data(); + if (bufContents.kind() == ArrayBufferObject::MAPPED) + ownership = JS::SCTAG_TMO_MAPPED_DATA; + else + ownership = JS::SCTAG_TMO_ALLOC_DATA; + extraData = nbytes; + } } else { if (!callbacks || !callbacks->writeTransfer) return reportDataCloneError(JS_SCERR_TRANSFERABLE); - if (!callbacks->writeTransfer(context(), obj, closure, &tag, &ownership, &content, &extraData)) + if (!callbacks->writeTransfer(cx, obj, closure, &tag, &ownership, &content, &extraData)) return false; MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY); } @@ -2100,7 +2161,8 @@ JSStructuredCloneReader::readHeader() } MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); - if (data < uint32_t(scope)) { + storedScope = JS::StructuredCloneScope(data); + if (storedScope < allowedScope) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "incompatible structured clone scope"); return false; @@ -2145,6 +2207,12 @@ JSStructuredCloneReader::readTransferMap() return false; if (tag == SCTAG_TRANSFER_MAP_ARRAY_BUFFER) { + if (storedScope == JS::StructuredCloneScope::DifferentProcess) { + // Transferred ArrayBuffers in a DifferentProcess clone buffer + // are treated as if they weren't Transferred at all. + continue; + } + size_t nbytes = extraData; MOZ_ASSERT(data == JS::SCTAG_TMO_ALLOC_DATA || data == JS::SCTAG_TMO_MAPPED_DATA); @@ -2152,6 +2220,22 @@ JSStructuredCloneReader::readTransferMap() obj = JS_NewArrayBufferWithContents(cx, nbytes, content); else if (data == JS::SCTAG_TMO_MAPPED_DATA) obj = JS_NewMappedArrayBufferWithContents(cx, nbytes, content); + } else if (tag == SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER) { + auto savedPos = in.tell(); + auto guard = mozilla::MakeScopeExit([&] { + in.seekTo(savedPos); + }); + in.seekTo(pos); + in.seekBy(static_cast(extraData)); + + uint32_t tag, data; + if (!in.readPair(&tag, &data)) + return false; + MOZ_ASSERT(tag == SCTAG_ARRAY_BUFFER_OBJECT); + RootedValue val(cx); + if (!readArrayBuffer(data, &val)) + return false; + obj = &val.toObject(); } else { if (!callbacks || !callbacks->readTransfer) { ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE); diff --git a/mfbt/BufferList.h b/mfbt/BufferList.h index 1c4bc8fc79a9..42aea12dbc05 100644 --- a/mfbt/BufferList.h +++ b/mfbt/BufferList.h @@ -224,6 +224,33 @@ class BufferList : private AllocPolicy { return mData == mDataEnd; } + + private: + + // Count the bytes we would need to advance in order to reach aTarget. + size_t BytesUntil(const BufferList& aBuffers, const IterImpl& aTarget) const { + size_t offset = 0; + + MOZ_ASSERT(aTarget.IsIn(aBuffers)); + + char* data = mData; + for (uintptr_t segment = mSegment; segment < aTarget.mSegment; segment++) { + offset += aBuffers.mSegments[segment].End() - data; + data = aBuffers.mSegments[segment].mData; + } + + MOZ_RELEASE_ASSERT(IsIn(aBuffers)); + MOZ_RELEASE_ASSERT(aTarget.mData >= data); + + offset += aTarget.mData - data; + return offset; + } + + bool IsIn(const BufferList& aBuffers) const { + return mSegment < aBuffers.mSegments.length() && + mData >= aBuffers.mSegments[mSegment].mData && + mData < aBuffers.mSegments[mSegment].End(); + } }; // Special convenience method that returns Iter().Data(). @@ -270,6 +297,13 @@ class BufferList : private AllocPolicy // This method requires aIter and aSize to be 8-byte aligned. BufferList Extract(IterImpl& aIter, size_t aSize, bool* aSuccess); + // Return the number of bytes from 'start' to 'end', two iterators within + // this BufferList. + size_t RangeLength(const IterImpl& start, const IterImpl& end) const { + MOZ_ASSERT(start.IsIn(*this) && end.IsIn(*this)); + return start.BytesUntil(*this, end); + } + private: explicit BufferList(AllocPolicy aAP) : AllocPolicy(aAP),