Refactor structured clone JSAPI to prevent mismatched scopes.

Roll-up of bugs 1442722, 1455071, 1433642, 1456604 and 1458320.
This commit is contained in:
wolfbeast 2018-05-02 11:07:35 -07:00 • committed by Roy Tam
commit 0795e50d87
15 changed files with 447 additions and 311 deletions

View file

@ -160,16 +160,16 @@ template<typename T, typename AllocPolicy>
struct BufferIterator {
typedef mozilla::BufferList<AllocPolicy> BufferList;
explicit BufferIterator(BufferList& buffer)
explicit BufferIterator(const BufferList& buffer)
: mBuffer(buffer)
, mIter(buffer.Iter())
{
JS_STATIC_ASSERT(8 % sizeof(T) == 0);
}
BufferIterator(const BufferIterator& other)
: mBuffer(other.mBuffer)
, mIter(other.mIter)
explicit BufferIterator(const JSStructuredCloneData& data)
: mBuffer(data.bufList_)
, mIter(data.Start())
{
}
@ -228,17 +228,26 @@ struct BufferIterator {
return mIter.HasRoomFor(sizeof(T));
}
BufferList& mBuffer;
const BufferList& mBuffer;
typename BufferList::IterImpl mIter;
};
// 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, TempAllocPolicy>;
using Iter = BufferIterator<uint64_t, SystemAllocPolicy>;
explicit SCOutput(JSContext* cx);
SCOutput(JSContext* cx, JS::StructuredCloneScope scope);
JSContext* context() const { return cx; }
JS::StructuredCloneScope scope() const { return buf.scope(); }
bool write(uint64_t u);
bool writePair(uint32_t tag, uint32_t data);
@ -251,22 +260,25 @@ struct SCOutput {
template <class T>
bool writeArray(const T* p, size_t nbytes);
bool extractBuffer(JSStructuredCloneData* data);
void discardTransferables(const JSStructuredCloneCallbacks* cb, void* cbClosure);
void setCallbacks(const JSStructuredCloneCallbacks* callbacks,
void* closure,
OwnTransferablePolicy policy)
{
buf.setCallbacks(callbacks, closure, policy);
}
void extractBuffer(JSStructuredCloneData* data) { *data = Move(buf); }
void discardTransferables();
uint64_t tell() const { return buf.Size(); }
uint64_t count() const { return buf.Size() / sizeof(uint64_t); }
Iter iter() {
return BufferIterator<uint64_t, TempAllocPolicy>(buf);
}
Iter iter() { return Iter(buf); }
size_t offset(Iter dest) {
return dest - iter();
}
private:
JSContext* cx;
mozilla::BufferList<TempAllocPolicy> buf;
JSStructuredCloneData buf;
};
class SCInput {
@ -356,13 +368,6 @@ struct JSStructuredCloneReader {
// be valid cross-process.)
JS::StructuredCloneScope allowedScope;
// The scope the buffer was generated for (what sort of buffer it is.) The
// scope is not just a permissions thing; it also affects the storage
// format (eg a Transferred ArrayBuffer can be stored as a pointer for
// SameProcessSameThread but must have its contents in the clone buffer for
// DifferentProcess.)
JS::StructuredCloneScope storedScope;
// Stack of objects with properties remaining to be read.
AutoValueVector objs;
@ -386,13 +391,15 @@ struct JSStructuredCloneWriter {
const JSStructuredCloneCallbacks* cb,
void* cbClosure,
const Value& tVal)
: out(cx), scope(scope), objs(out.context()),
: out(cx, scope), objs(out.context()),
counts(out.context()), entries(out.context()),
memory(out.context()), callbacks(cb),
closure(cbClosure), transferable(out.context(), tVal),
memory(out.context()),
transferable(out.context(), tVal),
transferableObjects(out.context(), GCHashSet<JSObject*>(cx)),
cloneDataPolicy(cloneDataPolicy)
{}
{
out.setCallbacks(cb, cbClosure, OwnTransferablePolicy::NoTransferables);
}
~JSStructuredCloneWriter();
@ -408,17 +415,10 @@ struct JSStructuredCloneWriter {
SCOutput& output() { return out; }
bool extractBuffer(JSStructuredCloneData* data) {
bool success = out.extractBuffer(data);
if (success) {
data->setOptionalCallbacks(callbacks, closure,
OwnTransferablePolicy::OwnsTransferablesIfAny);
}
return success;
void extractBuffer(JSStructuredCloneData* newData) {
out.extractBuffer(newData);
}
JS::StructuredCloneScope cloneScope() const { return scope; }
private:
JSStructuredCloneWriter() = delete;
JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete;
@ -449,9 +449,6 @@ struct JSStructuredCloneWriter {
SCOutput out;
// The (address space, thread) scope within which this clone is valid.
JS::StructuredCloneScope scope;
// Vector of objects with properties remaining to be written.
//
// NB: These can span multiple compartments, so the compartment must be
@ -477,12 +474,6 @@ struct JSStructuredCloneWriter {
SystemAllocPolicy>;
Rooted<CloneMemory> memory;
// The user defined callbacks that will be used for cloning.
const JSStructuredCloneCallbacks* callbacks;
// Any value passed to JS_WriteStructuredClone.
void* closure;
// Set of transferable objects
RootedValue transferable;
Rooted<GCHashSet<JSObject*>> transferableObjects;
@ -542,7 +533,12 @@ WriteStructuredClone(JSContext* cx, HandleValue v, JSStructuredCloneData* bufp,
const Value& transferable)
{
JSStructuredCloneWriter w(cx, scope, cloneDataPolicy, cb, cbClosure, transferable);
return w.init() && w.write(v) && w.extractBuffer(bufp);
if (!w.init())
return false;
if (!w.write(v))
return false;
w.extractBuffer(bufp);
return true;
}
bool
@ -555,91 +551,15 @@ ReadStructuredClone(JSContext* cx, JSStructuredCloneData& data,
return r.read(vp);
}
// If the given buffer contains Transferables, free them. Note that custom
// Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to
// delete their transferables.
template<typename AllocPolicy>
static void
DiscardTransferables(mozilla::BufferList<AllocPolicy>& buffer,
const JSStructuredCloneCallbacks* cb, void* cbClosure)
{
auto point = BufferIterator<uint64_t, AllocPolicy>(buffer);
if (point.done())
return; // Empty buffer
uint32_t tag, data;
MOZ_RELEASE_ASSERT(point.canPeek());
SCInput::getPair(point.peek(), &tag, &data);
point.next();
if (tag == SCTAG_HEADER) {
if (point.done())
return;
MOZ_RELEASE_ASSERT(point.canPeek());
SCInput::getPair(point.peek(), &tag, &data);
point.next();
}
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;
uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek());
point.next();
while (numTransferables--) {
if (!point.canPeek())
return;
uint32_t ownership;
SCInput::getPair(point.peek(), &tag, &ownership);
point.next();
MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
if (!point.canPeek())
return;
void* content;
SCInput::getPtr(point.peek(), &content);
point.next();
if (!point.canPeek())
return;
uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek());
point.next();
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 (cb && cb->freeTransfer) {
cb->freeTransfer(tag, JS::TransferableOwnership(ownership), content, extraData, cbClosure);
} else {
MOZ_ASSERT(false, "unknown ownership");
}
}
}
static bool
StructuredCloneHasTransferObjects(const JSStructuredCloneData& data)
{
auto iter = data.Iter();
if (data.Size() < sizeof(uint64_t))
return false;
uint64_t u;
data.ReadBytes(iter, reinterpret_cast<char*>(&u), sizeof(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);
}
@ -650,7 +570,7 @@ SCInput::SCInput(JSContext* cx, JSStructuredCloneData& data)
: cx(cx), point(data)
{
static_assert(JSStructuredCloneData::kSegmentAlignment % 8 == 0,
static_assert(JSStructuredCloneData::BufferList::kSegmentAlignment % 8 == 0,
"structured clone buffer reads should be aligned");
MOZ_ASSERT(data.Size() % 8 == 0);
}
@ -812,9 +732,8 @@ SCInput::readPtr(void** p)
return true;
}
SCOutput::SCOutput(JSContext* cx)
: cx(cx)
, buf(0, 0, 4096, cx)
SCOutput::SCOutput(JSContext* cx, JS::StructuredCloneScope scope)
: cx(cx), buf(scope)
{
}
@ -822,7 +741,11 @@ bool
SCOutput::write(uint64_t u)
{
uint64_t v = NativeEndian::swapToLittleEndian(u);
return buf.WriteBytes(reinterpret_cast<char*>(&v), sizeof(u));
if (!buf.AppendBytes(reinterpret_cast<char*>(&v), sizeof(u))) {
ReportOutOfMemory(context());
return false;
}
return true;
}
bool
@ -883,7 +806,7 @@ SCOutput::writeArray(const T* p, size_t nelems)
for (size_t i = 0; i < nelems; i++) {
T value = swapToLittleEndian(p[i]);
if (!buf.WriteBytes(reinterpret_cast<char*>(&value), sizeof(value)))
if (!buf.AppendBytes(reinterpret_cast<char*>(&value), sizeof(value)))
return false;
}
@ -892,7 +815,7 @@ SCOutput::writeArray(const T* p, size_t nelems)
size_t padbytes = sizeof(uint64_t) * nwords - sizeof(T) * nelems;
char zero = 0;
for (size_t i = 0; i < padbytes; i++) {
if (!buf.WriteBytes(&zero, sizeof(zero)))
if (!buf.AppendBytes(&zero, sizeof(zero)))
return false;
}
@ -927,34 +850,101 @@ SCOutput::writePtr(const void* p)
return write(reinterpret_cast<uint64_t>(p));
}
bool
SCOutput::extractBuffer(JSStructuredCloneData* data)
{
bool success;
mozilla::BufferList<SystemAllocPolicy> out =
buf.MoveFallible<SystemAllocPolicy>(&success);
if (!success) {
ReportOutOfMemory(cx);
return false;
}
*data = JSStructuredCloneData(Move(out));
return true;
}
void
SCOutput::discardTransferables(const JSStructuredCloneCallbacks* cb, void* cbClosure)
SCOutput::discardTransferables()
{
DiscardTransferables(buf, cb, cbClosure);
buf.discardTransferables();
}
} /* namespace js */
JSStructuredCloneData::~JSStructuredCloneData()
// 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)
DiscardTransferables(*this, callbacks_, closure_);
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);
point.next();
if (tag == SCTAG_HEADER) {
if (point.done())
return;
MOZ_RELEASE_ASSERT(point.canPeek());
SCInput::getPair(point.peek(), &tag, &data);
point.next();
}
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;
uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek());
point.next();
while (numTransferables--) {
if (!point.canPeek())
return;
uint32_t ownership;
SCInput::getPair(point.peek(), &tag, &ownership);
point.next();
MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
if (!point.canPeek())
return;
void* content;
SCInput::getPtr(point.peek(), &content);
point.next();
if (!point.canPeek())
return;
uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek());
point.next();
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");
}
}
}
JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX);
@ -962,9 +952,8 @@ JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX);
JSStructuredCloneWriter::~JSStructuredCloneWriter()
{
// Free any transferable data left lying around in the buffer
if (out.count()) {
out.discardTransferables(callbacks, closure);
}
if (out.count())
out.discardTransferables();
}
bool
@ -1038,7 +1027,7 @@ JSStructuredCloneWriter::parseTransferable()
bool
JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId)
{
ReportDataCloneError(context(), callbacks, errorId);
ReportDataCloneError(context(), out.buf.callbacks_, errorId);
return false;
}
@ -1454,8 +1443,8 @@ JSStructuredCloneWriter::startWrite(HandleValue v)
return traverseSavedFrame(obj);
}
if (callbacks && callbacks->write)
return callbacks->write(context(), this, obj, closure);
if (out.buf.callbacks_ && out.buf.callbacks_->write)
return out.buf.callbacks_->write(context(), this, obj, out.buf.closure_);
/* else fall through */
}
@ -1465,7 +1454,7 @@ JSStructuredCloneWriter::startWrite(HandleValue v)
bool
JSStructuredCloneWriter::writeHeader()
{
return out.writePair(SCTAG_HEADER, (uint32_t)scope);
return out.writePair(SCTAG_HEADER, (uint32_t)output().scope());
}
bool
@ -1523,6 +1512,7 @@ JSStructuredCloneWriter::transferOwnership()
JSContext* cx = context();
RootedObject obj(cx);
JS::StructuredCloneScope scope = output().scope();
for (auto tr = transferableObjects.all(); !tr.empty(); tr.popFront()) {
obj = tr.front();
@ -1555,7 +1545,9 @@ JSStructuredCloneWriter::transferOwnership()
return false;
}
if (scope == JS::StructuredCloneScope::DifferentProcess) {
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
@ -1592,9 +1584,9 @@ JSStructuredCloneWriter::transferOwnership()
extraData = nbytes;
}
} else {
if (!callbacks || !callbacks->writeTransfer)
if (!out.buf.callbacks_ || !out.buf.callbacks_->writeTransfer)
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
if (!callbacks->writeTransfer(cx, obj, closure, &tag, &ownership, &content, &extraData))
if (!out.buf.callbacks_->writeTransfer(cx, obj, out.buf.closure_, &tag, &ownership, &content, &extraData))
return false;
MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY);
}
@ -2187,25 +2179,33 @@ JSStructuredCloneReader::readHeader()
if (!in.getPair(&tag, &data))
return in.reportTruncated();
if (tag != SCTAG_HEADER) {
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::DifferentProcess;
return true;
storedScope = JS::StructuredCloneScope::DifferentProcessForIndexedDB;
}
MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
storedScope = JS::StructuredCloneScope(data);
if (data != uint32_t(JS::StructuredCloneScope::SameProcessSameThread) &&
data != uint32_t(JS::StructuredCloneScope::SameProcessDifferentThread) &&
data != uint32_t(JS::StructuredCloneScope::DifferentProcess))
if (storedScope < JS::StructuredCloneScope::SameProcessSameThread ||
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,
JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA,
"incompatible structured clone scope");
return false;
}
@ -2249,10 +2249,14 @@ JSStructuredCloneReader::readTransferMap()
return false;
if (tag == SCTAG_TRANSFER_MAP_ARRAY_BUFFER) {
if (storedScope == JS::StructuredCloneScope::DifferentProcess) {
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.
continue;
// 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);
return false;
}
size_t nbytes = extraData;
@ -2586,7 +2590,7 @@ JS_StructuredClone(JSContext* cx, HandleValue value, MutableHandleValue vp,
}
JSAutoStructuredCloneBuffer::JSAutoStructuredCloneBuffer(JSAutoStructuredCloneBuffer&& other)
: scope_(other.scope_)
: scope_(other.scope()), data_(other.scope())
{
data_.ownTransferables_ = other.data_.ownTransferables_;
other.steal(&data_, &version_, &data_.callbacks_, &data_.closure_);
@ -2604,45 +2608,14 @@ JSAutoStructuredCloneBuffer::operator=(JSAutoStructuredCloneBuffer&& other)
}
void
JSAutoStructuredCloneBuffer::clear(const JSStructuredCloneCallbacks* optionalCallbacks,
void* optionalClosure)
JSAutoStructuredCloneBuffer::clear()
{
if (!data_.Size())
return;
const JSStructuredCloneCallbacks* callbacks =
optionalCallbacks ? optionalCallbacks : data_.callbacks_;
void* closure = optionalClosure ? optionalClosure : data_.closure_;
if (data_.ownTransferables_ == OwnTransferablePolicy::OwnsTransferablesIfAny)
DiscardTransferables(data_, callbacks, closure);
data_.discardTransferables();
data_.ownTransferables_ = OwnTransferablePolicy::NoTransferables;
data_.Clear();
version_ = 0;
}
bool
JSAutoStructuredCloneBuffer::copy(const JSStructuredCloneData& srcData, uint32_t version,
const JSStructuredCloneCallbacks* callbacks,
void* closure)
{
// transferable objects cannot be copied
if (StructuredCloneHasTransferObjects(srcData))
return false;
clear();
auto iter = srcData.Iter();
while (!iter.Done()) {
data_.WriteBytes(iter.Data(), iter.RemainingInSegment());
iter.Advance(srcData, iter.RemainingInSegment());
}
version_ = version;
data_.setOptionalCallbacks(callbacks, closure, OwnTransferablePolicy::NoTransferables);
return true;
}
void
JSAutoStructuredCloneBuffer::adopt(JSStructuredCloneData&& data, uint32_t version,
const JSStructuredCloneCallbacks* callbacks,
@ -2651,7 +2624,7 @@ JSAutoStructuredCloneBuffer::adopt(JSStructuredCloneData&& data, uint32_t versio
clear();
data_ = Move(data);
version_ = version;
data_.setOptionalCallbacks(callbacks, closure, OwnTransferablePolicy::OwnsTransferablesIfAny);
data_.setCallbacks(callbacks, closure, OwnTransferablePolicy::OwnsTransferablesIfAny);
}
void
@ -2668,7 +2641,7 @@ JSAutoStructuredCloneBuffer::steal(JSStructuredCloneData* data, uint32_t* versio
*data = Move(data_);
version_ = 0;
data_.setOptionalCallbacks(nullptr, nullptr, OwnTransferablePolicy::NoTransferables);
data_.setCallbacks(nullptr, nullptr, OwnTransferablePolicy::NoTransferables);
}
bool
@ -2782,5 +2755,5 @@ JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj)
JS_PUBLIC_API(JS::StructuredCloneScope)
JS_GetStructuredCloneScope(JSStructuredCloneWriter* w)
{
return w->cloneScope();
return w->output().scope();
}