/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * This file implements the structured data algorithms of * https://html.spec.whatwg.org/multipage/structured-data.html * * The spec is in two parts: * * - StructuredSerialize examines a JS value and produces a graph of Records. * - StructuredDeserialize walks the Records and produces a new JS value. * * The differences between our implementation and the spec are minor: * * - We call the two phases "write" and "read". * - Our algorithms use an explicit work stack, rather than recursion. * - Serialized data is a flat array of bytes, not a (possibly cyclic) graph * of "Records". * - As a consequence, we handle non-treelike object graphs differently. * We serialize objects that appear in multiple places in the input as * backreferences, using sequential integer indexes. * See `JSStructuredCloneReader::allObjs`, our take on the "memory" map * in the spec's StructuredDeserialize. */ #include "js/StructuredClone.h" #include "mozilla/Casting.h" #include "mozilla/CheckedInt.h" #include "mozilla/EndianUtils.h" #include "mozilla/FloatingPoint.h" #include "mozilla/Maybe.h" #include "mozilla/ScopeExit.h" #include #include #include #include "jsdate.h" #include "builtin/DataViewObject.h" #include "builtin/MapObject.h" #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject #include "js/ArrayBuffer.h" // JS::{ArrayBufferHasData,DetachArrayBuffer,IsArrayBufferObject,New{,Mapped}ArrayBufferWithContents,ReleaseMappedArrayBufferContents} #include "js/Date.h" #include "js/experimental/TypedData.h" // JS_NewDataView, JS_New{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped,Big{Ui,I}nt64}ArrayWithBuffer #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/GCHashTable.h" #include "js/Object.h" // JS::GetBuiltinClass #include "js/PropertyAndElement.h" // JS_GetElement #include "js/RegExpFlags.h" // JS::RegExpFlag, JS::RegExpFlags #include "js/ScalarType.h" // js::Scalar::Type #include "js/SharedArrayBuffer.h" // JS::IsSharedArrayBufferObject #include "js/Wrapper.h" #include "util/DifferentialTesting.h" #include "vm/BigIntType.h" #include "vm/ErrorObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/RegExpObject.h" #include "vm/SavedFrame.h" #include "vm/SharedArrayObject.h" #include "vm/TypedArrayObject.h" #include "wasm/WasmJS.h" #include "vm/ArrayObject-inl.h" #include "vm/Compartment-inl.h" #include "vm/ErrorObject-inl.h" #include "vm/InlineCharBuffer-inl.h" #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" #include "vm/ObjectOperations-inl.h" #include "vm/Realm-inl.h" using namespace js; using JS::CanonicalizeNaN; using JS::GetBuiltinClass; using JS::RegExpFlag; using JS::RegExpFlags; using JS::RootedValueVector; using mozilla::AssertedCast; using mozilla::BitwiseCast; using mozilla::Maybe; using mozilla::NativeEndian; using mozilla::NumbersAreIdentical; // When you make updates here, make sure you consider whether you need to bump // the value of JS_STRUCTURED_CLONE_VERSION in js/public/StructuredClone.h. You // will likely need to increment the version if anything at all changes in the // serialization format. // // Note that SCTAG_END_OF_KEYS is written into the serialized form and should // have a stable ID, it need not be at the end of the list and should not be // used for sizing data structures. enum StructuredDataType : uint32_t { // Structured data types provided by the engine SCTAG_FLOAT_MAX = 0xFFF00000, SCTAG_HEADER = 0xFFF10000, SCTAG_NULL = 0xFFFF0000, SCTAG_UNDEFINED, SCTAG_BOOLEAN, SCTAG_INT32, SCTAG_STRING, SCTAG_DATE_OBJECT, SCTAG_REGEXP_OBJECT, SCTAG_ARRAY_OBJECT, SCTAG_OBJECT_OBJECT, SCTAG_ARRAY_BUFFER_OBJECT_V2, // Old version, for backwards compatibility. SCTAG_BOOLEAN_OBJECT, SCTAG_STRING_OBJECT, SCTAG_NUMBER_OBJECT, SCTAG_BACK_REFERENCE_OBJECT, SCTAG_DO_NOT_USE_1, // Required for backwards compatibility SCTAG_DO_NOT_USE_2, // Required for backwards compatibility SCTAG_TYPED_ARRAY_OBJECT_V2, // Old version, for backwards compatibility. SCTAG_MAP_OBJECT, SCTAG_SET_OBJECT, SCTAG_END_OF_KEYS, SCTAG_DO_NOT_USE_3, // Required for backwards compatibility SCTAG_DATA_VIEW_OBJECT_V2, // Old version, for backwards compatibility. SCTAG_SAVED_FRAME_OBJECT, // No new tags before principals. SCTAG_JSPRINCIPALS, SCTAG_NULL_JSPRINCIPALS, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM, SCTAG_SHARED_ARRAY_BUFFER_OBJECT, SCTAG_SHARED_WASM_MEMORY_OBJECT, SCTAG_BIGINT, SCTAG_BIGINT_OBJECT, SCTAG_ARRAY_BUFFER_OBJECT, SCTAG_TYPED_ARRAY_OBJECT, SCTAG_DATA_VIEW_OBJECT, SCTAG_ERROR_OBJECT, SCTAG_TYPED_ARRAY_V1_MIN = 0xFFFF0100, SCTAG_TYPED_ARRAY_V1_INT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int8, SCTAG_TYPED_ARRAY_V1_UINT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8, SCTAG_TYPED_ARRAY_V1_INT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int16, SCTAG_TYPED_ARRAY_V1_UINT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint16, SCTAG_TYPED_ARRAY_V1_INT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int32, SCTAG_TYPED_ARRAY_V1_UINT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint32, SCTAG_TYPED_ARRAY_V1_FLOAT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float32, SCTAG_TYPED_ARRAY_V1_FLOAT64 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float64, SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8Clamped, // BigInt64 and BigUint64 are not supported in the v1 format. SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED, // Define a separate range of numbers for Transferable-only tags, since // they are not used for persistent clone buffers and therefore do not // require bumping JS_STRUCTURED_CLONE_VERSION. 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 }; /* * Format of transfer map: * * numTransferables (64 bits) * array of: * * pointer (64 bits) * extraData (64 bits), eg byte length for ArrayBuffers */ // Data associated with an SCTAG_TRANSFER_MAP_HEADER that tells whether the // contents have been read out yet or not. enum TransferableMapHeader { SCTAG_TM_UNREAD = 0, SCTAG_TM_TRANSFERRED }; static inline uint64_t PairToUInt64(uint32_t tag, uint32_t data) { return uint64_t(data) | (uint64_t(tag) << 32); } namespace js { template struct BufferIterator { using BufferList = mozilla::BufferList; explicit BufferIterator(const BufferList& buffer) : mBuffer(buffer), mIter(buffer.Iter()) { static_assert(8 % sizeof(T) == 0); } explicit BufferIterator(const JSStructuredCloneData& data) : mBuffer(data.bufList_), mIter(data.Start()) {} BufferIterator& operator=(const BufferIterator& other) { MOZ_ASSERT(&mBuffer == &other.mBuffer); mIter = other.mIter; return *this; } [[nodiscard]] bool advance(size_t size = sizeof(T)) { return mIter.AdvanceAcrossSegments(mBuffer, size); } BufferIterator operator++(int) { BufferIterator ret = *this; if (!advance(sizeof(T))) { MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete"); } return ret; } BufferIterator& operator+=(size_t size) { if (!advance(size)) { MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete"); } return *this; } size_t operator-(const BufferIterator& other) const { MOZ_ASSERT(&mBuffer == &other.mBuffer); return mBuffer.RangeLength(other.mIter, mIter); } bool operator==(const BufferIterator& other) const { return mBuffer.Start() == other.mBuffer.Start() && mIter == other.mIter; } bool operator!=(const BufferIterator& other) const { return !(*this == other); } bool done() const { return mIter.Done(); } [[nodiscard]] bool readBytes(char* outData, size_t size) { return mBuffer.ReadBytes(mIter, outData, size); } void write(const T& data) { MOZ_ASSERT(mIter.HasRoomFor(sizeof(T))); *reinterpret_cast(mIter.Data()) = data; } T peek() const { MOZ_ASSERT(mIter.HasRoomFor(sizeof(T))); return *reinterpret_cast(mIter.Data()); } bool canPeek() const { return mIter.HasRoomFor(sizeof(T)); } const BufferList& mBuffer; typename BufferList::IterImpl mIter; }; SharedArrayRawBufferRefs& SharedArrayRawBufferRefs::operator=( SharedArrayRawBufferRefs&& other) { takeOwnership(std::move(other)); return *this; } SharedArrayRawBufferRefs::~SharedArrayRawBufferRefs() { releaseAll(); } bool SharedArrayRawBufferRefs::acquire(JSContext* cx, SharedArrayRawBuffer* rawbuf) { if (!refs_.append(rawbuf)) { ReportOutOfMemory(cx); return false; } if (!rawbuf->addReference()) { refs_.popBack(); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_SAB_REFCNT_OFLO); return false; } return true; } bool SharedArrayRawBufferRefs::acquireAll( JSContext* cx, const SharedArrayRawBufferRefs& that) { if (!refs_.reserve(refs_.length() + that.refs_.length())) { ReportOutOfMemory(cx); return false; } for (auto ref : that.refs_) { if (!ref->addReference()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_SAB_REFCNT_OFLO); return false; } MOZ_ALWAYS_TRUE(refs_.append(ref)); } return true; } void SharedArrayRawBufferRefs::takeOwnership(SharedArrayRawBufferRefs&& other) { MOZ_ASSERT(refs_.empty()); refs_ = std::move(other.refs_); } void SharedArrayRawBufferRefs::releaseAll() { for (auto ref : refs_) { ref->dropReference(); } refs_.clear(); } // SCOutput provides an interface to write raw data -- eg uint64_ts, doubles, // arrays of bytes -- into a structured clone data output stream. It also knows // how to free any transferable data within that stream. // // Note that it contains a full JSStructuredCloneData object, which holds the // callbacks necessary to read/write/transfer/free the data. For the purpose of // this class, only the freeTransfer callback is relevant; the rest of the // callbacks are used by the higher-level JSStructuredCloneWriter interface. struct SCOutput { public: using Iter = BufferIterator; SCOutput(JSContext* cx, JS::StructuredCloneScope scope); JSContext* context() const { return cx; } JS::StructuredCloneScope scope() const { return buf.scope(); } void sameProcessScopeRequired() { buf.sameProcessScopeRequired(); } [[nodiscard]] bool write(uint64_t u); [[nodiscard]] bool writePair(uint32_t tag, uint32_t data); [[nodiscard]] bool writeDouble(double d); [[nodiscard]] bool writeBytes(const void* p, size_t nbytes); [[nodiscard]] bool writeChars(const Latin1Char* p, size_t nchars); [[nodiscard]] bool writeChars(const char16_t* p, size_t nchars); template [[nodiscard]] bool writeArray(const T* p, size_t nelems); void setCallbacks(const JSStructuredCloneCallbacks* callbacks, void* closure, OwnTransferablePolicy policy) { buf.setCallbacks(callbacks, closure, policy); } void extractBuffer(JSStructuredCloneData* data) { *data = std::move(buf); } uint64_t tell() const { return buf.Size(); } uint64_t count() const { return buf.Size() / sizeof(uint64_t); } Iter iter() { return Iter(buf); } size_t offset(Iter dest) { return dest - iter(); } JSContext* cx; JSStructuredCloneData buf; }; class SCInput { public: using BufferIterator = js::BufferIterator; SCInput(JSContext* cx, const JSStructuredCloneData& data); JSContext* context() const { return cx; } static void getPtr(uint64_t data, void** ptr); static void getPair(uint64_t data, uint32_t* tagp, uint32_t* datap); [[nodiscard]] bool read(uint64_t* p); [[nodiscard]] bool readPair(uint32_t* tagp, uint32_t* datap); [[nodiscard]] bool readDouble(double* p); [[nodiscard]] bool readBytes(void* p, size_t nbytes); [[nodiscard]] bool readChars(Latin1Char* p, size_t nchars); [[nodiscard]] bool readChars(char16_t* p, size_t nchars); [[nodiscard]] bool readPtr(void**); [[nodiscard]] bool get(uint64_t* p); [[nodiscard]] bool getPair(uint32_t* tagp, uint32_t* datap); const BufferIterator& tell() const { return point; } void seekTo(const BufferIterator& pos) { point = pos; } [[nodiscard]] bool seekBy(size_t pos) { if (!point.advance(pos)) { reportTruncated(); return false; } return true; } template [[nodiscard]] bool readArray(T* p, size_t nelems); bool reportTruncated() { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "truncated"); return false; } private: void staticAssertions() { static_assert(sizeof(char16_t) == 2); static_assert(sizeof(uint32_t) == 4); } JSContext* cx; BufferIterator point; }; } // namespace js struct JSStructuredCloneReader { public: explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure) : in(in), allowedScope(scope), cloneDataPolicy(cloneDataPolicy), objs(in.context()), objState(in.context(), in.context()), allObjs(in.context()), numItemsRead(0), callbacks(cb), closure(cbClosure) { // Avoid the need to bounds check by keeping a never-matching element at the // base of the `objState` stack. This append() will always succeed because // the objState vector has a nonzero MinInlineCapacity. MOZ_ALWAYS_TRUE(objState.append(std::make_pair(nullptr, true))); } SCInput& input() { return in; } bool read(MutableHandleValue vp, size_t nbytes); private: JSContext* context() { return in.context(); } bool readHeader(); bool readTransferMap(); [[nodiscard]] bool readUint32(uint32_t* num); template JSString* readStringImpl(uint32_t nchars, gc::Heap heap); JSString* readString(uint32_t data, gc::Heap heap = gc::Heap::Default); BigInt* readBigInt(uint32_t data); [[nodiscard]] bool readTypedArray(uint32_t arrayType, uint64_t nelems, MutableHandleValue vp, bool v1Read = false); [[nodiscard]] bool readDataView(uint64_t byteLength, MutableHandleValue vp); [[nodiscard]] bool readArrayBuffer(StructuredDataType type, uint32_t data, MutableHandleValue vp); [[nodiscard]] bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp); [[nodiscard]] bool readSharedArrayBuffer(MutableHandleValue vp); [[nodiscard]] bool readSharedWasmMemory(uint32_t nbytes, MutableHandleValue vp); // A serialized SavedFrame contains primitive values in a header followed by // an optional parent frame that is read recursively. [[nodiscard]] JSObject* readSavedFrameHeader(uint32_t principalsTag); [[nodiscard]] bool readSavedFrameFields(Handle frameObj, HandleValue parent, bool* state); // A serialized Error contains primitive values in a header followed by // 'cause', 'errors', and 'stack' fields that are read recursively. [[nodiscard]] JSObject* readErrorHeader(uint32_t type); [[nodiscard]] bool readErrorFields(Handle errorObj, HandleValue cause, bool* state); [[nodiscard]] bool readMapField(Handle mapObj, HandleValue key); [[nodiscard]] bool readObjectField(HandleObject obj, HandleValue key); [[nodiscard]] bool startRead(MutableHandleValue vp, gc::Heap strHeap = gc::Heap::Default); SCInput& in; // The widest scope that the caller will accept, where // SameProcess 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; const JS::CloneDataPolicy cloneDataPolicy; // Stack of objects with properties remaining to be read. RootedValueVector objs; // Maintain a stack of state values for the `objs` stack. Since this is only // needed for a very small subset of objects (those with a known set of // object children), the state information is stored as a stack of // pairs where the object determines which element of the // `objs` stack that it corresponds to. So when reading from the `objs` stack, // the state will be retrieved only if the top object on `objState` matches // the top object of `objs`. // // Currently, the only state needed is a boolean indicating whether the fields // have been read yet. Rooted, bool>, 8>> objState; // Array of all objects read during this deserialization, for resolving // backreferences. // // For backreferences to work correctly, objects must be added to this // array in exactly the order expected by the version of the Writer that // created the serialized data, even across years and format versions. This // is usually no problem, since both algorithms do a single linear pass // over the serialized data. There is one hitch; see readTypedArray. // // The values in this vector are objects, except it can temporarily have // one `undefined` placeholder value (the readTypedArray hack). RootedValueVector allObjs; size_t numItemsRead; // The user defined callbacks that will be used for cloning. const JSStructuredCloneCallbacks* callbacks; // Any value passed to JS_ReadStructuredClone. void* closure; friend bool JS_ReadString(JSStructuredCloneReader* r, JS::MutableHandleString str); friend bool JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp); // Provide a way to detect whether any of the clone data is never used. When // "tail" data (currently, this is only stored data for Transferred // ArrayBuffers in the DifferentProcess scope) is read, record the first and // last positions. At the end of deserialization, make sure there's nothing // between the end of the main data and the beginning of the tail, nor after // the end of the tail. mozilla::Maybe tailStartPos; mozilla::Maybe tailEndPos; }; struct JSStructuredCloneWriter { public: explicit JSStructuredCloneWriter(JSContext* cx, JS::StructuredCloneScope scope, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure, const Value& tVal) : out(cx, scope), callbacks(cb), closure(cbClosure), objs(cx), counts(cx), objectEntries(cx), otherEntries(cx), memory(cx), transferable(cx, tVal), transferableObjects(cx, TransferableObjectsList(cx)), cloneDataPolicy(cloneDataPolicy) { out.setCallbacks(cb, cbClosure, OwnTransferablePolicy::OwnsTransferablesIfAny); } bool init() { return parseTransferable() && writeHeader() && writeTransferMap(); } bool write(HandleValue v); SCOutput& output() { return out; } void extractBuffer(JSStructuredCloneData* newData) { out.extractBuffer(newData); } private: JSStructuredCloneWriter() = delete; JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete; JSContext* context() { return out.context(); } bool writeHeader(); bool writeTransferMap(); bool writeString(uint32_t tag, JSString* str); bool writeBigInt(uint32_t tag, BigInt* bi); bool writeArrayBuffer(HandleObject obj); bool writeTypedArray(HandleObject obj); bool writeDataView(HandleObject obj); bool writeSharedArrayBuffer(HandleObject obj); bool writeSharedWasmMemory(HandleObject obj); bool startObject(HandleObject obj, bool* backref); bool writePrimitive(HandleValue v); bool startWrite(HandleValue v); bool traverseObject(HandleObject obj, ESClass cls); bool traverseMap(HandleObject obj); bool traverseSet(HandleObject obj); bool traverseSavedFrame(HandleObject obj); bool traverseError(HandleObject obj); template bool reportDataCloneError(uint32_t errorId, Args&&... aArgs); bool parseTransferable(); bool transferOwnership(); inline void checkStack(); SCOutput out; // The user defined callbacks that will be used to signal cloning, in some // cases. const JSStructuredCloneCallbacks* callbacks; // Any value passed to the callbacks. void* closure; // Vector of objects with properties remaining to be written. // // NB: These can span multiple compartments, so the compartment must be // entered before any manipulation is performed. RootedValueVector objs; // counts[i] is the number of entries of objs[i] remaining to be written. // counts.length() == objs.length() and sum(counts) == entries.length(). Vector counts; // For JSObject: Property IDs as value RootedIdVector objectEntries; // For Map: Key followed by value // For Set: Key // For SavedFrame: parent SavedFrame // For Error: cause, errors, stack RootedValueVector otherEntries; // The "memory" list described in the HTML5 internal structured cloning // algorithm. memory is a superset of objs; items are never removed from // Memory until a serialization operation is finished using CloneMemory = GCHashMap, SystemAllocPolicy>; Rooted memory; // Set of transferable objects RootedValue transferable; using TransferableObjectsList = GCVector; Rooted transferableObjects; const JS::CloneDataPolicy cloneDataPolicy; friend bool JS_WriteString(JSStructuredCloneWriter* w, HandleString str); friend bool JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v); friend bool JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj); }; JS_PUBLIC_API uint64_t js::GetSCOffset(JSStructuredCloneWriter* writer) { MOZ_ASSERT(writer); return writer->output().count() * sizeof(uint64_t); } static_assert(SCTAG_END_OF_BUILTIN_TYPES <= JS_SCTAG_USER_MIN); static_assert(JS_SCTAG_USER_MIN <= JS_SCTAG_USER_MAX); static_assert(Scalar::Int8 == 0); template static void ReportDataCloneError(JSContext* cx, const JSStructuredCloneCallbacks* callbacks, uint32_t errorId, void* closure, Args&&... aArgs) { unsigned errorNumber; switch (errorId) { case JS_SCERR_DUP_TRANSFERABLE: errorNumber = JSMSG_SC_DUP_TRANSFERABLE; break; case JS_SCERR_TRANSFERABLE: errorNumber = JSMSG_SC_NOT_TRANSFERABLE; break; case JS_SCERR_UNSUPPORTED_TYPE: errorNumber = JSMSG_SC_UNSUPPORTED_TYPE; break; case JS_SCERR_SHMEM_TRANSFERABLE: errorNumber = JSMSG_SC_SHMEM_TRANSFERABLE; break; case JS_SCERR_TYPED_ARRAY_DETACHED: errorNumber = JSMSG_TYPED_ARRAY_DETACHED; break; case JS_SCERR_WASM_NO_TRANSFER: errorNumber = JSMSG_WASM_NO_TRANSFER; break; case JS_SCERR_NOT_CLONABLE: errorNumber = JSMSG_SC_NOT_CLONABLE; break; case JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP: errorNumber = JSMSG_SC_NOT_CLONABLE_WITH_COOP_COEP; break; default: MOZ_CRASH("Unkown errorId"); break; } if (callbacks && callbacks->reportError) { MOZ_RELEASE_ASSERT(!cx->isExceptionPending()); JSErrorReport report; report.errorNumber = errorNumber; // Get js error message if it's possible and propagate it through callback. if (JS_ExpandErrorArgumentsASCII(cx, GetErrorMessage, errorNumber, &report, std::forward(aArgs)...) && report.message()) { callbacks->reportError(cx, errorId, closure, report.message().c_str()); } else { ReportOutOfMemory(cx); callbacks->reportError(cx, errorId, closure, ""); } return; } JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber, std::forward(aArgs)...); } bool WriteStructuredClone(JSContext* cx, HandleValue v, JSStructuredCloneData* bufp, JS::StructuredCloneScope scope, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure, const Value& transferable) { JSStructuredCloneWriter w(cx, scope, cloneDataPolicy, cb, cbClosure, transferable); if (!w.init()) { return false; } if (!w.write(v)) { return false; } w.extractBuffer(bufp); return true; } bool ReadStructuredClone(JSContext* cx, const JSStructuredCloneData& data, JS::StructuredCloneScope scope, MutableHandleValue vp, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* cb, void* cbClosure) { if (data.Size() % 8) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "misaligned"); return false; } SCInput in(cx, data); JSStructuredCloneReader r(in, scope, cloneDataPolicy, cb, cbClosure); return r.read(vp, data.Size()); } static bool StructuredCloneHasTransferObjects( const JSStructuredCloneData& data) { if (data.Size() < sizeof(uint64_t)) { return false; } uint64_t u; BufferIterator iter(data); MOZ_ALWAYS_TRUE(iter.readBytes(reinterpret_cast(&u), sizeof(u))); uint32_t tag = uint32_t(u >> 32); return (tag == SCTAG_TRANSFER_MAP_HEADER); } namespace js { SCInput::SCInput(JSContext* cx, const JSStructuredCloneData& data) : cx(cx), point(data) { static_assert(JSStructuredCloneData::BufferList::kSegmentAlignment % 8 == 0, "structured clone buffer reads should be aligned"); MOZ_ASSERT(data.Size() % 8 == 0); } bool SCInput::read(uint64_t* p) { if (!point.canPeek()) { *p = 0; // initialize to shut GCC up return reportTruncated(); } *p = NativeEndian::swapFromLittleEndian(point.peek()); MOZ_ALWAYS_TRUE(point.advance()); return true; } bool SCInput::readPair(uint32_t* tagp, uint32_t* datap) { uint64_t u; bool ok = read(&u); if (ok) { *tagp = uint32_t(u >> 32); *datap = uint32_t(u); } return ok; } bool SCInput::get(uint64_t* p) { if (!point.canPeek()) { return reportTruncated(); } *p = NativeEndian::swapFromLittleEndian(point.peek()); return true; } bool SCInput::getPair(uint32_t* tagp, uint32_t* datap) { uint64_t u = 0; if (!get(&u)) { return false; } *tagp = uint32_t(u >> 32); *datap = uint32_t(u); return true; } void SCInput::getPair(uint64_t data, uint32_t* tagp, uint32_t* datap) { uint64_t u = NativeEndian::swapFromLittleEndian(data); *tagp = uint32_t(u >> 32); *datap = uint32_t(u); } bool SCInput::readDouble(double* p) { uint64_t u; if (!read(&u)) { return false; } *p = CanonicalizeNaN(mozilla::BitwiseCast(u)); return true; } template static void swapFromLittleEndianInPlace(T* ptr, size_t nelems) { if (nelems > 0) { NativeEndian::swapFromLittleEndianInPlace(ptr, nelems); } } template <> void swapFromLittleEndianInPlace(uint8_t* ptr, size_t nelems) {} // Data is packed into an integral number of uint64_t words. Compute the // padding required to finish off the final word. static size_t ComputePadding(size_t nelems, size_t elemSize) { // We want total length mod 8, where total length is nelems * sizeof(T), // but that might overflow. So reduce nelems to nelems mod 8, since we are // going to be doing a mod 8 later anyway. size_t leftoverLength = (nelems % sizeof(uint64_t)) * elemSize; return (-leftoverLength) & (sizeof(uint64_t) - 1); } template bool SCInput::readArray(T* p, size_t nelems) { if (!nelems) { return true; } static_assert(sizeof(uint64_t) % sizeof(T) == 0); // Fail if nelems is so huge that computing the full size will overflow. mozilla::CheckedInt size = mozilla::CheckedInt(nelems) * sizeof(T); if (!size.isValid()) { return reportTruncated(); } if (!point.readBytes(reinterpret_cast(p), size.value())) { // To avoid any way in which uninitialized data could escape, zero the array // if filling it failed. std::uninitialized_fill_n(p, nelems, 0); return false; } swapFromLittleEndianInPlace(p, nelems); point += ComputePadding(nelems, sizeof(T)); return true; } bool SCInput::readBytes(void* p, size_t nbytes) { return readArray((uint8_t*)p, nbytes); } bool SCInput::readChars(Latin1Char* p, size_t nchars) { static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte"); return readBytes(p, nchars); } bool SCInput::readChars(char16_t* p, size_t nchars) { MOZ_ASSERT(sizeof(char16_t) == sizeof(uint16_t)); return readArray((uint16_t*)p, nchars); } void SCInput::getPtr(uint64_t data, void** ptr) { *ptr = reinterpret_cast(NativeEndian::swapFromLittleEndian(data)); } bool SCInput::readPtr(void** p) { uint64_t u; if (!read(&u)) { return false; } *p = reinterpret_cast(u); return true; } SCOutput::SCOutput(JSContext* cx, JS::StructuredCloneScope scope) : cx(cx), buf(scope) {} bool SCOutput::write(uint64_t u) { uint64_t v = NativeEndian::swapToLittleEndian(u); if (!buf.AppendBytes(reinterpret_cast(&v), sizeof(u))) { ReportOutOfMemory(context()); return false; } return true; } bool SCOutput::writePair(uint32_t tag, uint32_t data) { // As it happens, the tag word appears after the data word in the output. // This is because exponents occupy the last 2 bytes of doubles on the // little-endian platforms we care most about. // // For example, TrueValue() is written using writePair(SCTAG_BOOLEAN, 1). // PairToUInt64 produces the number 0xFFFF000200000001. // That is written out as the bytes 01 00 00 00 02 00 FF FF. return write(PairToUInt64(tag, data)); } static inline double ReinterpretPairAsDouble(uint32_t tag, uint32_t data) { return BitwiseCast(PairToUInt64(tag, data)); } bool SCOutput::writeDouble(double d) { return write(BitwiseCast(CanonicalizeNaN(d))); } template bool SCOutput::writeArray(const T* p, size_t nelems) { static_assert(8 % sizeof(T) == 0); static_assert(sizeof(uint64_t) % sizeof(T) == 0); if (nelems == 0) { return true; } for (size_t i = 0; i < nelems; i++) { T value = NativeEndian::swapToLittleEndian(p[i]); if (!buf.AppendBytes(reinterpret_cast(&value), sizeof(value))) { return false; } } // Zero-pad to 8 bytes boundary. size_t padbytes = ComputePadding(nelems, sizeof(T)); char zeroes[sizeof(uint64_t)] = {0}; if (!buf.AppendBytes(zeroes, padbytes)) { return false; } return true; } template <> bool SCOutput::writeArray(const uint8_t* p, size_t nelems) { if (nelems == 0) { return true; } if (!buf.AppendBytes(reinterpret_cast(p), nelems)) { return false; } // zero-pad to 8 bytes boundary size_t padbytes = ComputePadding(nelems, 1); char zeroes[sizeof(uint64_t)] = {0}; if (!buf.AppendBytes(zeroes, padbytes)) { return false; } return true; } bool SCOutput::writeBytes(const void* p, size_t nbytes) { return writeArray((const uint8_t*)p, nbytes); } bool SCOutput::writeChars(const char16_t* p, size_t nchars) { static_assert(sizeof(char16_t) == sizeof(uint16_t), "required so that treating char16_t[] memory as uint16_t[] " "memory is permissible"); return writeArray((const uint16_t*)p, nchars); } bool SCOutput::writeChars(const Latin1Char* p, size_t nchars) { static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte"); return writeBytes(p, nchars); } } // namespace js JSStructuredCloneData::~JSStructuredCloneData() { discardTransferables(); } // If the buffer contains Transferables, free them. Note that custom // Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to // delete their transferables. void JSStructuredCloneData::discardTransferables() { if (!Size()) { return; } if (ownTransferables_ != OwnTransferablePolicy::OwnsTransferablesIfAny) { return; } // DifferentProcess clones cannot contain pointers, so nothing needs to be // released. if (scope() == JS::StructuredCloneScope::DifferentProcess) { return; } FreeTransferStructuredCloneOp freeTransfer = nullptr; if (callbacks_) { freeTransfer = callbacks_->freeTransfer; } auto point = BufferIterator(*this); if (point.done()) { return; // Empty buffer } uint32_t tag, data; MOZ_RELEASE_ASSERT(point.canPeek()); SCInput::getPair(point.peek(), &tag, &data); MOZ_ALWAYS_TRUE(point.advance()); if (tag == SCTAG_HEADER) { if (point.done()) { return; } MOZ_RELEASE_ASSERT(point.canPeek()); SCInput::getPair(point.peek(), &tag, &data); MOZ_ALWAYS_TRUE(point.advance()); } if (tag != SCTAG_TRANSFER_MAP_HEADER) { return; } if (TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED) { return; } // freeTransfer should not GC JS::AutoSuppressGCAnalysis nogc; if (point.done()) { return; } MOZ_RELEASE_ASSERT(point.canPeek()); uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek()); MOZ_ALWAYS_TRUE(point.advance()); while (numTransferables--) { if (!point.canPeek()) { return; } uint32_t ownership; SCInput::getPair(point.peek(), &tag, &ownership); MOZ_ALWAYS_TRUE(point.advance()); MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY); if (!point.canPeek()) { return; } void* content; SCInput::getPtr(point.peek(), &content); MOZ_ALWAYS_TRUE(point.advance()); if (!point.canPeek()) { return; } uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek()); MOZ_ALWAYS_TRUE(point.advance()); if (ownership < JS::SCTAG_TMO_FIRST_OWNED) { continue; } if (ownership == JS::SCTAG_TMO_ALLOC_DATA) { js_free(content); } else if (ownership == JS::SCTAG_TMO_MAPPED_DATA) { JS::ReleaseMappedArrayBufferContents(content, extraData); } else if (freeTransfer) { freeTransfer(tag, JS::TransferableOwnership(ownership), content, extraData, closure_); } else { MOZ_ASSERT(false, "unknown ownership"); } } } static_assert(JSString::MAX_LENGTH < UINT32_MAX); bool JSStructuredCloneWriter::parseTransferable() { // NOTE: The transferables set is tested for non-emptiness at various // junctures in structured cloning, so this set must be initialized // by this method in all non-error cases. MOZ_ASSERT(transferableObjects.empty(), "parseTransferable called with stale data"); if (transferable.isNull() || transferable.isUndefined()) { return true; } if (!transferable.isObject()) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } JSContext* cx = context(); RootedObject array(cx, &transferable.toObject()); bool isArray; if (!JS::IsArrayObject(cx, array, &isArray)) { return false; } if (!isArray) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } uint32_t length; if (!JS::GetArrayLength(cx, array, &length)) { return false; } // Initialize the set for the provided array's length. if (!transferableObjects.reserve(length)) { return false; } if (length == 0) { return true; } RootedValue v(context()); RootedObject tObj(context()); for (uint32_t i = 0; i < length; ++i) { if (!CheckForInterrupt(cx)) { return false; } if (!JS_GetElement(cx, array, i, &v)) { return false; } if (!v.isObject()) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } tObj = &v.toObject(); RootedObject unwrappedObj(cx, CheckedUnwrapStatic(tObj)); if (!unwrappedObj) { ReportAccessDenied(cx); return false; } // Shared memory cannot be transferred because it is not possible (nor // desirable) to detach the memory in agents that already hold a // reference to it. if (unwrappedObj->is()) { return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE); } else if (unwrappedObj->is()) { if (unwrappedObj->as().isShared()) { return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE); } } // External array buffers may be able to be transferred in the future, // but that is not currently implemented. else if (unwrappedObj->is()) { if (unwrappedObj->as().isExternal()) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } } else { if (!out.buf.callbacks_ || !out.buf.callbacks_->canTransfer) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } JSAutoRealm ar(cx, unwrappedObj); bool sameProcessScopeRequired = false; if (!out.buf.callbacks_->canTransfer( cx, unwrappedObj, &sameProcessScopeRequired, out.buf.closure_)) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } if (sameProcessScopeRequired) { output().sameProcessScopeRequired(); } } // No duplicates allowed if (std::find(transferableObjects.begin(), transferableObjects.end(), tObj) != transferableObjects.end()) { return reportDataCloneError(JS_SCERR_DUP_TRANSFERABLE); } if (!transferableObjects.append(tObj)) { return false; } } return true; } template bool JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId, Args&&... aArgs) { ReportDataCloneError(context(), out.buf.callbacks_, errorId, out.buf.closure_, std::forward(aArgs)...); return false; } bool JSStructuredCloneWriter::writeString(uint32_t tag, JSString* str) { JSLinearString* linear = str->ensureLinear(context()); if (!linear) { return false; } #if FUZZING_JS_FUZZILLI if (js::SupportDifferentialTesting()) { // TODO we could always output a twoByteChar string return true; } #endif static_assert(JSString::MAX_LENGTH <= INT32_MAX, "String length must fit in 31 bits"); uint32_t length = linear->length(); uint32_t lengthAndEncoding = length | (uint32_t(linear->hasLatin1Chars()) << 31); if (!out.writePair(tag, lengthAndEncoding)) { return false; } JS::AutoCheckCannotGC nogc; return linear->hasLatin1Chars() ? out.writeChars(linear->latin1Chars(nogc), length) : out.writeChars(linear->twoByteChars(nogc), length); } bool JSStructuredCloneWriter::writeBigInt(uint32_t tag, BigInt* bi) { bool signBit = bi->isNegative(); size_t length = bi->digitLength(); // The length must fit in 31 bits to leave room for a sign bit. if (length > size_t(INT32_MAX)) { return false; } uint32_t lengthAndSign = length | (static_cast(signBit) << 31); if (!out.writePair(tag, lengthAndSign)) { return false; } return out.writeArray(bi->digits().data(), length); } inline void JSStructuredCloneWriter::checkStack() { #ifdef DEBUG // To avoid making serialization O(n^2), limit stack-checking at 10. const size_t MAX = 10; size_t limit = std::min(counts.length(), MAX); MOZ_ASSERT(objs.length() == counts.length()); size_t total = 0; for (size_t i = 0; i < limit; i++) { MOZ_ASSERT(total + counts[i] >= total); total += counts[i]; } if (counts.length() <= MAX) { MOZ_ASSERT(total == objectEntries.length() + otherEntries.length()); } else { MOZ_ASSERT(total <= objectEntries.length() + otherEntries.length()); } size_t j = objs.length(); for (size_t i = 0; i < limit; i++) { --j; MOZ_ASSERT(memory.has(&objs[j].toObject())); } #endif } /* * Write out a typed array. Note that post-v1 structured clone buffers do not * perform endianness conversion on stored data, so multibyte typed arrays * cannot be deserialized into a different endianness machine. Endianness * conversion would prevent sharing ArrayBuffers: if you have Int8Array and * Int16Array views of the same ArrayBuffer, should the data bytes be * byte-swapped when writing or not? The Int8Array requires them to not be * swapped; the Int16Array requires that they are. */ bool JSStructuredCloneWriter::writeTypedArray(HandleObject obj) { Rooted tarr(context(), obj->maybeUnwrapAs()); JSAutoRealm ar(context(), tarr); #ifdef FUZZING_JS_FUZZILLI if (js::SupportDifferentialTesting() && !tarr->hasBuffer()) { // fake oom because differential testing will fail fprintf(stderr, "[unhandlable oom]"); _exit(-1); return false; } #endif if (!TypedArrayObject::ensureHasBuffer(context(), tarr)) { return false; } if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, uint32_t(tarr->type()))) { return false; } uint64_t nelems = tarr->length(); if (!out.write(nelems)) { return false; } // Write out the ArrayBuffer tag and contents RootedValue val(context(), tarr->bufferValue()); if (!startWrite(val)) { return false; } uint64_t byteOffset = tarr->byteOffset(); return out.write(byteOffset); } bool JSStructuredCloneWriter::writeDataView(HandleObject obj) { Rooted view(context(), obj->maybeUnwrapAs()); JSAutoRealm ar(context(), view); if (!out.writePair(SCTAG_DATA_VIEW_OBJECT, 0)) { return false; } uint64_t byteLength = view->byteLength(); if (!out.write(byteLength)) { return false; } // Write out the ArrayBuffer tag and contents RootedValue val(context(), view->bufferValue()); if (!startWrite(val)) { return false; } uint64_t byteOffset = view->byteOffset(); return out.write(byteOffset); } bool JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj) { Rooted buffer(context(), obj->maybeUnwrapAs()); JSAutoRealm ar(context(), buffer); if (!out.writePair(SCTAG_ARRAY_BUFFER_OBJECT, 0)) { return false; } uint64_t byteLength = buffer->byteLength(); if (!out.write(byteLength)) { return false; } return out.writeBytes(buffer->dataPointer(), byteLength); } bool JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj) { MOZ_ASSERT(obj->canUnwrapAs()); if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) { auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled() ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP : JS_SCERR_NOT_CLONABLE; reportDataCloneError(error, "SharedArrayBuffer"); return false; } output().sameProcessScopeRequired(); // We must not transmit SAB pointers (including for WebAssembly.Memory) // cross-process. The cloneDataPolicy should have guarded against this; // since it did not then throw, with a very explicit message. if (output().scope() > JS::StructuredCloneScope::SameProcess) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_SHMEM_POLICY); return false; } Rooted sharedArrayBuffer( context(), obj->maybeUnwrapAs()); SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject(); if (!out.buf.refsHeld_.acquire(context(), rawbuf)) { return false; } // We must serialize the length so that the buffer object arrives in the // receiver with the same length, and not with the length read from the // rawbuf - that length can be different, and it can change at any time. intptr_t p = reinterpret_cast(rawbuf); uint64_t byteLength = sharedArrayBuffer->byteLength(); if (!(out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT, static_cast(sizeof(p))) && out.writeBytes(&byteLength, sizeof(byteLength)) && out.writeBytes(&p, sizeof(p)))) { return false; } if (callbacks && callbacks->sabCloned && !callbacks->sabCloned(context(), /*receiving=*/false, closure)) { return false; } return true; } bool JSStructuredCloneWriter::writeSharedWasmMemory(HandleObject obj) { MOZ_ASSERT(obj->canUnwrapAs()); // Check the policy here so that we can report a sane error. if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) { auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled() ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP : JS_SCERR_NOT_CLONABLE; reportDataCloneError(error, "WebAssembly.Memory"); return false; } // If this changes, might need to change what we write. MOZ_ASSERT(WasmMemoryObject::RESERVED_SLOTS == 3); Rooted memoryObj(context(), &obj->unwrapAs()); Rooted sab( context(), &memoryObj->buffer().as()); return out.writePair(SCTAG_SHARED_WASM_MEMORY_OBJECT, 0) && out.writePair(SCTAG_BOOLEAN, memoryObj->isHuge()) && writeSharedArrayBuffer(sab); } bool JSStructuredCloneWriter::startObject(HandleObject obj, bool* backref) { // Handle cycles in the object graph. CloneMemory::AddPtr p = memory.lookupForAdd(obj); if ((*backref = p.found())) { return out.writePair(SCTAG_BACK_REFERENCE_OBJECT, p->value()); } if (!memory.add(p, obj, memory.count())) { ReportOutOfMemory(context()); return false; } if (memory.count() == UINT32_MAX) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_NEED_DIET, "object graph to serialize"); return false; } return true; } static bool TryAppendNativeProperties(JSContext* cx, HandleObject obj, MutableHandleIdVector entries, size_t* properties, bool* optimized) { *optimized = false; if (!obj->is()) { return true; } Handle nobj = obj.as(); if (nobj->isIndexed() || nobj->is() || nobj->getClass()->getNewEnumerate() || nobj->getClass()->getEnumerate()) { return true; } *optimized = true; size_t count = 0; // We iterate from the last to the first property, so the property names // are already in reverse order. for (ShapePropertyIter iter(nobj->shape()); !iter.done(); iter++) { jsid id = iter->key(); // Ignore symbols and non-enumerable properties. if (!iter->enumerable() || id.isSymbol()) { continue; } MOZ_ASSERT(id.isString()); if (!entries.append(id)) { return false; } count++; } // Add dense element ids in reverse order. for (uint32_t i = nobj->getDenseInitializedLength(); i > 0; --i) { if (nobj->getDenseElement(i - 1).isMagic(JS_ELEMENTS_HOLE)) { continue; } if (!entries.append(PropertyKey::Int(i - 1))) { return false; } count++; } *properties = count; return true; } // Objects are written as a "preorder" traversal of the object graph: object // "headers" (the class tag and any data needed for initial construction) are // visited first, then the children are recursed through (where children are // properties, Set or Map entries, etc.). So for example // // obj1 = { key1: { key1.1: val1.1, key1.2: val1.2 }, key2: {} } // // would be stored as: // // // // // // // // // // // // // // // This nests nicely (ie, an entire recursive value starts with its tag and // ends with its end-of-children marker) and so it can be presented indented. // But see traverseMap below for how this looks different for Maps. bool JSStructuredCloneWriter::traverseObject(HandleObject obj, ESClass cls) { size_t count; bool optimized = false; if (!js::SupportDifferentialTesting()) { if (!TryAppendNativeProperties(context(), obj, &objectEntries, &count, &optimized)) { return false; } } if (!optimized) { // Get enumerable property ids and put them in reverse order so that they // will come off the stack in forward order. RootedIdVector properties(context()); if (!GetPropertyKeys(context(), obj, JSITER_OWNONLY, &properties)) { return false; } for (size_t i = properties.length(); i > 0; --i) { jsid id = properties[i - 1]; MOZ_ASSERT(id.isString() || id.isInt()); if (!objectEntries.append(id)) { return false; } } count = properties.length(); } // Push obj and count to the stack. if (!objs.append(ObjectValue(*obj)) || !counts.append(count)) { return false; } checkStack(); #if DEBUG ESClass cls2; if (!GetBuiltinClass(context(), obj, &cls2)) { return false; } MOZ_ASSERT(cls2 == cls); #endif // Write the header for obj. if (cls == ESClass::Array) { uint32_t length = 0; if (!JS::GetArrayLength(context(), obj, &length)) { return false; } return out.writePair(SCTAG_ARRAY_OBJECT, NativeEndian::swapToLittleEndian(length)); } return out.writePair(SCTAG_OBJECT_OBJECT, 0); } // Use the same basic setup as for traverseObject, but now keys can themselves // be complex objects. Keys and values are visited first via startWrite(), then // the key's children (if any) are handled, then the value's children. // // m = new Map(); // m.set(key1 = ..., value1 = ...) // // where key1 and value2 are both objects would be stored as // // // // // ...key1 fields... // // ...value1 fields... // // // // Notice how the end-of-children marker for key1 is sandwiched between the // value1 beginning and end. bool JSStructuredCloneWriter::traverseMap(HandleObject obj) { Rooted> newEntries(context(), GCVector(context())); { // If there is no wrapper, the compartment munging is a no-op. RootedObject unwrapped(context(), obj->maybeUnwrapAs()); MOZ_ASSERT(unwrapped); JSAutoRealm ar(context(), unwrapped); if (!MapObject::getKeysAndValuesInterleaved(unwrapped, &newEntries)) { return false; } } if (!context()->compartment()->wrap(context(), &newEntries)) { return false; } for (size_t i = newEntries.length(); i > 0; --i) { if (!otherEntries.append(newEntries[i - 1])) { return false; } } // Push obj and count to the stack. if (!objs.append(ObjectValue(*obj)) || !counts.append(newEntries.length())) { return false; } checkStack(); // Write the header for obj. return out.writePair(SCTAG_MAP_OBJECT, 0); } // Similar to traverseMap, only there is a single value instead of a key and // value, and thus no interleaving is possible: a value will be fully emitted // before the next value is begun. bool JSStructuredCloneWriter::traverseSet(HandleObject obj) { Rooted> keys(context(), GCVector(context())); { // If there is no wrapper, the compartment munging is a no-op. RootedObject unwrapped(context(), obj->maybeUnwrapAs()); MOZ_ASSERT(unwrapped); JSAutoRealm ar(context(), unwrapped); if (!SetObject::keys(context(), unwrapped, &keys)) { return false; } } if (!context()->compartment()->wrap(context(), &keys)) { return false; } for (size_t i = keys.length(); i > 0; --i) { if (!otherEntries.append(keys[i - 1])) { return false; } } // Push obj and count to the stack. if (!objs.append(ObjectValue(*obj)) || !counts.append(keys.length())) { return false; } checkStack(); // Write the header for obj. return out.writePair(SCTAG_SET_OBJECT, 0); } bool JSStructuredCloneWriter::traverseSavedFrame(HandleObject obj) { Rooted savedFrame(context(), obj->maybeUnwrapAs()); MOZ_ASSERT(savedFrame); RootedObject parent(context(), savedFrame->getParent()); if (!context()->compartment()->wrap(context(), &parent)) { return false; } if (!objs.append(ObjectValue(*obj)) || !otherEntries.append(parent ? ObjectValue(*parent) : NullValue()) || !counts.append(1)) { return false; } checkStack(); // Write the SavedFrame tag and the SavedFrame's principals. if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsSystem) { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM)) { return false; }; } else if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsNotSystem) { if (!out.writePair( SCTAG_SAVED_FRAME_OBJECT, SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM)) { return false; } } else { if (auto principals = savedFrame->getPrincipals()) { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_JSPRINCIPALS) || !principals->write(context(), this)) { return false; } } else { if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_NULL_JSPRINCIPALS)) { return false; } } } // Write the SavedFrame's reserved slots, except for the parent, which is // queued on objs for further traversal. RootedValue val(context()); val = BooleanValue(savedFrame->getMutedErrors()); if (!writePrimitive(val)) { return false; } context()->markAtom(savedFrame->getSource()); val = StringValue(savedFrame->getSource()); if (!writePrimitive(val)) { return false; } val = NumberValue(savedFrame->getLine()); if (!writePrimitive(val)) { return false; } val = NumberValue(savedFrame->getColumn()); if (!writePrimitive(val)) { return false; } auto name = savedFrame->getFunctionDisplayName(); if (name) { context()->markAtom(name); } val = name ? StringValue(name) : NullValue(); if (!writePrimitive(val)) { return false; } auto cause = savedFrame->getAsyncCause(); if (cause) { context()->markAtom(cause); } val = cause ? StringValue(cause) : NullValue(); if (!writePrimitive(val)) { return false; } return true; } // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal // 2.7.3 StructuredSerializeInternal ( value, forStorage [ , memory ] ) // // Step 17. Otherwise, if value has an [[ErrorData]] internal slot and // value is not a platform object, then: // // Note: This contains custom extensions for handling non-standard properties. bool JSStructuredCloneWriter::traverseError(HandleObject obj) { JSContext* cx = context(); // 1. Let name be ? Get(value, "name"). RootedValue name(cx); if (!GetProperty(cx, obj, obj, cx->names().name, &name)) { return false; } // 2. If name is not one of "Error", "EvalError", "RangeError", // "ReferenceError", "SyntaxError", "TypeError", or "URIError", // (not yet specified: or "AggregateError") // then set name to "Error". JSExnType type = JSEXN_ERR; if (name.isString()) { JSLinearString* linear = name.toString()->ensureLinear(cx); if (!linear) { return false; } if (EqualStrings(linear, cx->names().Error)) { type = JSEXN_ERR; } else if (EqualStrings(linear, cx->names().EvalError)) { type = JSEXN_EVALERR; } else if (EqualStrings(linear, cx->names().RangeError)) { type = JSEXN_RANGEERR; } else if (EqualStrings(linear, cx->names().ReferenceError)) { type = JSEXN_REFERENCEERR; } else if (EqualStrings(linear, cx->names().SyntaxError)) { type = JSEXN_SYNTAXERR; } else if (EqualStrings(linear, cx->names().TypeError)) { type = JSEXN_TYPEERR; } else if (EqualStrings(linear, cx->names().URIError)) { type = JSEXN_URIERR; } else if (EqualStrings(linear, cx->names().AggregateError)) { type = JSEXN_AGGREGATEERR; } } // 3. Let valueMessageDesc be ? value.[[GetOwnProperty]]("message"). RootedId messageId(cx, NameToId(cx->names().message)); Rooted> messageDesc(cx); if (!GetOwnPropertyDescriptor(cx, obj, messageId, &messageDesc)) { return false; } // 4. Let message be undefined if IsDataDescriptor(valueMessageDesc) is false, // and ? ToString(valueMessageDesc.[[Value]]) otherwise. RootedString message(cx); if (messageDesc.isSome() && messageDesc->isDataDescriptor()) { RootedValue messageVal(cx, messageDesc->value()); message = ToString(cx, messageVal); if (!message) { return false; } } // 5. Set serialized to { [[Type]]: "Error", [[Name]]: name, [[Message]]: // message }. if (!objs.append(ObjectValue(*obj))) { return false; } Rooted unwrapped(cx, obj->maybeUnwrapAs()); MOZ_ASSERT(unwrapped); // Non-standard: Serialize |stack|. // The Error stack property is saved as SavedFrames. RootedValue stack(cx, NullValue()); RootedObject stackObj(cx, unwrapped->stack()); if (stackObj && stackObj->canUnwrapAs()) { stack.setObject(*stackObj); if (!cx->compartment()->wrap(cx, &stack)) { return false; } } if (!otherEntries.append(stack)) { return false; } // Serialize |errors| if (type == JSEXN_AGGREGATEERR) { RootedValue errors(cx); if (!GetProperty(cx, obj, obj, cx->names().errors, &errors)) { return false; } if (!otherEntries.append(errors)) { return false; } } else { if (!otherEntries.append(NullValue())) { return false; } } // Non-standard: Serialize |cause|. Because this property // might be missing we also write "hasCause" later. RootedId causeId(cx, NameToId(cx->names().cause)); Rooted> causeDesc(cx); if (!GetOwnPropertyDescriptor(cx, obj, causeId, &causeDesc)) { return false; } Rooted> cause(cx); if (causeDesc.isSome() && causeDesc->isDataDescriptor()) { cause = mozilla::Some(causeDesc->value()); } if (!cx->compartment()->wrap(cx, &cause)) { return false; } if (!otherEntries.append(cause.get().valueOr(NullValue()))) { return false; } // |cause| + |errors| + |stack|, pushed in reverse order if (!counts.append(3)) { return false; } checkStack(); if (!out.writePair(SCTAG_ERROR_OBJECT, type)) { return false; } RootedValue val(cx, message ? StringValue(message) : NullValue()); if (!writePrimitive(val)) { return false; } // hasCause val = BooleanValue(cause.isSome()); if (!writePrimitive(val)) { return false; } // Non-standard: Also serialize fileName, lineNumber and columnNumber. { JSAutoRealm ar(cx, unwrapped); val = StringValue(unwrapped->fileName(cx)); } if (!cx->compartment()->wrap(cx, &val) || !writePrimitive(val)) { return false; } val = Int32Value(unwrapped->lineNumber()); if (!writePrimitive(val)) { return false; } val = Int32Value(unwrapped->columnNumber()); return writePrimitive(val); } bool JSStructuredCloneWriter::writePrimitive(HandleValue v) { MOZ_ASSERT(v.isPrimitive()); context()->check(v); if (v.isString()) { return writeString(SCTAG_STRING, v.toString()); } else if (v.isInt32()) { if (js::SupportDifferentialTesting()) { return out.writeDouble(v.toInt32()); } return out.writePair(SCTAG_INT32, v.toInt32()); } else if (v.isDouble()) { return out.writeDouble(v.toDouble()); } else if (v.isBoolean()) { return out.writePair(SCTAG_BOOLEAN, v.toBoolean()); } else if (v.isNull()) { return out.writePair(SCTAG_NULL, 0); } else if (v.isUndefined()) { return out.writePair(SCTAG_UNDEFINED, 0); } else if (v.isBigInt()) { return writeBigInt(SCTAG_BIGINT, v.toBigInt()); } return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE); } bool JSStructuredCloneWriter::startWrite(HandleValue v) { context()->check(v); if (v.isPrimitive()) { return writePrimitive(v); } if (!v.isObject()) { return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE); } RootedObject obj(context(), &v.toObject()); bool backref; if (!startObject(obj, &backref)) { return false; } if (backref) { return true; } ESClass cls; if (!GetBuiltinClass(context(), obj, &cls)) { return false; } switch (cls) { case ESClass::Object: case ESClass::Array: return traverseObject(obj, cls); case ESClass::Number: { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) { return false; } return out.writePair(SCTAG_NUMBER_OBJECT, 0) && out.writeDouble(unboxed.toNumber()); } case ESClass::String: { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) { return false; } return writeString(SCTAG_STRING_OBJECT, unboxed.toString()); } case ESClass::Boolean: { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) { return false; } return out.writePair(SCTAG_BOOLEAN_OBJECT, unboxed.toBoolean()); } case ESClass::RegExp: { RegExpShared* re = RegExpToShared(context(), obj); if (!re) { return false; } return out.writePair(SCTAG_REGEXP_OBJECT, re->getFlags().value()) && writeString(SCTAG_STRING, re->getSource()); } case ESClass::ArrayBuffer: { if (JS::IsArrayBufferObject(obj) && JS::ArrayBufferHasData(obj)) { return writeArrayBuffer(obj); } break; } case ESClass::SharedArrayBuffer: if (JS::IsSharedArrayBufferObject(obj)) { return writeSharedArrayBuffer(obj); } break; case ESClass::Date: { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) { return false; } return out.writePair(SCTAG_DATE_OBJECT, 0) && out.writeDouble(unboxed.toNumber()); } case ESClass::Set: return traverseSet(obj); case ESClass::Map: return traverseMap(obj); case ESClass::Error: return traverseError(obj); case ESClass::BigInt: { RootedValue unboxed(context()); if (!Unbox(context(), obj, &unboxed)) { return false; } return writeBigInt(SCTAG_BIGINT_OBJECT, unboxed.toBigInt()); } case ESClass::Promise: case ESClass::MapIterator: case ESClass::SetIterator: case ESClass::Arguments: case ESClass::Function: break; #ifdef ENABLE_RECORD_TUPLE case ESClass::Record: case ESClass::Tuple: MOZ_CRASH("Record and Tuple are not supported"); #endif case ESClass::Other: { if (obj->canUnwrapAs()) { return writeTypedArray(obj); } if (obj->canUnwrapAs()) { return writeDataView(obj); } if (wasm::IsSharedWasmMemoryObject(obj)) { return writeSharedWasmMemory(obj); } if (obj->canUnwrapAs()) { return traverseSavedFrame(obj); } break; } } if (out.buf.callbacks_ && out.buf.callbacks_->write) { bool sameProcessScopeRequired = false; if (!out.buf.callbacks_->write(context(), this, obj, &sameProcessScopeRequired, out.buf.closure_)) { return false; } if (sameProcessScopeRequired) { output().sameProcessScopeRequired(); } return true; } return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE); } bool JSStructuredCloneWriter::writeHeader() { return out.writePair(SCTAG_HEADER, (uint32_t)output().scope()); } bool JSStructuredCloneWriter::writeTransferMap() { if (transferableObjects.empty()) { return true; } if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_UNREAD)) { return false; } if (!out.write(transferableObjects.length())) { return false; } RootedObject obj(context()); for (auto* o : transferableObjects) { obj = o; if (!memory.put(obj, memory.count())) { ReportOutOfMemory(context()); return false; } // Emit a placeholder pointer. We defer stealing the data until later // (and, if necessary, detaching this object if it's an ArrayBuffer). if (!out.writePair(SCTAG_TRANSFER_MAP_PENDING_ENTRY, JS::SCTAG_TMO_UNFILLED)) { return false; } if (!out.write(0)) { // Pointer to ArrayBuffer contents. return false; } if (!out.write(0)) { // extraData return false; } } return true; } bool JSStructuredCloneWriter::transferOwnership() { if (transferableObjects.empty()) { return true; } // Walk along the transferables and the transfer map at the same time, // grabbing out pointers from the transferables and stuffing them into the // transfer map. auto point = out.iter(); MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_HEADER); point++; MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(uint32_t(NativeEndian::swapFromLittleEndian(point.peek()) >> 32) == SCTAG_TRANSFER_MAP_HEADER); point++; MOZ_RELEASE_ASSERT(point.canPeek()); MOZ_ASSERT(NativeEndian::swapFromLittleEndian(point.peek()) == transferableObjects.length()); point++; JSContext* cx = context(); RootedObject obj(cx); JS::StructuredCloneScope scope = output().scope(); for (auto* o : transferableObjects) { obj = o; uint32_t tag; JS::TransferableOwnership ownership; void* content; uint64_t extraData; #if DEBUG SCInput::getPair(point.peek(), &tag, (uint32_t*)&ownership); MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_PENDING_ENTRY); MOZ_ASSERT(ownership == JS::SCTAG_TMO_UNFILLED); #endif ESClass 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( cx, obj->maybeUnwrapAs()); JSAutoRealm ar(cx, arrayBuffer); if (arrayBuffer->isDetached()) { reportDataCloneError(JS_SCERR_TYPED_ARRAY_DETACHED); return false; } if (arrayBuffer->isPreparedForAsmJS()) { reportDataCloneError(JS_SCERR_WASM_NO_TRANSFER); return false; } if (scope == JS::StructuredCloneScope::DifferentProcess || scope == JS::StructuredCloneScope::DifferentProcessForIndexedDB) { // 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. 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)) { ReportOutOfMemory(cx); return false; } // Must refresh the point iterator after its collection has // been modified. point = out.iter(); point += pointOffset; if (!JS::DetachArrayBuffer(cx, arrayBuffer)) { return false; } } else { size_t nbytes = arrayBuffer->byteLength(); using BufferContents = ArrayBufferObject::BufferContents; BufferContents bufContents = ArrayBufferObject::extractStructuredCloneContents(cx, arrayBuffer); if (!bufContents) { return false; // out of memory } content = bufContents.data(); if (bufContents.kind() == ArrayBufferObject::MAPPED) { ownership = JS::SCTAG_TMO_MAPPED_DATA; } else { MOZ_ASSERT(bufContents.kind() == ArrayBufferObject::MALLOCED, "failing to handle new ArrayBuffer kind?"); ownership = JS::SCTAG_TMO_ALLOC_DATA; } extraData = nbytes; } } else { if (!out.buf.callbacks_ || !out.buf.callbacks_->writeTransfer) { return reportDataCloneError(JS_SCERR_TRANSFERABLE); } if (!out.buf.callbacks_->writeTransfer(cx, obj, out.buf.closure_, &tag, &ownership, &content, &extraData)) { return false; } MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY); } point.write(NativeEndian::swapToLittleEndian(PairToUInt64(tag, ownership))); MOZ_ALWAYS_TRUE(point.advance()); point.write( NativeEndian::swapToLittleEndian(reinterpret_cast(content))); MOZ_ALWAYS_TRUE(point.advance()); point.write(NativeEndian::swapToLittleEndian(extraData)); MOZ_ALWAYS_TRUE(point.advance()); } #if DEBUG // Make sure there aren't any more transfer map entries after the expected // number we read out. if (!point.done()) { uint32_t tag, data; SCInput::getPair(point.peek(), &tag, &data); MOZ_ASSERT(tag < SCTAG_TRANSFER_MAP_HEADER || tag >= SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES); } #endif return true; } bool JSStructuredCloneWriter::write(HandleValue v) { if (!startWrite(v)) { return false; } RootedObject obj(context()); RootedValue key(context()); RootedValue val(context()); RootedId id(context()); RootedValue cause(context()); RootedValue errors(context()); RootedValue stack(context()); while (!counts.empty()) { obj = &objs.back().toObject(); context()->check(obj); if (counts.back()) { counts.back()--; ESClass cls; if (!GetBuiltinClass(context(), obj, &cls)) { return false; } if (cls == ESClass::Map) { key = otherEntries.popCopy(); checkStack(); counts.back()--; val = otherEntries.popCopy(); checkStack(); if (!startWrite(key) || !startWrite(val)) { return false; } } else if (cls == ESClass::Set || obj->canUnwrapAs()) { key = otherEntries.popCopy(); checkStack(); if (!startWrite(key)) { return false; } } else if (cls == ESClass::Error) { cause = otherEntries.popCopy(); checkStack(); counts.back()--; errors = otherEntries.popCopy(); checkStack(); counts.back()--; stack = otherEntries.popCopy(); checkStack(); if (!startWrite(cause) || !startWrite(errors) || !startWrite(stack)) { return false; } } else { id = objectEntries.popCopy(); key = IdToValue(id); checkStack(); // If obj still has an own property named id, write it out. bool found; if (GetOwnPropertyPure(context(), obj, id, val.address(), &found)) { if (found) { if (!writePrimitive(key) || !startWrite(val)) { return false; } } continue; } if (!HasOwnProperty(context(), obj, id, &found)) { return false; } if (found) { #if FUZZING_JS_FUZZILLI // supress calls into user code if (js::SupportDifferentialTesting()) { fprintf(stderr, "Differential testing: cannot call GetProperty\n"); return false; } #endif if (!writePrimitive(key) || !GetProperty(context(), obj, obj, id, &val) || !startWrite(val)) { return false; } } } } else { if (!out.writePair(SCTAG_END_OF_KEYS, 0)) { return false; } objs.popBack(); counts.popBack(); } } memory.clear(); return transferOwnership(); } template JSString* JSStructuredCloneReader::readStringImpl(uint32_t nchars, gc::Heap heap) { if (nchars > JSString::MAX_LENGTH) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "string length"); return nullptr; } InlineCharBuffer chars; if (!chars.maybeAlloc(context(), nchars) || !in.readChars(chars.get(), nchars)) { return nullptr; } return chars.toStringDontDeflate(context(), nchars, heap); } JSString* JSStructuredCloneReader::readString(uint32_t data, gc::Heap heap) { uint32_t nchars = data & BitMask(31); bool latin1 = data & (1 << 31); return latin1 ? readStringImpl(nchars, heap) : readStringImpl(nchars, heap); } [[nodiscard]] bool JSStructuredCloneReader::readUint32(uint32_t* num) { Rooted lineVal(context()); if (!startRead(&lineVal)) { return false; } if (!lineVal.isInt32()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "integer required"); return false; } *num = uint32_t(lineVal.toInt32()); return true; } BigInt* JSStructuredCloneReader::readBigInt(uint32_t data) { size_t length = data & BitMask(31); bool isNegative = data & (1 << 31); if (length == 0) { return BigInt::zero(context()); } RootedBigInt result( context(), BigInt::createUninitialized(context(), length, isNegative)); if (!result) { return nullptr; } if (!in.readArray(result->digits().data(), length)) { return nullptr; } return result; } static uint32_t TagToV1ArrayType(uint32_t tag) { MOZ_ASSERT(tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX); return tag - SCTAG_TYPED_ARRAY_V1_MIN; } bool JSStructuredCloneReader::readTypedArray(uint32_t arrayType, uint64_t nelems, MutableHandleValue vp, bool v1Read) { if (arrayType > (v1Read ? Scalar::Uint8Clamped : Scalar::BigUint64)) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unhandled typed array element type"); return false; } // Push a placeholder onto the allObjs list to stand in for the typed array. uint32_t placeholderIndex = allObjs.length(); Value dummy = UndefinedValue(); if (!allObjs.append(dummy)) { return false; } // Read the ArrayBuffer object and its contents (but no properties) RootedValue v(context()); uint64_t byteOffset; if (v1Read) { if (!readV1ArrayBuffer(arrayType, nelems, &v)) { return false; } byteOffset = 0; } else { if (!startRead(&v)) { return false; } if (!in.read(&byteOffset)) { return false; } } // Ensure invalid 64-bit values won't be truncated below. if (nelems > ArrayBufferObject::MaxByteLength || byteOffset > ArrayBufferObject::MaxByteLength) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid typed array length or offset"); return false; } if (!v.isObject() || !v.toObject().is()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "typed array must be backed by an ArrayBuffer"); return false; } RootedObject buffer(context(), &v.toObject()); RootedObject obj(context(), nullptr); switch (arrayType) { #define CREATE_FROM_BUFFER(ExternalType, NativeType, Name) \ case Scalar::Name: \ obj = JS::TypedArray::fromBuffer(context(), buffer, \ byteOffset, nelems) \ .asObject(); \ break; JS_FOR_EACH_TYPED_ARRAY(CREATE_FROM_BUFFER) #undef CREATE_FROM_BUFFER default: MOZ_CRASH("Can't happen: arrayType range checked above"); } if (!obj) { return false; } vp.setObject(*obj); allObjs[placeholderIndex].set(vp); return true; } bool JSStructuredCloneReader::readDataView(uint64_t byteLength, MutableHandleValue vp) { // Push a placeholder onto the allObjs list to stand in for the DataView. uint32_t placeholderIndex = allObjs.length(); Value dummy = UndefinedValue(); if (!allObjs.append(dummy)) { return false; } // Read the ArrayBuffer object and its contents (but no properties). RootedValue v(context()); if (!startRead(&v)) { return false; } if (!v.isObject() || !v.toObject().is()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "DataView must be backed by an ArrayBuffer"); return false; } // Read byteOffset. uint64_t byteOffset; if (!in.read(&byteOffset)) { return false; } // Ensure invalid 64-bit values won't be truncated below. if (byteLength > ArrayBufferObject::MaxByteLength || byteOffset > ArrayBufferObject::MaxByteLength) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid DataView length or offset"); return false; } RootedObject buffer(context(), &v.toObject()); RootedObject obj(context(), JS_NewDataView(context(), buffer, byteOffset, byteLength)); if (!obj) { return false; } vp.setObject(*obj); allObjs[placeholderIndex].set(vp); return true; } bool JSStructuredCloneReader::readArrayBuffer(StructuredDataType type, uint32_t data, MutableHandleValue vp) { // V2 stores the length in |data|. The current version stores the // length separately to allow larger length values. uint64_t nbytes = 0; if (type == SCTAG_ARRAY_BUFFER_OBJECT) { if (!in.read(&nbytes)) { return false; } } else { MOZ_ASSERT(type == SCTAG_ARRAY_BUFFER_OBJECT_V2); nbytes = data; } // The maximum ArrayBuffer size depends on the platform, and we cast to size_t // below, so we have to check this here. if (nbytes > ArrayBufferObject::MaxByteLength) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } JSObject* obj = ArrayBufferObject::createZeroed(context(), size_t(nbytes)); if (!obj) { return false; } vp.setObject(*obj); ArrayBufferObject& buffer = obj->as(); MOZ_ASSERT(buffer.byteLength() == nbytes); return in.readArray(buffer.dataPointer(), nbytes); } bool JSStructuredCloneReader::readSharedArrayBuffer(MutableHandleValue vp) { if (!cloneDataPolicy.areIntraClusterClonableSharedObjectsAllowed() || !cloneDataPolicy.areSharedMemoryObjectsAllowed()) { auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled() ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP : JS_SCERR_NOT_CLONABLE; ReportDataCloneError(context(), callbacks, error, closure, "SharedArrayBuffer"); return false; } uint64_t byteLength; if (!in.readBytes(&byteLength, sizeof(byteLength))) { return in.reportTruncated(); } // The maximum ArrayBuffer size depends on the platform, and we cast to size_t // below, so we have to check this here. if (byteLength > ArrayBufferObject::MaxByteLength) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_BAD_ARRAY_LENGTH); return false; } intptr_t p; if (!in.readBytes(&p, sizeof(p))) { return in.reportTruncated(); } SharedArrayRawBuffer* rawbuf = reinterpret_cast(p); // There's no guarantee that the receiving agent has enabled shared memory // even if the transmitting agent has done so. Ideally we'd check at the // transmission point, but that's tricky, and it will be a very rare problem // in any case. Just fail at the receiving end if we can't handle it. if (!context() ->realm() ->creationOptions() .getSharedMemoryAndAtomicsEnabled()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_SAB_DISABLED); return false; } // The new object will have a new reference to the rawbuf. if (!rawbuf->addReference()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_SAB_REFCNT_OFLO); return false; } RootedObject obj(context(), SharedArrayBufferObject::New(context(), rawbuf, byteLength)); if (!obj) { rawbuf->dropReference(); return false; } // `rawbuf` is now owned by `obj`. if (callbacks && callbacks->sabCloned && !callbacks->sabCloned(context(), /*receiving=*/true, closure)) { return false; } vp.setObject(*obj); return true; } bool JSStructuredCloneReader::readSharedWasmMemory(uint32_t nbytes, MutableHandleValue vp) { JSContext* cx = context(); if (nbytes != 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid shared wasm memory tag"); return false; } if (!cloneDataPolicy.areIntraClusterClonableSharedObjectsAllowed() || !cloneDataPolicy.areSharedMemoryObjectsAllowed()) { auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled() ? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP : JS_SCERR_NOT_CLONABLE; ReportDataCloneError(cx, callbacks, error, closure, "WebAssembly.Memory"); return false; } // Read the isHuge flag RootedValue isHuge(cx); if (!startRead(&isHuge)) { return false; } // Read the SharedArrayBuffer object. RootedValue payload(cx); if (!startRead(&payload)) { return false; } if (!payload.isObject() || !payload.toObject().is()) { JS_ReportErrorNumberASCII( context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "shared wasm memory must be backed by a SharedArrayBuffer"); return false; } Rooted sab( cx, &payload.toObject().as()); // Construct the memory. RootedObject proto(cx, &cx->global()->getPrototype(JSProto_WasmMemory)); RootedObject memory( cx, WasmMemoryObject::create(cx, sab, isHuge.toBoolean(), proto)); if (!memory) { return false; } vp.setObject(*memory); return true; } /* * Read in the data for a structured clone version 1 ArrayBuffer, performing * endianness-conversion while reading. */ bool JSStructuredCloneReader::readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp) { if (arrayType > Scalar::Uint8Clamped) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid TypedArray type"); return false; } mozilla::CheckedInt nbytes = mozilla::CheckedInt(nelems) * TypedArrayElemSize(static_cast(arrayType)); if (!nbytes.isValid() || nbytes.value() > UINT32_MAX) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid typed array size"); return false; } JSObject* obj = ArrayBufferObject::createZeroed(context(), nbytes.value()); if (!obj) { return false; } vp.setObject(*obj); ArrayBufferObject& buffer = obj->as(); MOZ_ASSERT(buffer.byteLength() == nbytes); switch (arrayType) { case Scalar::Int8: case Scalar::Uint8: case Scalar::Uint8Clamped: return in.readArray((uint8_t*)buffer.dataPointer(), nelems); case Scalar::Int16: case Scalar::Uint16: return in.readArray((uint16_t*)buffer.dataPointer(), nelems); case Scalar::Int32: case Scalar::Uint32: case Scalar::Float32: return in.readArray((uint32_t*)buffer.dataPointer(), nelems); case Scalar::Float64: case Scalar::BigInt64: case Scalar::BigUint64: return in.readArray((uint64_t*)buffer.dataPointer(), nelems); default: MOZ_CRASH("Can't happen: arrayType range checked by caller"); } } static bool PrimitiveToObject(JSContext* cx, MutableHandleValue vp) { JSObject* obj = js::PrimitiveToObject(cx, vp); if (!obj) { return false; } vp.setObject(*obj); return true; } bool JSStructuredCloneReader::startRead(MutableHandleValue vp, gc::Heap strHeap) { uint32_t tag, data; bool alreadAppended = false; if (!in.readPair(&tag, &data)) { return false; } numItemsRead++; switch (tag) { case SCTAG_NULL: vp.setNull(); break; case SCTAG_UNDEFINED: vp.setUndefined(); break; case SCTAG_INT32: vp.setInt32(data); break; case SCTAG_BOOLEAN: case SCTAG_BOOLEAN_OBJECT: vp.setBoolean(!!data); if (tag == SCTAG_BOOLEAN_OBJECT && !PrimitiveToObject(context(), vp)) { return false; } break; case SCTAG_STRING: case SCTAG_STRING_OBJECT: { JSString* str = readString(data, strHeap); if (!str) { return false; } vp.setString(str); if (tag == SCTAG_STRING_OBJECT && !PrimitiveToObject(context(), vp)) { return false; } break; } case SCTAG_NUMBER_OBJECT: { double d; if (!in.readDouble(&d)) { return false; } vp.setDouble(CanonicalizeNaN(d)); if (!PrimitiveToObject(context(), vp)) { return false; } break; } case SCTAG_BIGINT: case SCTAG_BIGINT_OBJECT: { RootedBigInt bi(context(), readBigInt(data)); if (!bi) { return false; } vp.setBigInt(bi); if (tag == SCTAG_BIGINT_OBJECT && !PrimitiveToObject(context(), vp)) { return false; } break; } case SCTAG_DATE_OBJECT: { double d; if (!in.readDouble(&d)) { return false; } JS::ClippedTime t = JS::TimeClip(d); if (!NumbersAreIdentical(d, t.toDouble())) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "date"); return false; } JSObject* obj = NewDateObjectMsec(context(), t); if (!obj) { return false; } vp.setObject(*obj); break; } case SCTAG_REGEXP_OBJECT: { if ((data & RegExpFlag::AllFlags) != data) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "regexp"); return false; } RegExpFlags flags(AssertedCast(data)); uint32_t tag2, stringData; if (!in.readPair(&tag2, &stringData)) { return false; } if (tag2 != SCTAG_STRING) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "regexp"); return false; } JSString* str = readString(stringData, gc::Heap::Tenured); if (!str) { return false; } Rooted atom(context(), AtomizeString(context(), str)); if (!atom) { return false; } RegExpObject* reobj = RegExpObject::create(context(), atom, flags, GenericObject); if (!reobj) { return false; } vp.setObject(*reobj); break; } case SCTAG_ARRAY_OBJECT: case SCTAG_OBJECT_OBJECT: { JSObject* obj = (tag == SCTAG_ARRAY_OBJECT) ? (JSObject*)NewDenseUnallocatedArray( context(), NativeEndian::swapFromLittleEndian(data)) : (JSObject*)NewPlainObject(context()); if (!obj || !objs.append(ObjectValue(*obj))) { return false; } vp.setObject(*obj); break; } case SCTAG_BACK_REFERENCE_OBJECT: { if (data >= allObjs.length() || !allObjs[data].isObject()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid back reference in input"); return false; } vp.set(allObjs[data]); return true; } case SCTAG_TRANSFER_MAP_HEADER: case SCTAG_TRANSFER_MAP_PENDING_ENTRY: // We should be past all the transfer map tags. JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid input"); return false; case SCTAG_ARRAY_BUFFER_OBJECT_V2: case SCTAG_ARRAY_BUFFER_OBJECT: if (!readArrayBuffer(StructuredDataType(tag), data, vp)) { return false; } break; case SCTAG_SHARED_ARRAY_BUFFER_OBJECT: if (!readSharedArrayBuffer(vp)) { return false; } break; case SCTAG_SHARED_WASM_MEMORY_OBJECT: if (!readSharedWasmMemory(data, vp)) { return false; } break; case SCTAG_TYPED_ARRAY_OBJECT_V2: { // readTypedArray adds the array to allObjs. // V2 stores the length (nelems) in |data| and the arrayType separately. uint64_t arrayType; if (!in.read(&arrayType)) { return false; } uint64_t nelems = data; return readTypedArray(arrayType, nelems, vp); } case SCTAG_TYPED_ARRAY_OBJECT: { // readTypedArray adds the array to allObjs. // The current version stores the array type in |data| and the length // (nelems) separately to support large TypedArrays. uint32_t arrayType = data; uint64_t nelems; if (!in.read(&nelems)) { return false; } return readTypedArray(arrayType, nelems, vp); } case SCTAG_DATA_VIEW_OBJECT_V2: { // readDataView adds the array to allObjs. uint64_t byteLength = data; return readDataView(byteLength, vp); } case SCTAG_DATA_VIEW_OBJECT: { // readDataView adds the array to allObjs. uint64_t byteLength; if (!in.read(&byteLength)) { return false; } return readDataView(byteLength, vp); } case SCTAG_MAP_OBJECT: { JSObject* obj = MapObject::create(context()); if (!obj || !objs.append(ObjectValue(*obj))) { return false; } vp.setObject(*obj); break; } case SCTAG_SET_OBJECT: { JSObject* obj = SetObject::create(context()); if (!obj || !objs.append(ObjectValue(*obj))) { return false; } vp.setObject(*obj); break; } case SCTAG_SAVED_FRAME_OBJECT: { auto* obj = readSavedFrameHeader(data); if (!obj || !objs.append(ObjectValue(*obj)) || !objState.append(std::make_pair(obj, false))) { return false; } vp.setObject(*obj); break; } case SCTAG_ERROR_OBJECT: { auto* obj = readErrorHeader(data); if (!obj || !objs.append(ObjectValue(*obj)) || !objState.append(std::make_pair(obj, false))) { return false; } vp.setObject(*obj); break; } case SCTAG_END_OF_KEYS: JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "truncated input"); return false; break; default: { if (tag <= SCTAG_FLOAT_MAX) { double d = ReinterpretPairAsDouble(tag, data); vp.setNumber(CanonicalizeNaN(d)); break; } if (SCTAG_TYPED_ARRAY_V1_MIN <= tag && tag <= SCTAG_TYPED_ARRAY_V1_MAX) { // A v1-format typed array // readTypedArray adds the array to allObjs return readTypedArray(TagToV1ArrayType(tag), data, vp, true); } if (!callbacks || !callbacks->read) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unsupported type"); return false; } // callbacks->read() might read other objects from the buffer. // In startWrite we always write the object itself before calling // the custom function. We should do the same here to keep // indexing consistent. uint32_t placeholderIndex = allObjs.length(); Value dummy = UndefinedValue(); if (!allObjs.append(dummy)) { return false; } JSObject* obj = callbacks->read(context(), this, cloneDataPolicy, tag, data, closure); if (!obj) { return false; } vp.setObject(*obj); allObjs[placeholderIndex].set(vp); alreadAppended = true; } } if (!alreadAppended && vp.isObject() && !allObjs.append(vp)) { return false; } return true; } bool JSStructuredCloneReader::readHeader() { uint32_t tag, data; if (!in.getPair(&tag, &data)) { return in.reportTruncated(); } JS::StructuredCloneScope storedScope; if (tag == SCTAG_HEADER) { MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); storedScope = JS::StructuredCloneScope(data); } else { // Old structured clone buffer. We must have read it from disk. storedScope = JS::StructuredCloneScope::DifferentProcessForIndexedDB; } // Backward compatibility with old structured clone buffers. Value '0' was // used for SameProcessSameThread scope. if ((int)storedScope == 0) { storedScope = JS::StructuredCloneScope::SameProcess; } if (storedScope < JS::StructuredCloneScope::SameProcess || storedScope > JS::StructuredCloneScope::DifferentProcessForIndexedDB) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid structured clone scope"); return false; } if (allowedScope == JS::StructuredCloneScope::DifferentProcessForIndexedDB) { // Bug 1434308 and bug 1458320 - the scopes stored in old IndexedDB // clones are incorrect. Treat them as if they were DifferentProcess. allowedScope = JS::StructuredCloneScope::DifferentProcess; return true; } if (storedScope < allowedScope) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "incompatible structured clone scope"); return false; } return true; } bool JSStructuredCloneReader::readTransferMap() { JSContext* cx = context(); auto headerPos = in.tell(); uint32_t tag, data; if (!in.getPair(&tag, &data)) { return in.reportTruncated(); } if (tag != SCTAG_TRANSFER_MAP_HEADER || TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED) { return true; } uint64_t numTransferables; MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); if (!in.read(&numTransferables)) { return false; } for (uint64_t i = 0; i < numTransferables; i++) { auto pos = in.tell(); if (!in.readPair(&tag, &data)) { return false; } if (tag == SCTAG_TRANSFER_MAP_PENDING_ENTRY) { ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure); return false; } RootedObject obj(cx); void* content; if (!in.readPtr(&content)) { return false; } uint64_t extraData; if (!in.read(&extraData)) { return false; } if (tag == SCTAG_TRANSFER_MAP_ARRAY_BUFFER) { if (allowedScope == JS::StructuredCloneScope::DifferentProcess || allowedScope == JS::StructuredCloneScope::DifferentProcessForIndexedDB) { // Transferred ArrayBuffers in a DifferentProcess clone buffer // are treated as if they weren't Transferred at all. We should // only see SCTAG_TRANSFER_MAP_STORED_ARRAY_BUFFER. ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure); return false; } MOZ_RELEASE_ASSERT(extraData <= ArrayBufferObject::MaxByteLength); size_t nbytes = extraData; MOZ_ASSERT(data == JS::SCTAG_TMO_ALLOC_DATA || data == JS::SCTAG_TMO_MAPPED_DATA); if (data == JS::SCTAG_TMO_ALLOC_DATA) { 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); if (!in.seekBy(static_cast(extraData))) { return false; } if (tailStartPos.isNothing()) { tailStartPos = mozilla::Some(in.tell()); } uint32_t tag, data; if (!in.readPair(&tag, &data)) { return false; } if (tag != SCTAG_ARRAY_BUFFER_OBJECT_V2 && tag != SCTAG_ARRAY_BUFFER_OBJECT) { ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure); return false; } RootedValue val(cx); if (!readArrayBuffer(StructuredDataType(tag), data, &val)) { return false; } obj = &val.toObject(); tailEndPos = mozilla::Some(in.tell()); } else { if (!callbacks || !callbacks->readTransfer) { ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure); return false; } if (!callbacks->readTransfer(cx, this, tag, content, extraData, closure, &obj)) { if (!cx->isExceptionPending()) { ReportDataCloneError(cx, callbacks, JS_SCERR_TRANSFERABLE, closure); } return false; } MOZ_ASSERT(obj); MOZ_ASSERT(!cx->isExceptionPending()); } // On failure, the buffer will still own the data (since its ownership // will not get set to SCTAG_TMO_UNOWNED), so the data will be freed by // DiscardTransferables. if (!obj) { return false; } // Mark the SCTAG_TRANSFER_MAP_* entry as no longer owned by the input // buffer. pos.write(PairToUInt64(tag, JS::SCTAG_TMO_UNOWNED)); MOZ_ASSERT(!pos.done()); if (!allObjs.append(ObjectValue(*obj))) { return false; } } // Mark the whole transfer map as consumed. #ifdef DEBUG SCInput::getPair(headerPos.peek(), &tag, &data); MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_HEADER); MOZ_ASSERT(TransferableMapHeader(data) != SCTAG_TM_TRANSFERRED); #endif headerPos.write( PairToUInt64(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_TRANSFERRED)); return true; } JSObject* JSStructuredCloneReader::readSavedFrameHeader( uint32_t principalsTag) { Rooted savedFrame(context(), SavedFrame::create(context())); if (!savedFrame) { return nullptr; } JSPrincipals* principals; if (principalsTag == SCTAG_JSPRINCIPALS) { if (!context()->runtime()->readPrincipals) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_UNSUPPORTED_TYPE); return nullptr; } if (!context()->runtime()->readPrincipals(context(), this, &principals)) { return nullptr; } } else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM) { principals = &ReconstructedSavedFramePrincipals::IsSystem; principals->refcount++; } else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM) { principals = &ReconstructedSavedFramePrincipals::IsNotSystem; principals->refcount++; } else if (principalsTag == SCTAG_NULL_JSPRINCIPALS) { principals = nullptr; } else { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "bad SavedFrame principals"); return nullptr; } RootedValue mutedErrors(context()); RootedValue source(context()); { // Read a |mutedErrors| boolean followed by a |source| string. // The |mutedErrors| boolean is present in all new structured-clone data, // but in older data it will be absent and only the |source| string will be // found. if (!startRead(&mutedErrors)) { return nullptr; } if (mutedErrors.isBoolean()) { if (!startRead(&source, gc::Heap::Tenured) || !source.isString()) { return nullptr; } } else if (mutedErrors.isString()) { // Backwards compatibility: Handle missing |mutedErrors| boolean, // this is actually just a |source| string. source = mutedErrors; mutedErrors.setBoolean(true); // Safe default value. } else { // Invalid type. return nullptr; } } savedFrame->initPrincipalsAlreadyHeldAndMutedErrors(principals, mutedErrors.toBoolean()); auto atomSource = AtomizeString(context(), source.toString()); if (!atomSource) { return nullptr; } savedFrame->initSource(atomSource); RootedValue lineVal(context()); uint32_t line; if (!readUint32(&line)) { return nullptr; } savedFrame->initLine(line); RootedValue columnVal(context()); uint32_t column; if (!readUint32(&column)) { return nullptr; } savedFrame->initColumn(column); // Don't specify a source ID when reading a cloned saved frame, as these IDs // are only valid within a specific process. savedFrame->initSourceId(0); RootedValue name(context()); if (!startRead(&name, gc::Heap::Tenured)) { return nullptr; } if (!(name.isString() || name.isNull())) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid saved frame cause"); return nullptr; } JSAtom* atomName = nullptr; if (name.isString()) { atomName = AtomizeString(context(), name.toString()); if (!atomName) { return nullptr; } } savedFrame->initFunctionDisplayName(atomName); RootedValue cause(context()); if (!startRead(&cause, gc::Heap::Tenured)) { return nullptr; } if (!(cause.isString() || cause.isNull())) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid saved frame cause"); return nullptr; } JSAtom* atomCause = nullptr; if (cause.isString()) { atomCause = AtomizeString(context(), cause.toString()); if (!atomCause) { return nullptr; } } savedFrame->initAsyncCause(atomCause); return savedFrame; } // SavedFrame object: there is one child value, the parent SavedFrame, // which is either null or another SavedFrame object. bool JSStructuredCloneReader::readSavedFrameFields(Handle frameObj, HandleValue parent, bool* state) { if (*state) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "multiple SavedFrame parents"); return false; } SavedFrame* parentFrame; if (parent.isNull()) { parentFrame = nullptr; } else if (parent.isObject() && parent.toObject().is()) { parentFrame = &parent.toObject().as(); } else { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid SavedFrame parent"); return false; } frameObj->initParent(parentFrame); *state = true; return true; } JSObject* JSStructuredCloneReader::readErrorHeader(uint32_t type) { JSContext* cx = context(); switch (type) { case JSEXN_ERR: case JSEXN_EVALERR: case JSEXN_RANGEERR: case JSEXN_REFERENCEERR: case JSEXN_SYNTAXERR: case JSEXN_TYPEERR: case JSEXN_URIERR: case JSEXN_AGGREGATEERR: break; default: JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid error type"); return nullptr; } RootedString message(cx); { RootedValue messageVal(cx); if (!startRead(&messageVal)) { return nullptr; } if (messageVal.isString()) { message = messageVal.toString(); } else if (!messageVal.isNull()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid 'message' field for Error object"); return nullptr; } } // We have to set |cause| to something if it exists, otherwise the shape // would be wrong. The actual value will be overwritten later. RootedValue val(cx); if (!startRead(&val)) { return nullptr; } bool hasCause = ToBoolean(val); Rooted> cause(cx, mozilla::Nothing()); if (hasCause) { cause = mozilla::Some(BooleanValue(true)); } if (!startRead(&val)) { return nullptr; } if (!val.isString()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid 'fileName' field for Error object"); return nullptr; } RootedString fileName(cx, val.toString()); uint32_t lineNumber, columnNumber; if (!readUint32(&lineNumber) || !readUint32(&columnNumber)) { return nullptr; } // The |cause| and |stack| slots of the objects might be overwritten later. // For AggregateErrors the |errors| property will be added. RootedObject errorObj( cx, ErrorObject::create(cx, static_cast(type), nullptr, fileName, 0, lineNumber, columnNumber, nullptr, message, cause)); if (!errorObj) { return nullptr; } return errorObj; } // Error objects have 3 fields, some or all of them null: cause, // errors, and stack. bool JSStructuredCloneReader::readErrorFields(Handle errorObj, HandleValue cause, bool* state) { JSContext* cx = context(); if (*state) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unexpected child value seen for Error object"); return false; } RootedValue errors(cx); RootedValue stack(cx); if (!startRead(&errors) || !startRead(&stack)) { return false; } bool hasCause = errorObj->getCause().isSome(); if (hasCause) { errorObj->setCauseSlot(cause); } else if (!cause.isNull()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid 'cause' field for Error object"); return false; } if (errorObj->type() == JSEXN_AGGREGATEERR) { if (!DefineDataProperty(context(), errorObj, cx->names().errors, errors, 0)) { return false; } } else if (!errors.isNull()) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "unexpected 'errors' field seen for non-AggregateError"); return false; } if (stack.isObject()) { RootedObject stackObj(cx, &stack.toObject()); if (!stackObj->is()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid 'stack' field for Error object"); return false; } errorObj->setStackSlot(stack); } else if (!stack.isNull()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "invalid 'stack' field for Error object"); return false; } *state = true; return true; } // Read a value and treat as a key,value pair. bool JSStructuredCloneReader::readMapField(Handle mapObj, HandleValue key) { RootedValue val(context()); if (!startRead(&val)) { return false; } return MapObject::set(context(), mapObj, key, val); } // Read a value and treat as a key,value pair. Interpret as a plain property // value. bool JSStructuredCloneReader::readObjectField(HandleObject obj, HandleValue key) { RootedValue val(context()); if (!startRead(&val)) { return false; } if (!key.isString() && !key.isInt32()) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "property key expected"); return false; } RootedId id(context()); if (!PrimitiveValueToId(context(), key, &id)) { return false; } // Fast path for adding a new property to a plain object. The property names // we see here should be unique, but we check for duplicates to guard against // corrupt or malicious data. if (id.isString() && obj->is() && MOZ_LIKELY(!obj->as().contains(context(), id))) { return AddDataPropertyToPlainObject(context(), obj.as(), id, val); } // Fast path for adding an array element. The index shouldn't exceed the // array's length, but we check for this in `addDenseElementNoLengthChange` to // guard against corrupt or malicious data. if (id.isInt() && obj->is()) { ArrayObject* arr = &obj->as(); switch (arr->addDenseElementNoLengthChange(context(), id.toInt(), val)) { case DenseElementResult::Failure: return false; case DenseElementResult::Success: return true; case DenseElementResult::Incomplete: // Fall-through to slow path. break; } } return DefineDataProperty(context(), obj, id, val); } // Perform the whole recursive reading procedure. bool JSStructuredCloneReader::read(MutableHandleValue vp, size_t nbytes) { auto startTime = mozilla::TimeStamp::Now(); if (!readHeader()) { return false; } if (!readTransferMap()) { return false; } MOZ_ASSERT(objs.length() == 0); MOZ_ASSERT(objState.length() == 1); // Start out by reading in the main object and pushing it onto the 'objs' // stack. The data related to this object and its descendants extends from // here to the SCTAG_END_OF_KEYS at the end of the stream. if (!startRead(vp)) { return false; } // Stop when the stack shows that all objects have been read. while (objs.length() != 0) { // What happens depends on the top obj on the objs stack. RootedObject obj(context(), &objs.back().toObject()); uint32_t tag, data; if (!in.getPair(&tag, &data)) { return false; } if (tag == SCTAG_END_OF_KEYS) { // Pop the current obj off the stack, since we are done with it and // its children. MOZ_ALWAYS_TRUE(in.readPair(&tag, &data)); objs.popBack(); if (objState.back().first == obj) { objState.popBack(); } continue; } // Remember the index of the current top of the state stack, which will // correspond to the state for `obj` iff `obj` is a type that uses state. // startRead() may push additional entries before the state is accessed and // updated while filling in the object's data. size_t objStateIdx = objState.length() - 1; // The input stream contains a sequence of "child" values, whose // interpretation depends on the type of obj. These values can be // anything, and startRead() will push onto 'objs' for any non-leaf // value (i.e., anything that may contain children). // // startRead() will allocate the (empty) object, but note that when // startRead() returns, 'key' is not yet initialized with any of its // properties. Those will be filled in by returning to the head of this // loop, processing the first child obj, and continuing until all // children have been fully created. // // Note that this means the ordering in the stream is a little funky for // things like Map. See the comment above traverseMap() for an example. RootedValue key(context()); if (!startRead(&key)) { return false; } if (key.isNull() && !(obj->is() || obj->is() || obj->is() || obj->is())) { // Backwards compatibility: Null formerly indicated the end of // object properties. // No legacy objects used the state stack. MOZ_ASSERT(objState[objStateIdx].first() != obj); objs.popBack(); continue; } context()->check(key); if (obj->is()) { // Set object: the values between obj header (from startRead()) and // SCTAG_END_OF_KEYS are all interpreted as values to add to the set. if (!SetObject::add(context(), obj, key)) { return false; } } else if (obj->is()) { Rooted mapObj(context(), &obj->as()); if (!readMapField(mapObj, key)) { return false; } } else if (obj->is()) { Rooted frameObj(context(), &obj->as()); MOZ_ASSERT(objState[objStateIdx].first() == obj); bool state = objState[objStateIdx].second(); if (!readSavedFrameFields(frameObj, key, &state)) { return false; } objState[objStateIdx].second() = state; } else if (obj->is()) { Rooted errorObj(context(), &obj->as()); MOZ_ASSERT(objState[objStateIdx].first() == obj); bool state = objState[objStateIdx].second(); if (!readErrorFields(errorObj, key, &state)) { return false; } objState[objStateIdx].second() = state; } else { // Everything else uses a series of key,value,key,value,... Value // objects. if (!readObjectField(obj, key)) { return false; } } } allObjs.clear(); // For fuzzing, it is convenient to allow extra data at the end // of the input buffer so that more possible inputs are considered // valid. #ifndef FUZZING bool extraData; if (tailStartPos.isSome()) { // in.tell() is the end of the main data. If "tail" data was consumed, // then check whether there's any data between the main data and the // beginning of the tail, or after the last read point in the tail. extraData = (in.tell() != *tailStartPos || !tailEndPos->done()); } else { extraData = !in.tell().done(); } if (extraData) { JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "extra data after end"); return false; } #endif JSRuntime* rt = context()->runtime(); rt->metrics().DESERIALIZE_BYTES(nbytes); rt->metrics().DESERIALIZE_ITEMS(numItemsRead); mozilla::TimeDuration elapsed = mozilla::TimeStamp::Now() - startTime; rt->metrics().DESERIALIZE_US(elapsed); return true; } JS_PUBLIC_API bool JS_ReadStructuredClone( JSContext* cx, const JSStructuredCloneData& buf, uint32_t version, JS::StructuredCloneScope scope, MutableHandleValue vp, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { AssertHeapIsIdle(); CHECK_THREAD(cx); if (version > JS_STRUCTURED_CLONE_VERSION) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_BAD_CLONE_VERSION); return false; } const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; return ReadStructuredClone(cx, buf, scope, vp, cloneDataPolicy, callbacks, closure); } JS_PUBLIC_API bool JS_WriteStructuredClone( JSContext* cx, HandleValue value, JSStructuredCloneData* bufp, JS::StructuredCloneScope scope, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure, HandleValue transferable) { AssertHeapIsIdle(); CHECK_THREAD(cx); cx->check(value); const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; return WriteStructuredClone(cx, value, bufp, scope, cloneDataPolicy, callbacks, closure, transferable); } JS_PUBLIC_API bool JS_StructuredCloneHasTransferables( JSStructuredCloneData& data, bool* hasTransferable) { *hasTransferable = StructuredCloneHasTransferObjects(data); return true; } JS_PUBLIC_API bool JS_StructuredClone( JSContext* cx, HandleValue value, MutableHandleValue vp, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { AssertHeapIsIdle(); CHECK_THREAD(cx); // Strings are associated with zones, not compartments, // so we copy the string by wrapping it. if (value.isString()) { RootedString strValue(cx, value.toString()); if (!cx->compartment()->wrap(cx, &strValue)) { return false; } vp.setString(strValue); return true; } const JSStructuredCloneCallbacks* callbacks = optionalCallbacks; JSAutoStructuredCloneBuffer buf(JS::StructuredCloneScope::SameProcess, callbacks, closure); { if (value.isObject()) { RootedObject obj(cx, &value.toObject()); obj = CheckedUnwrapStatic(obj); if (!obj) { ReportAccessDenied(cx); return false; } AutoRealm ar(cx, obj); RootedValue unwrappedVal(cx, ObjectValue(*obj)); if (!buf.write(cx, unwrappedVal, callbacks, closure)) { return false; } } else { if (!buf.write(cx, value, callbacks, closure)) { return false; } } } return buf.read(cx, vp, JS::CloneDataPolicy(), callbacks, closure); } JSAutoStructuredCloneBuffer::JSAutoStructuredCloneBuffer( JSAutoStructuredCloneBuffer&& other) : data_(other.scope()) { version_ = other.version_; other.giveTo(&data_); } JSAutoStructuredCloneBuffer& JSAutoStructuredCloneBuffer::operator=( JSAutoStructuredCloneBuffer&& other) { MOZ_ASSERT(&other != this); MOZ_ASSERT(scope() == other.scope()); clear(); version_ = other.version_; other.giveTo(&data_); return *this; } void JSAutoStructuredCloneBuffer::clear() { data_.discardTransferables(); data_.ownTransferables_ = OwnTransferablePolicy::NoTransferables; data_.refsHeld_.releaseAll(); data_.Clear(); version_ = 0; } void JSAutoStructuredCloneBuffer::adopt( JSStructuredCloneData&& data, uint32_t version, const JSStructuredCloneCallbacks* callbacks, void* closure) { clear(); data_ = std::move(data); version_ = version; data_.setCallbacks(callbacks, closure, OwnTransferablePolicy::OwnsTransferablesIfAny); } void JSAutoStructuredCloneBuffer::giveTo(JSStructuredCloneData* data) { *data = std::move(data_); version_ = 0; data_.setCallbacks(nullptr, nullptr, OwnTransferablePolicy::NoTransferables); data_.Clear(); } bool JSAutoStructuredCloneBuffer::read( JSContext* cx, MutableHandleValue vp, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { MOZ_ASSERT(cx); return !!JS_ReadStructuredClone( cx, data_, version_, data_.scope(), vp, cloneDataPolicy, optionalCallbacks ? optionalCallbacks : data_.callbacks_, optionalCallbacks ? closure : data_.closure_); } bool JSAutoStructuredCloneBuffer::write( JSContext* cx, HandleValue value, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { HandleValue transferable = UndefinedHandleValue; return write(cx, value, transferable, JS::CloneDataPolicy(), optionalCallbacks ? optionalCallbacks : data_.callbacks_, optionalCallbacks ? closure : data_.closure_); } bool JSAutoStructuredCloneBuffer::write( JSContext* cx, HandleValue value, HandleValue transferable, const JS::CloneDataPolicy& cloneDataPolicy, const JSStructuredCloneCallbacks* optionalCallbacks, void* closure) { clear(); bool ok = JS_WriteStructuredClone( cx, value, &data_, data_.scopeForInternalWriting(), cloneDataPolicy, optionalCallbacks ? optionalCallbacks : data_.callbacks_, optionalCallbacks ? closure : data_.closure_, transferable); if (!ok) { version_ = JS_STRUCTURED_CLONE_VERSION; } return ok; } JS_PUBLIC_API bool JS_ReadUint32Pair(JSStructuredCloneReader* r, uint32_t* p1, uint32_t* p2) { return r->input().readPair((uint32_t*)p1, (uint32_t*)p2); } JS_PUBLIC_API bool JS_ReadBytes(JSStructuredCloneReader* r, void* p, size_t len) { return r->input().readBytes(p, len); } JS_PUBLIC_API bool JS_ReadString(JSStructuredCloneReader* r, MutableHandleString str) { uint32_t tag, data; if (!r->input().readPair(&tag, &data)) { return false; } if (tag == SCTAG_STRING) { if (JSString* s = r->readString(data)) { str.set(s); return true; } return false; } JS_ReportErrorNumberASCII(r->context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "expected string"); return false; } JS_PUBLIC_API bool JS_ReadDouble(JSStructuredCloneReader* r, double* v) { return r->input().readDouble(v); } JS_PUBLIC_API bool JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp) { uint32_t tag, data; if (!r->input().readPair(&tag, &data)) { return false; } if (tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX) { return r->readTypedArray(TagToV1ArrayType(tag), data, vp, true); } if (tag == SCTAG_TYPED_ARRAY_OBJECT_V2) { // V2 stores the length (nelems) in |data| and the arrayType separately. uint64_t arrayType; if (!r->input().read(&arrayType)) { return false; } uint64_t nelems = data; return r->readTypedArray(arrayType, nelems, vp); } if (tag == SCTAG_TYPED_ARRAY_OBJECT) { // The current version stores the array type in |data| and the length // (nelems) separately to support large TypedArrays. uint32_t arrayType = data; uint64_t nelems; if (!r->input().read(&nelems)) { return false; } return r->readTypedArray(arrayType, nelems, vp); } JS_ReportErrorNumberASCII(r->context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA, "expected type array"); return false; } JS_PUBLIC_API bool JS_WriteUint32Pair(JSStructuredCloneWriter* w, uint32_t tag, uint32_t data) { return w->output().writePair(tag, data); } JS_PUBLIC_API bool JS_WriteBytes(JSStructuredCloneWriter* w, const void* p, size_t len) { return w->output().writeBytes(p, len); } JS_PUBLIC_API bool JS_WriteString(JSStructuredCloneWriter* w, HandleString str) { return w->writeString(SCTAG_STRING, str); } JS_PUBLIC_API bool JS_WriteDouble(JSStructuredCloneWriter* w, double v) { return w->output().writeDouble(v); } JS_PUBLIC_API bool JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v) { MOZ_ASSERT(v.isObject()); w->context()->check(v); RootedObject obj(w->context(), &v.toObject()); // startWrite can write everything, thus we should check here // and report error if the user passes a wrong type. if (!obj->canUnwrapAs()) { ReportAccessDenied(w->context()); return false; } // We should use startWrite instead of writeTypedArray, because // typed array is an object, we should add it to the |memory| // (allObjs) list. Directly calling writeTypedArray won't add it. return w->startWrite(v); } JS_PUBLIC_API bool JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj) { w->memory.remove(w->memory.lookup(obj)); return true; } JS_PUBLIC_API JS::StructuredCloneScope JS_GetStructuredCloneScope( JSStructuredCloneWriter* w) { return w->output().scope(); }