Bug 1312001 - Scramble hash codes securely, to avoid leaking bits of object and symbol addresses. r=jandem, a=lizzard
authorJason Orendorff <jorendorff@mozilla.com>
Wed, 30 Nov 2016 15:31:56 -0600
changeset 353561 a4996e963d0d8c5deb61df992e98a19dc393b0b5
parent 353560 9160ce16aa646ad59b53006e215cefd1e451a2dd
child 353562 98ecf719163d1c0cf19359186ab1cac6f34bb621
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem, lizzard
bugs1312001
milestone52.0a2
Bug 1312001 - Scramble hash codes securely, to avoid leaking bits of object and symbol addresses. r=jandem, a=lizzard 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;
@@ -145,11 +149,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.isNull() || !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());
 
@@ -379,17 +398,19 @@ MapObject::mark(JSTracer* trc, JSObject*
             MarkKey(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>
@@ -429,27 +450,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)
@@ -527,17 +549,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;
@@ -593,17 +616,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();
@@ -1115,17 +1138,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 mark(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
@@ -71,16 +71,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),
@@ -1280,16 +1281,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
@@ -240,16 +240,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;
 
@@ -4151,17 +4152,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
@@ -770,16 +770,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
@@ -760,16 +760,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 */