Bug 1312001 - Scramble hash codes securely, to avoid leaking bits of object and symbol addresses.

MozReview-Commit-ID: yR1cIjrlPP

--HG--
extra : rebase_source : 871821e53eee5502cd255d52f02665f6845e3f09
This commit is contained in:
Jason Orendorff 2016-11-30 15:31:56 -06:00
Родитель 2aced9c475
Коммит 5d60d5ca87
12 изменённых файлов: 206 добавлений и 25 удалений

Просмотреть файл

@ -46,6 +46,10 @@ class MOZ_STACK_CLASS SourceBufferHolder;
class HandleValueArray;
class ObjectOpResult;
class Symbol;
enum class SymbolCode: uint32_t;
} // namespace JS
// Do the importing.
@ -154,6 +158,9 @@ using JS::ObjectOpResult;
using JS::Zone;
using JS::Symbol;
using JS::SymbolCode;
} /* namespace js */
#endif /* NamespaceImports_h */

Просмотреть файл

@ -16,6 +16,7 @@
#include "vm/GlobalObject.h"
#include "vm/Interpreter.h"
#include "vm/SelfHosting.h"
#include "vm/Symbol.h"
#include "jsobjinlines.h"
@ -65,20 +66,38 @@ HashableValue::setValue(JSContext* cx, HandleValue v)
}
static HashNumber
HashValue(const Value& v)
HashValue(const Value& v, const mozilla::HashCodeScrambler& hcs)
{
// HashableValue::setValue normalizes values so that the SameValue relation
// on HashableValues is the same as the == relationship on
// value.data.asBits.
// value.asRawBits(). So why not just return that? Security.
//
// To avoid revealing GC of atoms, string-based hash codes are computed
// from the string contents rather than any pointer; to avoid revealing
// addresses, pointer-based hash codes are computed using the
// HashCodeScrambler.
if (v.isString())
return v.toString()->asAtom().hash();
if (v.isSymbol()) {
Symbol* sym = v.toSymbol();
if (sym->isWellKnownSymbol())
return HashNumber(sym->code());
if (sym->code() == SymbolCode::InSymbolRegistry)
return sym->description()->hash();
return hcs.scramble(v.asRawBits());
}
if (v.isObject())
return hcs.scramble(v.asRawBits());
MOZ_ASSERT(!v.isGCThing(), "do not reveal pointers via hash codes");
return v.asRawBits();
}
HashNumber
HashableValue::hash() const
HashableValue::hash(const mozilla::HashCodeScrambler& hcs) const
{
return HashValue(value);
return HashValue(value, hcs);
}
bool
@ -365,7 +384,9 @@ MapObject::trace(JSTracer* trc, JSObject* obj)
struct js::UnbarrieredHashPolicy {
typedef Value Lookup;
static HashNumber hash(const Lookup& v) { return HashValue(v); }
static HashNumber hash(const Lookup& v, const mozilla::HashCodeScrambler& hcs) {
return HashValue(v, hcs);
}
static bool match(const Value& k, const Lookup& l) { return k == l; }
static bool isEmpty(const Value& v) { return v.isMagic(JS_HASH_KEY_EMPTY); }
static void makeEmpty(Value* vp) { vp->setMagic(JS_HASH_KEY_EMPTY); }
@ -415,17 +436,18 @@ class js::OrderedHashTableRef : public gc::BufferableRef
explicit OrderedHashTableRef(ObjectT* obj) : object(obj) {}
void trace(JSTracer* trc) override {
auto table = reinterpret_cast<typename ObjectT::UnbarrieredTable*>(object->getData());
auto realTable = object->getData();
auto unbarrieredTable = reinterpret_cast<typename ObjectT::UnbarrieredTable*>(realTable);
NurseryKeysVector* keys = GetNurseryKeys(object);
MOZ_ASSERT(keys);
for (JSObject* obj : *keys) {
MOZ_ASSERT(obj);
Value key = ObjectValue(*obj);
Value prior = key;
MOZ_ASSERT(UnbarrieredHashPolicy::hash(key) ==
HashableValue::Hasher::hash(*reinterpret_cast<HashableValue*>(&key)));
MOZ_ASSERT(unbarrieredTable->hash(key) ==
realTable->hash(*reinterpret_cast<HashableValue*>(&key)));
TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key");
table->rekeyOneEntry(prior, key);
unbarrieredTable->rekeyOneEntry(prior, key);
}
DeleteNurseryKeys(object);
}
@ -513,7 +535,8 @@ MapObject::set(JSContext* cx, HandleObject obj, HandleValue k, HandleValue v)
MapObject*
MapObject::create(JSContext* cx, HandleObject proto /* = nullptr */)
{
auto map = cx->make_unique<ValueMap>(cx->runtime());
auto map = cx->make_unique<ValueMap>(cx->runtime(),
cx->compartment()->randomHashCodeScrambler());
if (!map || !map->init()) {
ReportOutOfMemory(cx);
return nullptr;
@ -579,7 +602,7 @@ MapObject::is(HandleObject o)
}
#define ARG0_KEY(cx, args, key) \
Rooted<HashableValue> key(cx); \
Rooted<HashableValue> key(cx); \
if (args.length() > 0 && !key.setValue(cx, args[0])) \
return false
@ -1094,7 +1117,8 @@ SetObject::add(JSContext* cx, HandleObject obj, HandleValue k)
SetObject*
SetObject::create(JSContext* cx, HandleObject proto /* = nullptr */)
{
auto set = cx->make_unique<ValueSet>(cx->runtime());
auto set = cx->make_unique<ValueSet>(cx->runtime(),
cx->compartment()->randomHashCodeScrambler());
if (!set || !set->init()) {
ReportOutOfMemory(cx);
return nullptr;

Просмотреть файл

@ -32,7 +32,9 @@ class HashableValue
public:
struct Hasher {
typedef HashableValue Lookup;
static HashNumber hash(const Lookup& v) { return v.hash(); }
static HashNumber hash(const Lookup& v, const mozilla::HashCodeScrambler& hcs) {
return v.hash(hcs);
}
static bool match(const HashableValue& k, const Lookup& l) { return k == l; }
static bool isEmpty(const HashableValue& v) { return v.value.isMagic(JS_HASH_KEY_EMPTY); }
static void makeEmpty(HashableValue* vp) { vp->value = MagicValue(JS_HASH_KEY_EMPTY); }
@ -41,7 +43,7 @@ class HashableValue
HashableValue() : value(UndefinedValue()) {}
MOZ_MUST_USE bool setValue(JSContext* cx, HandleValue v);
HashNumber hash() const;
HashNumber hash(const mozilla::HashCodeScrambler& hcs) const;
bool operator==(const HashableValue& other) const;
HashableValue trace(JSTracer* trc) const;
Value get() const { return value.get(); }

Просмотреть файл

@ -29,12 +29,15 @@
*
* See the comment about "Hash policy" in HashTable.h for general features that
* hash policy classes must provide. Hash policies for OrderedHashMaps and Sets
* must additionally provide a distinguished "empty" key value and the
* differ in that the hash() method takes an extra argument:
* static js::HashNumber hash(Lookup, const HashCodeScrambler&);
* They must additionally provide a distinguished "empty" key value and the
* following static member functions:
* bool isEmpty(const Key&);
* void makeEmpty(Key*);
*/
#include "mozilla/HashFunctions.h"
#include "mozilla/Move.h"
using mozilla::Forward;
@ -78,10 +81,11 @@ class OrderedHashTable
uint32_t hashShift; // multiplicative hash shift
Range* ranges; // list of all live Ranges on this table
AllocPolicy alloc;
mozilla::HashCodeScrambler hcs; // don't reveal pointer hash codes
public:
explicit OrderedHashTable(AllocPolicy& ap)
: hashTable(nullptr), data(nullptr), dataLength(0), ranges(nullptr), alloc(ap) {}
OrderedHashTable(AllocPolicy& ap, mozilla::HashCodeScrambler hcs)
: hashTable(nullptr), data(nullptr), dataLength(0), ranges(nullptr), alloc(ap), hcs(hcs) {}
MOZ_MUST_USE bool init() {
MOZ_ASSERT(!hashTable, "init must be called at most once");
@ -432,8 +436,8 @@ class OrderedHashTable
void rekeyFront(const Key& k) {
MOZ_ASSERT(valid());
Data& entry = ht->data[i];
HashNumber oldHash = prepareHash(Ops::getKey(entry.element)) >> ht->hashShift;
HashNumber newHash = prepareHash(k) >> ht->hashShift;
HashNumber oldHash = ht->prepareHash(Ops::getKey(entry.element)) >> ht->hashShift;
HashNumber newHash = ht->prepareHash(k) >> ht->hashShift;
Ops::setKey(entry.element, k);
if (newHash != oldHash) {
// Remove this entry from its old hash chain. (If this crashes
@ -558,10 +562,12 @@ class OrderedHashTable
*/
static double minDataFill() { return 0.25; }
static HashNumber prepareHash(const Lookup& l) {
return ScrambleHashCode(Ops::hash(l));
public:
HashNumber prepareHash(const Lookup& l) const {
return ScrambleHashCode(Ops::hash(l, hcs));
}
private:
/* The size of hashTable, in elements. Always a power of two. */
uint32_t hashBuckets() const {
return 1 << (HashNumberSizeBits - hashShift);
@ -740,7 +746,7 @@ class OrderedHashMap
public:
typedef typename Impl::Range Range;
explicit OrderedHashMap(AllocPolicy ap = AllocPolicy()) : impl(ap) {}
OrderedHashMap(AllocPolicy ap, mozilla::HashCodeScrambler hcs) : impl(ap, hcs) {}
MOZ_MUST_USE bool init() { return impl.init(); }
uint32_t count() const { return impl.count(); }
bool has(const Key& key) const { return impl.has(key); }
@ -755,6 +761,8 @@ class OrderedHashMap
return impl.put(Entry(key, Forward<V>(value)));
}
HashNumber hash(const Key& key) const { return impl.prepareHash(key); }
void rekeyOneEntry(const Key& current, const Key& newKey) {
const Entry* e = get(current);
if (!e)
@ -796,7 +804,7 @@ class OrderedHashSet
public:
typedef typename Impl::Range Range;
explicit OrderedHashSet(AllocPolicy ap = AllocPolicy()) : impl(ap) {}
explicit OrderedHashSet(AllocPolicy ap, mozilla::HashCodeScrambler hcs) : impl(ap, hcs) {}
MOZ_MUST_USE bool init() { return impl.init(); }
uint32_t count() const { return impl.count(); }
bool has(const T& value) const { return impl.has(value); }
@ -805,6 +813,8 @@ class OrderedHashSet
bool remove(const T& value, bool* foundp) { return impl.remove(value, foundp); }
MOZ_MUST_USE bool clear() { return impl.clear(); }
HashNumber hash(const T& value) const { return impl.prepareHash(value); }
void rekeyOneEntry(const T& current, const T& newKey) {
return impl.rekeyOneEntry(current, newKey, newKey);
}

Просмотреть файл

@ -141,7 +141,9 @@ namespace gc {
struct WeakKeyTableHashPolicy {
typedef JS::GCCellPtr Lookup;
static HashNumber hash(const Lookup& v) { return mozilla::HashGeneric(v.asCell()); }
static HashNumber hash(const Lookup& v, const mozilla::HashCodeScrambler&) {
return mozilla::HashGeneric(v.asCell());
}
static bool match(const JS::GCCellPtr& k, const Lookup& l) { return k == l; }
static bool isEmpty(const JS::GCCellPtr& v) { return !v; }
static void makeEmpty(JS::GCCellPtr* vp) { *vp = nullptr; }

Просмотреть файл

@ -31,6 +31,7 @@ JS::Zone::Zone(JSRuntime* rt)
types(this),
compartments(),
gcGrayRoots(),
gcWeakKeys(SystemAllocPolicy(), rt->randomHashCodeScrambler()),
typeDescrObjects(this, SystemAllocPolicy()),
gcMallocBytes(0),
gcMallocGCTriggered(false),

Просмотреть файл

@ -77,6 +77,7 @@ JSCompartment::JSCompartment(Zone* zone, const JS::CompartmentOptions& options =
nonSyntacticLexicalEnvironments_(nullptr),
gcIncomingGrayPointers(nullptr),
debugModeBits(0),
randomKeyGenerator_(runtime_->forkRandomKeyGenerator()),
watchpointMap(nullptr),
scriptCountsMap(nullptr),
debugScriptMap(nullptr),
@ -1277,6 +1278,13 @@ JSCompartment::addTelemetry(const char* filename, DeprecatedLanguageExtension e)
sawDeprecatedLanguageExtension[e] = true;
}
mozilla::HashCodeScrambler
JSCompartment::randomHashCodeScrambler()
{
return mozilla::HashCodeScrambler(randomKeyGenerator_.next(),
randomKeyGenerator_.next());
}
AutoSetNewObjectMetadata::AutoSetNewObjectMetadata(ExclusiveContext* ecx
MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
: CustomAutoRooter(ecx)

Просмотреть файл

@ -701,6 +701,12 @@ struct JSCompartment
// Initialize randomNumberGenerator if needed.
void ensureRandomNumberGenerator();
private:
mozilla::non_crypto::XorShift128PlusRNG randomKeyGenerator_;
public:
mozilla::HashCodeScrambler randomHashCodeScrambler();
static size_t offsetOfRegExps() {
return offsetof(JSCompartment, regExps);
}

Просмотреть файл

@ -246,6 +246,7 @@ using namespace js::gc;
using mozilla::ArrayLength;
using mozilla::Get;
using mozilla::HashCodeScrambler;
using mozilla::Maybe;
using mozilla::Swap;
@ -4163,7 +4164,7 @@ js::gc::MarkingValidator::nonIncrementalMark(AutoLockForExclusiveAccess& lock)
* For saving, smush all of the keys into one big table and split them back
* up into per-zone tables when restoring.
*/
gc::WeakKeyTable savedWeakKeys;
gc::WeakKeyTable savedWeakKeys(SystemAllocPolicy(), runtime->randomHashCodeScrambler());
if (!savedWeakKeys.init())
return;

Просмотреть файл

@ -776,6 +776,32 @@ JSRuntime::removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promis
PromiseRejectionHandlingState::Handled, data);
}
mozilla::non_crypto::XorShift128PlusRNG&
JSRuntime::randomKeyGenerator()
{
MOZ_ASSERT(CurrentThreadCanAccessRuntime(this));
if (randomKeyGenerator_.isNothing()) {
mozilla::Array<uint64_t, 2> seed;
GenerateXorShift128PlusSeed(seed);
randomKeyGenerator_.emplace(seed[0], seed[1]);
}
return randomKeyGenerator_.ref();
}
mozilla::HashCodeScrambler
JSRuntime::randomHashCodeScrambler()
{
auto& rng = randomKeyGenerator();
return mozilla::HashCodeScrambler(rng.next(), rng.next());
}
mozilla::non_crypto::XorShift128PlusRNG
JSRuntime::forkRandomKeyGenerator()
{
auto& rng = randomKeyGenerator();
return mozilla::non_crypto::XorShift128PlusRNG(rng.next(), rng.next());
}
void
JSRuntime::updateMallocCounter(size_t nbytes)
{

Просмотреть файл

@ -769,6 +769,15 @@ struct JSRuntime : public JS::shadow::Runtime,
void addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
void removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
private:
// Used to generate random keys for hash tables.
mozilla::Maybe<mozilla::non_crypto::XorShift128PlusRNG> randomKeyGenerator_;
mozilla::non_crypto::XorShift128PlusRNG& randomKeyGenerator();
public:
mozilla::HashCodeScrambler randomHashCodeScrambler();
mozilla::non_crypto::XorShift128PlusRNG forkRandomKeyGenerator();
//-------------------------------------------------------------------------
// Self-hosting support
//-------------------------------------------------------------------------

Просмотреть файл

@ -50,6 +50,7 @@
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/Char16.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Types.h"
#include <stdint.h>
@ -298,6 +299,90 @@ HashString(const wchar_t* aStr, size_t aLength)
MOZ_MUST_USE extern MFBT_API uint32_t
HashBytes(const void* bytes, size_t aLength);
/**
* A pseudorandom function mapping 32-bit integers to 32-bit integers.
*
* This is for when you're feeding private data (like pointer values or credit
* card numbers) to a non-crypto hash function (like HashBytes) and then using
* the hash code for something that untrusted parties could observe (like a JS
* Map). Plug in a HashCodeScrambler before that last step to avoid leaking the
* private data.
*
* By itself, this does not prevent hash-flooding DoS attacks, because an
* attacker can still generate many values with exactly equal hash codes by
* attacking the non-crypto hash function alone. Equal hash codes will, of
* course, still be equal however much you scramble them.
*
* The algorithm is SipHash-1-3. See <https://131002.net/siphash/>.
*/
class HashCodeScrambler
{
struct SipHasher;
uint64_t mK0, mK1;
public:
/** Creates a new scrambler with the given 128-bit key. */
constexpr HashCodeScrambler(uint64_t aK0, uint64_t aK1) : mK0(aK0), mK1(aK1) {}
/**
* Scramble a hash code. Always produces the same result for the same
* combination of key and hash code.
*/
uint32_t scramble(uint32_t aHashCode) const
{
SipHasher hasher(mK0, mK1);
return uint32_t(hasher.sipHash(aHashCode));
}
private:
struct SipHasher
{
SipHasher(uint64_t aK0, uint64_t aK1)
{
// 1. Initialization.
mV0 = aK0 ^ UINT64_C(0x736f6d6570736575);
mV1 = aK1 ^ UINT64_C(0x646f72616e646f6d);
mV2 = aK0 ^ UINT64_C(0x6c7967656e657261);
mV3 = aK1 ^ UINT64_C(0x7465646279746573);
}
uint64_t sipHash(uint64_t aM)
{
// 2. Compression.
mV3 ^= aM;
sipRound();
mV0 ^= aM;
// 3. Finalization.
mV2 ^= 0xff;
for (int i = 0; i < 3; i++)
sipRound();
return mV0 ^ mV1 ^ mV2 ^ mV3;
}
void sipRound()
{
mV0 += mV1;
mV1 = RotateLeft(mV1, 13);
mV1 ^= mV0;
mV0 = RotateLeft(mV0, 32);
mV2 += mV3;
mV3 = RotateLeft(mV3, 16);
mV3 ^= mV2;
mV0 += mV3;
mV3 = RotateLeft(mV3, 21);
mV3 ^= mV0;
mV2 += mV1;
mV1 = RotateLeft(mV1, 17);
mV1 ^= mV2;
mV2 = RotateLeft(mV2, 32);
}
uint64_t mV0, mV1, mV2, mV3;
};
};
} /* namespace mozilla */
#endif /* __cplusplus */