Bug 1401939 - Align XDR content to avoid undefined behaviours. r=kmag,tcampbell

This commit is contained in:
Nicolas B. Pierron 2018-02-26 16:49:23 +00:00
parent f200bd41c5
commit dbd24194ed
5 changed files with 166 additions and 14 deletions

View File

@ -712,27 +712,38 @@ template<XDRMode mode>
bool
js::XDRAtom(XDRState<mode>* 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<char16_t*>(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<char16_t*>(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<mode>* xdr, MutableHandleAtom atomp)
size_t nbyte = length * sizeof(char16_t);
if (!xdr->peekData(&ptr, nbyte))
return false;
MOZ_ASSERT(reinterpret_cast<uintptr_t>(ptr) % sizeof(char16_t) == 0,
"non-aligned buffer during JSAtom decoding");
chars = reinterpret_cast<const char16_t*>(ptr);
}
atom = AtomizeChars(cx, chars, length);

View File

@ -622,6 +622,8 @@ js::XDRInterpretedFunction(XDRState<mode>* 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<mode>* 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;
}

View File

@ -11,6 +11,7 @@
#include <string.h>
#include "jsapi.h"
#include "jsutil.h"
#include "vm/Debugger.h"
#include "vm/EnvironmentObject.h"
@ -170,6 +171,12 @@ XDRState<mode>::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<mode>::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<SlicesNode::ConstRange> 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);

View File

@ -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 <XDRMode mode>
@ -52,6 +73,7 @@ class XDRBuffer<XDR_ENCODE> : 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<XDR_ENCODE> : public XDRBufferBase
return nullptr;
}
uintptr_t uptr() const {
// Note: Avoid bounds check assertion if the buffer is not yet allocated.
return reinterpret_cast<uintptr_t>(buffer_.begin() + cursor_);
}
private:
JS::TranscodeBuffer& buffer_;
};
@ -84,6 +111,7 @@ class XDRBuffer<XDR_DECODE> : 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<XDR_DECODE> : public XDRBufferBase
return nullptr;
}
uintptr_t uptr() const {
// Note: Avoid bounds check assertion at the end of the buffer.
return reinterpret_cast<uintptr_t>(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));

View File

@ -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>();
uint8_t* start = data.get();
MOZ_ASSERT(reinterpret_cast<uintptr_t>(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<CachedScript>(*this, buf);
@ -503,6 +519,7 @@ ScriptPreloader::InitCacheInternal(JS::HandleObject scope)
}
offset += script->mSize;
MOZ_ASSERT(reinterpret_cast<uintptr_t>(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();