From dbd24194ed8442f4211bc079ea8eb56a31a9d559 Mon Sep 17 00:00:00 2001 From: "Nicolas B. Pierron" Date: Mon, 26 Feb 2018 16:49:23 +0000 Subject: [PATCH] Bug 1401939 - Align XDR content to avoid undefined behaviours. r=kmag,tcampbell --- js/src/vm/JSAtom.cpp | 39 ++++++++----- js/src/vm/JSFunction.cpp | 7 +++ js/src/vm/Xdr.cpp | 28 +++++++++ js/src/vm/Xdr.h | 77 ++++++++++++++++++++++++- js/xpconnect/loader/ScriptPreloader.cpp | 29 ++++++++++ 5 files changed, 166 insertions(+), 14 deletions(-) diff --git a/js/src/vm/JSAtom.cpp b/js/src/vm/JSAtom.cpp index d9834f533a81..c1c0ff260cf3 100644 --- a/js/src/vm/JSAtom.cpp +++ b/js/src/vm/JSAtom.cpp @@ -712,27 +712,38 @@ template bool js::XDRAtom(XDRState* xdr, MutableHandleAtom atomp) { + bool latin1 = false; + uint32_t length = 0; + uint32_t lengthAndEncoding = 0; if (mode == XDR_ENCODE) { static_assert(JSString::MAX_LENGTH <= INT32_MAX, "String length must fit in 31 bits"); - uint32_t length = atomp->length(); - uint32_t lengthAndEncoding = (length << 1) | uint32_t(atomp->hasLatin1Chars()); - if (!xdr->codeUint32(&lengthAndEncoding)) - return false; - - JS::AutoCheckCannotGC nogc; - return atomp->hasLatin1Chars() - ? xdr->codeChars(atomp->latin1Chars(nogc), length) - : xdr->codeChars(const_cast(atomp->twoByteChars(nogc)), length); + latin1 = atomp->hasLatin1Chars(); + length = atomp->length(); + lengthAndEncoding = (length << 1) | uint32_t(latin1); } - /* Avoid JSString allocation for already existing atoms. See bug 321985. */ - uint32_t lengthAndEncoding; if (!xdr->codeUint32(&lengthAndEncoding)) return false; - uint32_t length = lengthAndEncoding >> 1; - bool latin1 = lengthAndEncoding & 0x1; + if (mode == XDR_DECODE) { + length = lengthAndEncoding >> 1; + latin1 = lengthAndEncoding & 0x1; + } + // We need to align the string in the XDR buffer such that we can avoid + // non-align loads of 16bits characters. + if (!latin1 && !xdr->codeAlign(sizeof(char16_t))) + return false; + + if (mode == XDR_ENCODE) { + JS::AutoCheckCannotGC nogc; + if (latin1) + return xdr->codeChars(atomp->latin1Chars(nogc), length); + return xdr->codeChars(const_cast(atomp->twoByteChars(nogc)), length); + } + + MOZ_ASSERT(mode == XDR_DECODE); + /* Avoid JSString allocation for already existing atoms. See bug 321985. */ JSContext* cx = xdr->cx(); JSAtom* atom; if (latin1) { @@ -754,6 +765,8 @@ js::XDRAtom(XDRState* xdr, MutableHandleAtom atomp) size_t nbyte = length * sizeof(char16_t); if (!xdr->peekData(&ptr, nbyte)) return false; + MOZ_ASSERT(reinterpret_cast(ptr) % sizeof(char16_t) == 0, + "non-aligned buffer during JSAtom decoding"); chars = reinterpret_cast(ptr); } atom = AtomizeChars(cx, chars, length); diff --git a/js/src/vm/JSFunction.cpp b/js/src/vm/JSFunction.cpp index f6581a5462e4..172cdcc81826 100644 --- a/js/src/vm/JSFunction.cpp +++ b/js/src/vm/JSFunction.cpp @@ -622,6 +622,8 @@ js::XDRInterpretedFunction(XDRState* xdr, HandleScope enclosingScope, // Everything added below can substituted by the non-lazy-script version of // this function later. + if (!xdr->codeAlign(sizeof(js::XDRAlignment))) + return false; js::AutoXDRTree funTree(xdr, xdr->getTreeKey(fun)); if (!xdr->codeUint32(&firstword)) @@ -680,6 +682,11 @@ js::XDRInterpretedFunction(XDRState* xdr, HandleScope enclosingScope, if (!xdr->codeMarker(0x9E35CA1F)) return false; + // Required by AutoXDRTree to copy & paste snipet of sub-trees while keeping + // the alignment. + if (!xdr->codeAlign(sizeof(js::XDRAlignment))) + return false; + return true; } diff --git a/js/src/vm/Xdr.cpp b/js/src/vm/Xdr.cpp index 260d148feacb..c303623cfc18 100644 --- a/js/src/vm/Xdr.cpp +++ b/js/src/vm/Xdr.cpp @@ -11,6 +11,7 @@ #include #include "jsapi.h" +#include "jsutil.h" #include "vm/Debugger.h" #include "vm/EnvironmentObject.h" @@ -170,6 +171,12 @@ XDRState::codeScript(MutableHandleScript scriptp) mode == XDR_DECODE ? TraceLogger_DecodeScript : TraceLogger_EncodeScript; AutoTraceLog tl(logger, event); + // This should be a no-op when encoding, but when decoding it would eat any + // miss-aligned bytes if when encoding the XDR buffer is appended at the end + // of an existing buffer, such as in XDRIncrementalEncoder::linearize + // function. + if (!codeAlign(sizeof(js::XDRAlignment))) + return false; AutoXDRTree scriptTree(this, getTopLevelTreeKey()); if (mode == XDR_DECODE) @@ -188,6 +195,9 @@ XDRState::codeScript(MutableHandleScript scriptp) return false; } + if (!codeAlign(sizeof(js::XDRAlignment))) + return false; + return true; } @@ -206,12 +216,16 @@ AutoXDRTree::AutoXDRTree(XDRCoderBase* xdr, AutoXDRTree::Key key) parent_(this), xdr_(xdr) { + // Expect sub-tree to start with the maximum alignment required. + MOZ_ASSERT(xdr->isAligned(sizeof(js::XDRAlignment))); if (key_ != AutoXDRTree::noKey) xdr->createOrReplaceSubTree(this); } AutoXDRTree::~AutoXDRTree() { + // Expect sub-tree to end with the maximum alignment required. + MOZ_ASSERT(xdr_->isAligned(sizeof(js::XDRAlignment))); if (key_ != AutoXDRTree::noKey) xdr_->endSubTree(); } @@ -340,6 +354,18 @@ XDRIncrementalEncoder::linearize(JS::TranscodeBuffer& buffer) // Do not linearize while we are currently adding bytes. MOZ_ASSERT(scope_ == nullptr); + // Ensure the content of the buffer is properly aligned within the buffer. + // This alignment difference should be consumed by the codeAlign function + // call of codeScript when decoded. + size_t alignLen = sizeof(js::XDRAlignment); + if (buffer.length() % alignLen) { + alignLen = ComputeByteAlignment(buffer.length(), alignLen); + if (!buffer.appendN(0, alignLen)) { + ReportOutOfMemory(cx()); + return fail(JS::TranscodeResult_Throw); + } + } + // Visit the tree parts in a depth first order, to linearize the bits. Vector depthFirst(cx()); @@ -364,6 +390,8 @@ XDRIncrementalEncoder::linearize(JS::TranscodeBuffer& buffer) // buffer which would be serialized. MOZ_ASSERT(slice.sliceBegin <= slices_.length()); MOZ_ASSERT(slice.sliceBegin + slice.sliceLength <= slices_.length()); + MOZ_ASSERT(buffer.length() % sizeof(XDRAlignment) == 0); + MOZ_ASSERT(slice.sliceLength % sizeof(XDRAlignment) == 0); if (!buffer.append(slices_.begin() + slice.sliceBegin, slice.sliceLength)) { ReportOutOfMemory(cx()); return fail(JS::TranscodeResult_Throw); diff --git a/js/src/vm/Xdr.h b/js/src/vm/Xdr.h index dac4d2e205ec..10c2e16b80a0 100644 --- a/js/src/vm/Xdr.h +++ b/js/src/vm/Xdr.h @@ -24,7 +24,13 @@ class XDRBufferBase { public: explicit XDRBufferBase(JSContext* cx, size_t cursor = 0) - : context_(cx), cursor_(cursor) { } + : context_(cx), cursor_(cursor) +#ifdef DEBUG + // Note, when decoding the buffer can be set to a range, which does not + // have any alignment requirement as opposed to allocations. + , aligned_(false) +#endif + { } JSContext* cx() const { return context_; @@ -34,9 +40,24 @@ class XDRBufferBase return cursor_; } +#ifdef DEBUG + // This function records if the cursor got changed by codeAlign or by any + // other read/write of data. This is used for AutoXDRTree assertions, as a + // way to ensure that the last thing done is properly setting the alignment + // with codeAlign function. + void setAligned(bool aligned) { aligned_ = aligned; } + bool isAligned() const { return aligned_; } +#else + void setAligned(bool) const {} + bool isAligned() const { return true; } +#endif + protected: JSContext* const context_; size_t cursor_; +#ifdef DEBUG + bool aligned_; +#endif }; template @@ -52,6 +73,7 @@ class XDRBuffer : public XDRBufferBase uint8_t* write(size_t n) { MOZ_ASSERT(n != 0); + setAligned(false); if (!buffer_.growByUninitialized(n)) { ReportOutOfMemory(cx()); return nullptr; @@ -66,6 +88,11 @@ class XDRBuffer : public XDRBufferBase return nullptr; } + uintptr_t uptr() const { + // Note: Avoid bounds check assertion if the buffer is not yet allocated. + return reinterpret_cast(buffer_.begin() + cursor_); + } + private: JS::TranscodeBuffer& buffer_; }; @@ -84,6 +111,7 @@ class XDRBuffer : public XDRBufferBase const uint8_t* read(size_t n) { MOZ_ASSERT(cursor_ < buffer_.length()); + setAligned(false); uint8_t* ptr = &buffer_[cursor_]; cursor_ += n; @@ -99,12 +127,19 @@ class XDRBuffer : public XDRBufferBase return nullptr; } + uintptr_t uptr() const { + // Note: Avoid bounds check assertion at the end of the buffer. + return reinterpret_cast(buffer_.begin().get() + cursor_); + } + private: const JS::TranscodeRange buffer_; }; class XDRCoderBase; class XDRIncrementalEncoder; +using XDRAlignment = char16_t; +static const uint8_t AlignPadding[sizeof(XDRAlignment)] = { 0, 0 }; // An AutoXDRTree is used to identify section encoded by an XDRIncrementalEncoder. // @@ -157,6 +192,7 @@ class XDRCoderBase virtual AutoXDRTree::Key getTreeKey(JSFunction* fun) const { return AutoXDRTree::noKey; } virtual void createOrReplaceSubTree(AutoXDRTree* child) {}; virtual void endSubTree() {}; + virtual bool isAligned(size_t n) = 0; }; /* @@ -219,6 +255,45 @@ class XDRState : public XDRCoderBase return true; } + // Alignment is required when doing memcpy of data which contains element + // largers than 1 byte. + bool isAligned(size_t n) override { + MOZ_ASSERT(mozilla::IsPowerOfTwo(n)); + // If there is a failure, always assume that we are aligned. + if (resultCode() != JS::TranscodeResult_Ok) + return true; + size_t mask = n - 1; + size_t offset = buf.uptr() & mask; + // In debug build, we not only check if the cursor is aligned, but also + // if the last cursor manipulation was made by the codeAlign function. + return offset == 0 && buf.isAligned(); + } + bool codeAlign(size_t n) { + MOZ_ASSERT(mozilla::IsPowerOfTwo(n)); + size_t mask = n - 1; + MOZ_ASSERT_IF(mode == XDR_ENCODE, (buf.uptr() & mask) == (buf.cursor() & mask)); + size_t offset = buf.uptr() & mask; + if (offset) { + size_t padding = n - offset; + MOZ_ASSERT(padding < sizeof(AlignPadding)); + if (mode == XDR_ENCODE) { + uint8_t* ptr = buf.write(padding); + if (!ptr) + return fail(JS::TranscodeResult_Throw); + memcpy(ptr, AlignPadding, padding); + } else { + const uint8_t* ptr = buf.read(padding); + if (!ptr) + return fail(JS::TranscodeResult_Failure_BadDecode); + if (memcmp(ptr, AlignPadding, padding) != 0) + return fail(JS::TranscodeResult_Failure_BadDecode); + } + } + buf.setAligned(true); + MOZ_ASSERT(isAligned(n)); + return true; + } + bool codeUint8(uint8_t* n) { if (mode == XDR_ENCODE) { uint8_t* ptr = buf.write(sizeof(*n)); diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp index d0a3c5cb4093..0782e5987f1e 100644 --- a/js/xpconnect/loader/ScriptPreloader.cpp +++ b/js/xpconnect/loader/ScriptPreloader.cpp @@ -52,6 +52,16 @@ using namespace mozilla::loader; ProcessType ScriptPreloader::sProcessType; +// This type correspond to js::vm::XDRAlignment type, which is used as a size +// reference for alignment of XDR buffers. +using XDRAlign = uint16_t; +static const uint8_t sAlignPadding[sizeof(XDRAlign)] = { 0, 0 }; + +static inline size_t +ComputeByteAlignment(size_t bytes, size_t align) +{ + return (align - (bytes % align)) % align; +} nsresult ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, @@ -460,6 +470,8 @@ ScriptPreloader::InitCacheInternal(JS::HandleObject scope) } auto data = mCacheData.get(); + uint8_t* start = data.get(); + MOZ_ASSERT(reinterpret_cast(start) % sizeof(XDRAlign) == 0); auto end = data + size; if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) { @@ -486,6 +498,10 @@ ScriptPreloader::InitCacheInternal(JS::HandleObject scope) InputBuffer buf(header); + size_t len = data.get() - start; + size_t alignLen = ComputeByteAlignment(len, sizeof(XDRAlign)); + data += alignLen; + size_t offset = 0; while (!buf.finished()) { auto script = MakeUnique(*this, buf); @@ -503,6 +519,7 @@ ScriptPreloader::InitCacheInternal(JS::HandleObject scope) } offset += script->mSize; + MOZ_ASSERT(reinterpret_cast(scriptData.get()) % sizeof(XDRAlign) == 0); script->mXDRRange.emplace(scriptData, scriptData + script->mSize); // Don't pre-decode the script unless it was used in this process type during the @@ -657,6 +674,7 @@ ScriptPreloader::WriteCache() OutputBuffer buf; size_t offset = 0; for (auto script : scripts) { + MOZ_ASSERT(offset % sizeof(XDRAlign) == 0); script->mOffset = offset; script->Code(buf); @@ -666,11 +684,22 @@ ScriptPreloader::WriteCache() uint8_t headerSize[4]; LittleEndian::writeUint32(headerSize, buf.cursor()); + size_t len = 0; MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC))); + len += sizeof(MAGIC); MOZ_TRY(Write(fd, headerSize, sizeof(headerSize))); + len += sizeof(headerSize); MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); + len += buf.cursor(); + size_t alignLen = ComputeByteAlignment(len, sizeof(XDRAlign)); + if (alignLen) { + MOZ_TRY(Write(fd, sAlignPadding, alignLen)); + len += alignLen; + } for (auto script : scripts) { + MOZ_ASSERT(script->mSize % sizeof(XDRAlign) == 0); MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize)); + len += script->mSize; if (script->mScript) { script->FreeData();