forked from mirrors/gecko-dev
4127 lines
125 KiB
C++
4127 lines
125 KiB
C++
/* -*- 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 <algorithm>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#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:
|
|
* <SCTAG_TRANSFER_MAP_HEADER, TransferableMapHeader(UNREAD|TRANSFERRED)>
|
|
* numTransferables (64 bits)
|
|
* array of:
|
|
* <SCTAG_TRANSFER_MAP_*, TransferableOwnership>
|
|
* 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 <typename T, typename AllocPolicy>
|
|
struct BufferIterator {
|
|
using BufferList = mozilla::BufferList<AllocPolicy>;
|
|
|
|
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<T*>(mIter.Data()) = data;
|
|
}
|
|
|
|
T peek() const {
|
|
MOZ_ASSERT(mIter.HasRoomFor(sizeof(T)));
|
|
return *reinterpret_cast<T*>(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<uint64_t, SystemAllocPolicy>;
|
|
|
|
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 <class T>
|
|
[[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<uint64_t, SystemAllocPolicy>;
|
|
|
|
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 <class T>
|
|
[[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 <typename CharT>
|
|
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<SavedFrame*> 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<ErrorObject*> errorObj,
|
|
HandleValue cause, bool* state);
|
|
|
|
[[nodiscard]] bool readMapField(Handle<MapObject*> 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
|
|
// <object, state> 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<GCVector<std::pair<HeapPtr<JSObject*>, 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<SCInput::BufferIterator> tailStartPos;
|
|
mozilla::Maybe<SCInput::BufferIterator> 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 <typename... Args>
|
|
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<size_t> 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<JSObject*, uint32_t,
|
|
StableCellHasher<JSObject*>, SystemAllocPolicy>;
|
|
Rooted<CloneMemory> memory;
|
|
|
|
// Set of transferable objects
|
|
RootedValue transferable;
|
|
using TransferableObjectsList = GCVector<JSObject*>;
|
|
Rooted<TransferableObjectsList> 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 <typename... Args>
|
|
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<Args>(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<Args>(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<uint64_t, SystemAllocPolicy> iter(data);
|
|
MOZ_ALWAYS_TRUE(iter.readBytes(reinterpret_cast<char*>(&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<double>(u));
|
|
return true;
|
|
}
|
|
|
|
template <typename T>
|
|
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 <class T>
|
|
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_t> size =
|
|
mozilla::CheckedInt<size_t>(nelems) * sizeof(T);
|
|
if (!size.isValid()) {
|
|
return reportTruncated();
|
|
}
|
|
|
|
if (!point.readBytes(reinterpret_cast<char*>(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<void*>(NativeEndian::swapFromLittleEndian(data));
|
|
}
|
|
|
|
bool SCInput::readPtr(void** p) {
|
|
uint64_t u;
|
|
if (!read(&u)) {
|
|
return false;
|
|
}
|
|
*p = reinterpret_cast<void*>(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<char*>(&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<double>(PairToUInt64(tag, data));
|
|
}
|
|
|
|
bool SCOutput::writeDouble(double d) {
|
|
return write(BitwiseCast<uint64_t>(CanonicalizeNaN(d)));
|
|
}
|
|
|
|
template <class T>
|
|
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<char*>(&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<uint8_t>(const uint8_t* p, size_t nelems) {
|
|
if (nelems == 0) {
|
|
return true;
|
|
}
|
|
|
|
if (!buf.AppendBytes(reinterpret_cast<const char*>(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<uint64_t, SystemAllocPolicy>(*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<SharedArrayBufferObject>()) {
|
|
return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE);
|
|
}
|
|
|
|
else if (unwrappedObj->is<WasmMemoryObject>()) {
|
|
if (unwrappedObj->as<WasmMemoryObject>().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<ArrayBufferObject>()) {
|
|
if (unwrappedObj->as<ArrayBufferObject>().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 <typename... Args>
|
|
bool JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId,
|
|
Args&&... aArgs) {
|
|
ReportDataCloneError(context(), out.buf.callbacks_, errorId, out.buf.closure_,
|
|
std::forward<Args>(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<uint32_t>(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<TypedArrayObject*> tarr(context(),
|
|
obj->maybeUnwrapAs<TypedArrayObject>());
|
|
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<DataViewObject*> view(context(), obj->maybeUnwrapAs<DataViewObject>());
|
|
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<ArrayBufferObject*> buffer(context(),
|
|
obj->maybeUnwrapAs<ArrayBufferObject>());
|
|
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<SharedArrayBufferObject>());
|
|
|
|
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<SharedArrayBufferObject*> sharedArrayBuffer(
|
|
context(), obj->maybeUnwrapAs<SharedArrayBufferObject>());
|
|
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<intptr_t>(rawbuf);
|
|
uint64_t byteLength = sharedArrayBuffer->byteLength();
|
|
if (!(out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT,
|
|
static_cast<uint32_t>(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<WasmMemoryObject>());
|
|
|
|
// 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<WasmMemoryObject*> memoryObj(context(),
|
|
&obj->unwrapAs<WasmMemoryObject>());
|
|
Rooted<SharedArrayBufferObject*> sab(
|
|
context(), &memoryObj->buffer().as<SharedArrayBufferObject>());
|
|
|
|
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<NativeObject>()) {
|
|
return true;
|
|
}
|
|
|
|
Handle<NativeObject*> nobj = obj.as<NativeObject>();
|
|
if (nobj->isIndexed() || nobj->is<TypedArrayObject>() ||
|
|
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<NoGC> 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:
|
|
//
|
|
// <Object tag for obj1>
|
|
// <key1 data>
|
|
// <Object tag for key1's value>
|
|
// <key1.1 data>
|
|
// <val1.1 data>
|
|
// <key1.2 data>
|
|
// <val1.2 data>
|
|
// <end-of-children marker for key1's value>
|
|
// <key2 data>
|
|
// <Object tag for key2's value>
|
|
// <end-of-children marker for key2's value>
|
|
// <end-of-children marker for obj1>
|
|
//
|
|
// 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
|
|
//
|
|
// <Map tag>
|
|
// <key1 class tag>
|
|
// <value1 class tag>
|
|
// ...key1 fields...
|
|
// <end-of-children marker for key1>
|
|
// ...value1 fields...
|
|
// <end-of-children marker for value1>
|
|
// <end-of-children marker for Map>
|
|
//
|
|
// Notice how the end-of-children marker for key1 is sandwiched between the
|
|
// value1 beginning and end.
|
|
bool JSStructuredCloneWriter::traverseMap(HandleObject obj) {
|
|
Rooted<GCVector<Value>> newEntries(context(), GCVector<Value>(context()));
|
|
{
|
|
// If there is no wrapper, the compartment munging is a no-op.
|
|
RootedObject unwrapped(context(), obj->maybeUnwrapAs<MapObject>());
|
|
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<GCVector<Value>> keys(context(), GCVector<Value>(context()));
|
|
{
|
|
// If there is no wrapper, the compartment munging is a no-op.
|
|
RootedObject unwrapped(context(), obj->maybeUnwrapAs<SetObject>());
|
|
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*> savedFrame(context(), obj->maybeUnwrapAs<SavedFrame>());
|
|
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<Maybe<PropertyDescriptor>> 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<CanGC>(cx, messageVal);
|
|
if (!message) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 5. Set serialized to { [[Type]]: "Error", [[Name]]: name, [[Message]]:
|
|
// message }.
|
|
|
|
if (!objs.append(ObjectValue(*obj))) {
|
|
return false;
|
|
}
|
|
|
|
Rooted<ErrorObject*> unwrapped(cx, obj->maybeUnwrapAs<ErrorObject>());
|
|
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<SavedFrame>()) {
|
|
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<Maybe<PropertyDescriptor>> causeDesc(cx);
|
|
if (!GetOwnPropertyDescriptor(cx, obj, causeId, &causeDesc)) {
|
|
return false;
|
|
}
|
|
|
|
Rooted<Maybe<Value>> 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<TypedArrayObject>()) {
|
|
return writeTypedArray(obj);
|
|
}
|
|
if (obj->canUnwrapAs<DataViewObject>()) {
|
|
return writeDataView(obj);
|
|
}
|
|
if (wasm::IsSharedWasmMemoryObject(obj)) {
|
|
return writeSharedWasmMemory(obj);
|
|
}
|
|
if (obj->canUnwrapAs<SavedFrame>()) {
|
|
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<ArrayBufferObject*> arrayBuffer(
|
|
cx, obj->maybeUnwrapAs<ArrayBufferObject>());
|
|
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<uint64_t>(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<SavedFrame>()) {
|
|
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 <typename CharT>
|
|
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<CharT> 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<Latin1Char>(nchars, heap)
|
|
: readStringImpl<char16_t>(nchars, heap);
|
|
}
|
|
|
|
[[nodiscard]] bool JSStructuredCloneReader::readUint32(uint32_t* num) {
|
|
Rooted<Value> 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<ArrayBufferObjectMaybeShared>()) {
|
|
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<Scalar::Name>::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<ArrayBufferObjectMaybeShared>()) {
|
|
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<ArrayBufferObject>();
|
|
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<SharedArrayRawBuffer*>(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<SharedArrayBufferObject>()) {
|
|
JS_ReportErrorNumberASCII(
|
|
context(), GetErrorMessage, nullptr, JSMSG_SC_BAD_SERIALIZED_DATA,
|
|
"shared wasm memory must be backed by a SharedArrayBuffer");
|
|
return false;
|
|
}
|
|
|
|
Rooted<ArrayBufferObjectMaybeShared*> sab(
|
|
cx, &payload.toObject().as<SharedArrayBufferObject>());
|
|
|
|
// 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<size_t> nbytes =
|
|
mozilla::CheckedInt<size_t>(nelems) *
|
|
TypedArrayElemSize(static_cast<Scalar::Type>(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<ArrayBufferObject>();
|
|
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<uint8_t>(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<JSAtom*> 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) {
|
|
// When the ArrayBuffer can't be allocated, |content| will be free'ed
|
|
// in `JSStructuredCloneData::discardTransferables()`.
|
|
obj = JS::NewArrayBufferWithContents(
|
|
cx, nbytes, content,
|
|
JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory);
|
|
} 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<size_t>(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*> 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<SavedFrame*> 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<SavedFrame>()) {
|
|
parentFrame = &parent.toObject().as<SavedFrame>();
|
|
} 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<Maybe<Value>> 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<JSExnType>(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<ErrorObject*> 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<SavedFrame>()) {
|
|
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<MapObject*> 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<CanGC>(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<PlainObject>() &&
|
|
MOZ_LIKELY(!obj->as<PlainObject>().contains(context(), id))) {
|
|
return AddDataPropertyToPlainObject(context(), obj.as<PlainObject>(), 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>()) {
|
|
ArrayObject* arr = &obj->as<ArrayObject>();
|
|
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<MapObject>() || obj->is<SetObject>() ||
|
|
obj->is<SavedFrame>() || obj->is<ErrorObject>())) {
|
|
// 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<SetObject>()) {
|
|
// 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<MapObject>()) {
|
|
Rooted<MapObject*> mapObj(context(), &obj->as<MapObject>());
|
|
if (!readMapField(mapObj, key)) {
|
|
return false;
|
|
}
|
|
} else if (obj->is<SavedFrame>()) {
|
|
Rooted<SavedFrame*> frameObj(context(), &obj->as<SavedFrame>());
|
|
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<ErrorObject>()) {
|
|
Rooted<ErrorObject*> errorObj(context(), &obj->as<ErrorObject>());
|
|
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<TypedArrayObject>()) {
|
|
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();
|
|
}
|