Bug 1312001 - Scramble hash codes securely, to avoid leaking bits of object and symbol addresses.
authorJason Orendorff <jorendorff@mozilla.com>
Wed, 30 Nov 2016 15:31:56 -0600
changeset 328161 c484c1e7eeb61f4abd6d9e2352eacd52b1a47cbf
parent 328160 ccc536cc6d4dc41f4a568ad36d44908b453ff26e
child 328162 2b8dc663bfe0b594d1acbb1844a6aa4314ce4d52
push id31165
push userihsiao@mozilla.com
push dateFri, 06 Jan 2017 16:00:03 +0000
treeherdermozilla-central@c5619045b65d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1312001
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1312001 - Scramble hash codes securely, to avoid leaking bits of object and symbol addresses. MozReview-Commit-ID: yR1cIjrlPP
js/src/NamespaceImports.h
js/src/builtin/MapObject.cpp
js/src/builtin/MapObject.h
js/src/ds/OrderedHashTable.h
js/src/gc/Marking.h
js/src/gc/Zone.cpp
js/src/jscompartment.cpp
js/src/jscompartment.h
js/src/jsgc.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
mfbt/HashFunctions.h
--- a/js/src/NamespaceImports.h
+++ b/js/src/NamespaceImports.h
@@ -41,16 +41,20 @@ using ScriptVector = JS::GCVector<JSScri
 template<typename K, typename V> class AutoHashMapRooter;
 template<typename T> class AutoHashSetRooter;
 
 class MOZ_STACK_CLASS SourceBufferHolder;
 
 class HandleValueArray;
 
 class ObjectOpResult;
+
+class Symbol;
+enum class SymbolCode: uint32_t;
+
 } // namespace JS
 
 // Do the importing.
 namespace js {
 
 using JS::Value;
 using JS::BooleanValue;
 using JS::DoubleValue;
@@ -149,11 +153,14 @@ using JS::TrueHandleValue;
 using JS::FalseHandleValue;
 
 using JS::HandleValueArray;
 
 using JS::ObjectOpResult;
 
 using JS::Zone;
 
+using JS::Symbol;
+using JS::SymbolCode;
+
 } /* namespace js */
 
 #endif /* NamespaceImports_h */
--- a/js/src/builtin/MapObject.cpp
+++ b/js/src/builtin/MapObject.cpp
@@ -11,16 +11,17 @@
 #include "jsobj.h"
 
 #include "ds/OrderedHashTable.h"
 #include "gc/Marking.h"
 #include "js/Utility.h"
 #include "vm/GlobalObject.h"
 #include "vm/Interpreter.h"
 #include "vm/SelfHosting.h"
+#include "vm/Symbol.h"
 
 #include "jsobjinlines.h"
 
 #include "vm/Interpreter-inl.h"
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
@@ -60,30 +61,48 @@ HashableValue::setValue(JSContext* cx, H
     }
 
     MOZ_ASSERT(value.isUndefined() || value.isNull() || value.isBoolean() || value.isNumber() ||
                value.isString() || value.isSymbol() || value.isObject());
     return true;
 }
 
 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
 HashableValue::operator==(const HashableValue& other) const
 {
     // Two HashableValues are equal if they have equal bits.
     bool b = (value.asRawBits() == other.value.asRawBits());
 
@@ -360,17 +379,19 @@ MapObject::trace(JSTracer* trc, JSObject
             TraceKey(r, r.front().key, trc);
             TraceEdge(trc, &r.front().value, "value");
         }
     }
 }
 
 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); }
 };
 
 using NurseryKeysVector = Vector<JSObject*, 0, SystemAllocPolicy>;
 
 template <typename TableObject>
@@ -410,27 +431,28 @@ template <typename ObjectT>
 class js::OrderedHashTableRef : public gc::BufferableRef
 {
     ObjectT* object;
 
   public:
     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);
     }
 };
 
 template <typename ObjectT>
 inline static MOZ_MUST_USE bool
 WriteBarrierPostImpl(JSRuntime* rt, ObjectT* obj, const Value& keyValue)
@@ -508,17 +530,18 @@ MapObject::set(JSContext* cx, HandleObje
     }
 
     return true;
 }
 
 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;
     }
 
     MapObject* mapObj = NewObjectWithClassProto<MapObject>(cx,  proto);
     if (!mapObj)
         return nullptr;
@@ -574,17 +597,17 @@ MapObject::is(HandleValue v)
 
 bool
 MapObject::is(HandleObject o)
 {
     return o->hasClass(&class_) && o->as<MapObject>().getPrivate();
 }
 
 #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
 
 ValueMap&
 MapObject::extract(HandleObject o)
 {
     MOZ_ASSERT(o->hasClass(&MapObject::class_));
     return *o->as<MapObject>().getData();
@@ -1089,17 +1112,18 @@ SetObject::add(JSContext* cx, HandleObje
         return false;
     }
     return true;
 }
 
 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;
     }
 
     SetObject* obj = NewObjectWithClassProto<SetObject>(cx, proto);
     if (!obj)
         return nullptr;
--- a/js/src/builtin/MapObject.h
+++ b/js/src/builtin/MapObject.h
@@ -27,26 +27,28 @@ namespace js {
  */
 class HashableValue
 {
     PreBarrieredValue value;
 
   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); }
     };
 
     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(); }
 
     void trace(JSTracer* trc) {
         TraceEdge(trc, &value, "HashableValue");
     }
 };
--- a/js/src/ds/OrderedHashTable.h
+++ b/js/src/ds/OrderedHashTable.h
@@ -24,22 +24,25 @@
  *   - The API is a little different, so it's not a drop-in replacement.
  *     In particular, the hash policy is a little different.
  *     Also, the Ordered templates lack the Ptr and AddPtr types.
  *
  * Hash policies
  *
  * 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;
 using mozilla::Move;
 
 namespace js {
 
 namespace detail {
@@ -73,20 +76,21 @@ class OrderedHashTable
     Data* data;                 // data vector, an array of Data objects
                                 // data[0:dataLength] are constructed
     uint32_t dataLength;        // number of constructed elements in data
     uint32_t dataCapacity;      // size of data, in elements
     uint32_t liveCount;         // dataLength less empty (removed) entries
     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");
 
         uint32_t buckets = initialBuckets();
         Data** tableAlloc = alloc.template pod_malloc<Data*>(buckets);
         if (!tableAlloc)
             return false;
@@ -427,18 +431,18 @@ class OrderedHashTable
          *
          * This calls Ops::hash on both the current key and the new key.
          * Ops::hash on the current key must return the same hash code as
          * when the entry was added to the table.
          */
         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
                 // reading nullptr, it would mean we did not find this entry on
                 // the hash chain where we expected it. That probably means the
                 // key's hash code changed since it was inserted, breaking the
                 // hash code invariant.)
                 Data** ep = &ht->hashTable[oldHash];
@@ -553,20 +557,22 @@ class OrderedHashTable
     static double fillFactor() { return 8.0 / 3.0; }
 
     /*
      * The minimum permitted value of (liveCount / dataLength).
      * If that ratio drops below this value, we shrink the table.
      */
     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);
     }
 
     static void destroyData(Data* data, uint32_t length) {
         for (Data* p = data + length; p != data; )
             (--p)->~Data();
@@ -735,31 +741,33 @@ class OrderedHashMap
     };
 
     typedef detail::OrderedHashTable<Entry, MapOps, AllocPolicy> Impl;
     Impl impl;
 
   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); }
     Range all()                                     { return impl.all(); }
     const Entry* get(const Key& key) const          { return impl.get(key); }
     Entry* get(const Key& key)                      { return impl.get(key); }
     bool remove(const Key& key, bool* foundp)       { return impl.remove(key, foundp); }
     MOZ_MUST_USE bool clear()                       { return impl.clear(); }
 
     template <typename V>
     MOZ_MUST_USE bool put(const Key& key, V&& value) {
         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)
             return;
         return impl.rekeyOneEntry(current, newKey, Entry(newKey, e->value));
     }
 
     static size_t offsetOfEntryKey() {
@@ -791,25 +799,27 @@ class OrderedHashSet
     };
 
     typedef detail::OrderedHashTable<T, SetOps, AllocPolicy> Impl;
     Impl impl;
 
   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); }
     Range all()                                     { return impl.all(); }
     MOZ_MUST_USE bool put(const T& value)           { return impl.put(value); }
     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);
     }
 
     static size_t offsetOfEntryKey() {
         return 0;
     }
     static size_t offsetOfImplDataLength() {
--- a/js/src/gc/Marking.h
+++ b/js/src/gc/Marking.h
@@ -136,17 +136,19 @@ class MarkStack
 
     size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
 };
 
 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; }
 };
 
 struct WeakMarkable {
     WeakMapBase* weakmap;
     JS::GCCellPtr key;
--- a/js/src/gc/Zone.cpp
+++ b/js/src/gc/Zone.cpp
@@ -26,16 +26,17 @@ Zone * const Zone::NotOnList = reinterpr
 JS::Zone::Zone(JSRuntime* rt)
   : JS::shadow::Zone(rt, &rt->gc.marker),
     debuggers(nullptr),
     suppressAllocationMetadataBuilder(false),
     arenas(rt),
     types(this),
     compartments(),
     gcGrayRoots(),
+    gcWeakKeys(SystemAllocPolicy(), rt->randomHashCodeScrambler()),
     typeDescrObjects(this, SystemAllocPolicy()),
     gcMallocBytes(0),
     gcMallocGCTriggered(false),
     usage(&rt->gc.usage),
     gcDelayBytes(0),
     propertyTree(this),
     baseShapes(this, BaseShapeSet()),
     initialShapes(this, InitialShapeSet()),
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -72,16 +72,17 @@ JSCompartment::JSCompartment(Zone* zone,
     selfHostingScriptSource(nullptr),
     objectMetadataTable(nullptr),
     innerViews(zone, InnerViewTable()),
     lazyArrayBuffers(nullptr),
     wasm(zone),
     nonSyntacticLexicalEnvironments_(nullptr),
     gcIncomingGrayPointers(nullptr),
     debugModeBits(0),
+    randomKeyGenerator_(runtime_->forkRandomKeyGenerator()),
     watchpointMap(nullptr),
     scriptCountsMap(nullptr),
     debugScriptMap(nullptr),
     debugEnvs(nullptr),
     enumerators(nullptr),
     lastCachedNativeIterator(nullptr),
     compartmentStats_(nullptr),
     scheduledForDestruction(false),
@@ -1272,16 +1273,23 @@ JSCompartment::addTelemetry(const char* 
     if (isSystem_)
         return;
     if (!creationOptions_.addonIdOrNull() && (!filename || strncmp(filename, "http", 4) != 0))
         return;
 
     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)
     , cx_(ecx->maybeJSContext())
     , prevState_(ecx->compartment()->objectMetadataState)
 {
     MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     if (cx_)
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -696,16 +696,22 @@ struct JSCompartment
     js::DtoaCache dtoaCache;
 
     // Random number generator for Math.random().
     mozilla::Maybe<mozilla::non_crypto::XorShift128PlusRNG> randomNumberGenerator;
 
     // Initialize randomNumberGenerator if needed.
     void ensureRandomNumberGenerator();
 
+  private:
+    mozilla::non_crypto::XorShift128PlusRNG randomKeyGenerator_;
+
+  public:
+    mozilla::HashCodeScrambler randomHashCodeScrambler();
+
     static size_t offsetOfRegExps() {
         return offsetof(JSCompartment, regExps);
     }
 
   private:
     JSCompartment* thisForCtor() { return this; }
 
   public:
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -241,16 +241,17 @@
 #include "vm/Stack-inl.h"
 #include "vm/String-inl.h"
 
 using namespace js;
 using namespace js::gc;
 
 using mozilla::ArrayLength;
 using mozilla::Get;
+using mozilla::HashCodeScrambler;
 using mozilla::Maybe;
 using mozilla::Swap;
 
 using JS::AutoGCRooter;
 
 /* Increase the IGC marking slice time if we are in highFrequencyGC mode. */
 static const int IGC_MARK_SLICE_MULTIPLIER = 2;
 
@@ -4158,17 +4159,17 @@ js::gc::MarkingValidator::nonIncremental
     WeakMapSet markedWeakMaps;
     if (!markedWeakMaps.init())
         return;
 
     /*
      * 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;
 
     for (GCZonesIter zone(runtime); !zone.done(); zone.next()) {
         if (!WeakMapBase::saveZoneMarkedWeakMaps(zone, markedWeakMaps))
             return;
 
         AutoEnterOOMUnsafeRegion oomUnsafe;
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -771,16 +771,42 @@ JSRuntime::removeUnhandledRejectedPromis
     if (!cx->runtime()->promiseRejectionTrackerCallback)
         return;
 
     void* data = cx->runtime()->promiseRejectionTrackerCallbackData;
     cx->runtime()->promiseRejectionTrackerCallback(cx, promise,
                                                    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)
 {
     updateMallocCounter(nullptr, nbytes);
 }
 
 void
 JSRuntime::updateMallocCounter(JS::Zone* zone, size_t nbytes)
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -764,16 +764,25 @@ struct JSRuntime : public JS::shadow::Ru
     inline JSContext* contextFromMainThread();
 
     JSObject* getIncumbentGlobal(JSContext* cx);
     bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job, js::HandleObject promise,
                            js::HandleObject incumbentGlobal);
     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
     //-------------------------------------------------------------------------
 
     bool hasInitializedSelfHosting() const {
         return selfHostingGlobal_;
     }
 
--- a/mfbt/HashFunctions.h
+++ b/mfbt/HashFunctions.h
@@ -45,16 +45,17 @@
  */
 
 #ifndef mozilla_HashFunctions_h
 #define mozilla_HashFunctions_h
 
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Char16.h"
+#include "mozilla/MathAlgorithms.h"
 #include "mozilla/Types.h"
 
 #include <stdint.h>
 
 #ifdef __cplusplus
 namespace mozilla {
 
 /**
@@ -293,12 +294,96 @@ HashString(const wchar_t* aStr, size_t a
  * Hash some number of bytes.
  *
  * This hash walks word-by-word, rather than byte-by-byte, so you won't get the
  * same result out of HashBytes as you would out of HashString.
  */
 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 */
 
 #endif /* mozilla_HashFunctions_h */