tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Compartment.cpp (19393B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
      2 * vim: set ts=8 sts=2 et sw=2 tw=80:
      3 * This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "vm/Compartment-inl.h"
      8 
      9 #include "mozilla/MemoryReporting.h"
     10 
     11 #include <stddef.h>
     12 
     13 #include "jsfriendapi.h"
     14 
     15 #include "debugger/DebugAPI.h"
     16 #include "gc/GC.h"
     17 #include "gc/Memory.h"
     18 #include "gc/PublicIterators.h"
     19 #include "gc/Zone.h"
     20 #include "js/friend/StackLimits.h"  // js::AutoCheckRecursionLimit
     21 #include "js/friend/WindowProxy.h"  // js::IsWindow, js::IsWindowProxy, js::ToWindowProxyIfWindow
     22 #include "js/Proxy.h"
     23 #include "js/RootingAPI.h"
     24 #include "js/StableStringChars.h"
     25 #include "js/Wrapper.h"
     26 #include "js/WrapperCallbacks.h"
     27 #include "proxy/DeadObjectProxy.h"
     28 #include "proxy/DOMProxy.h"
     29 #include "vm/JSContext.h"
     30 #include "vm/WrapperObject.h"
     31 
     32 #include "gc/Marking-inl.h"
     33 #include "gc/WeakMap-inl.h"
     34 #include "vm/JSObject-inl.h"
     35 #include "vm/Realm-inl.h"
     36 #include "vm/StringType-inl.h"
     37 
     38 using namespace js;
     39 
     40 using JS::AutoStableStringChars;
     41 
     42 Compartment::Compartment(Zone* zone, bool invisibleToDebugger)
     43    : zone_(zone),
     44      runtime_(zone->runtimeFromAnyThread()),
     45      invisibleToDebugger_(invisibleToDebugger),
     46      crossCompartmentObjectWrappers(zone, 0),
     47      realms_(zone) {}
     48 
     49 #ifdef JSGC_HASH_TABLE_CHECKS
     50 
     51 void Compartment::checkObjectWrappersAfterMovingGC() {
     52  for (ObjectWrapperEnum e(this); !e.empty(); e.popFront()) {
     53    auto key = e.front().key();
     54    CheckGCThingAfterMovingGC(key.get());  // Keys may be in a different zone.
     55    CheckGCThingAfterMovingGC(e.front().value().unbarrieredGet(), zone());
     56    CheckTableEntryAfterMovingGC(crossCompartmentObjectWrappers, e, key);
     57  }
     58 }
     59 
     60 #endif  // JSGC_HASH_TABLE_CHECKS
     61 
     62 bool Compartment::putWrapper(JSContext* cx, JSObject* wrapped,
     63                             JSObject* wrapper) {
     64  MOZ_ASSERT(!js::IsProxy(wrapper) || js::GetProxyHandler(wrapper)->family() !=
     65                                          js::GetDOMRemoteProxyHandlerFamily());
     66 
     67  if (!crossCompartmentObjectWrappers.put(wrapped, wrapper)) {
     68    ReportOutOfMemory(cx);
     69    return false;
     70  }
     71 
     72  return true;
     73 }
     74 
     75 bool Compartment::putWrapper(JSContext* cx, JSString* wrapped,
     76                             JSString* wrapper) {
     77  if (!zone()->crossZoneStringWrappers().put(wrapped, wrapper)) {
     78    ReportOutOfMemory(cx);
     79    return false;
     80  }
     81 
     82  return true;
     83 }
     84 
     85 void Compartment::removeWrapper(js::ObjectWrapperMap::Ptr p) {
     86  JSObject* key = p->key();
     87  JSObject* value = p->value().unbarrieredGet();
     88  if (js::gc::detail::GetDelegate(value) == key) {
     89    key->zone()->beforeClearDelegate(value, key);
     90  }
     91 
     92  crossCompartmentObjectWrappers.remove(p);
     93 }
     94 
     95 JSString* js::CopyStringPure(JSContext* cx, JSString* str) {
     96  /*
     97   * Directly allocate the copy in the destination compartment, rather than
     98   * first flattening it (and possibly allocating in source compartment),
     99   * because we don't know whether the flattening will pay off later.
    100   */
    101 
    102  size_t len = str->length();
    103  JSString* copy;
    104  if (str->isLinear()) {
    105    // If the string has a refcounted StringBuffer, we can share it.
    106    if (str->hasStringBuffer()) {
    107      RefPtr<mozilla::StringBuffer> buffer(str->asLinear().stringBuffer());
    108      if (str->hasLatin1Chars()) {
    109        Rooted<JSString::OwnedChars<Latin1Char>> owned(cx, std::move(buffer),
    110                                                       len);
    111        return JSLinearString::newValidLength<CanGC, Latin1Char>(
    112            cx, &owned, gc::Heap::Default);
    113      }
    114      Rooted<JSString::OwnedChars<char16_t>> owned(cx, std::move(buffer), len);
    115      return JSLinearString::newValidLength<CanGC, char16_t>(cx, &owned,
    116                                                             gc::Heap::Default);
    117    }
    118 
    119    /* Only use AutoStableStringChars if the NoGC allocation fails. */
    120    if (str->hasLatin1Chars()) {
    121      JS::AutoCheckCannotGC nogc;
    122      copy = NewStringCopyN<NoGC>(cx, str->asLinear().latin1Chars(nogc), len);
    123    } else {
    124      JS::AutoCheckCannotGC nogc;
    125      copy = NewStringCopyNDontDeflate<NoGC>(
    126          cx, str->asLinear().twoByteChars(nogc), len);
    127    }
    128    if (copy) {
    129      return copy;
    130    }
    131 
    132    AutoStableStringChars chars(cx);
    133    if (!chars.init(cx, str)) {
    134      return nullptr;
    135    }
    136 
    137    return chars.isLatin1() ? NewStringCopyN<CanGC>(
    138                                  cx, chars.latin1Range().begin().get(), len)
    139                            : NewStringCopyNDontDeflate<CanGC>(
    140                                  cx, chars.twoByteRange().begin().get(), len);
    141  }
    142 
    143  if (str->hasLatin1Chars()) {
    144    UniquePtr<Latin1Char[], JS::FreePolicy> copiedChars =
    145        str->asRope().copyLatin1Chars(cx, js::StringBufferArena);
    146    if (!copiedChars) {
    147      return nullptr;
    148    }
    149 
    150    return NewString<CanGC>(cx, std::move(copiedChars), len);
    151  }
    152 
    153  UniqueTwoByteChars copiedChars =
    154      str->asRope().copyTwoByteChars(cx, js::StringBufferArena);
    155  if (!copiedChars) {
    156    return nullptr;
    157  }
    158 
    159  return NewStringDontDeflate<CanGC>(cx, std::move(copiedChars), len);
    160 }
    161 
    162 bool Compartment::wrap(JSContext* cx, MutableHandleString strp) {
    163  MOZ_ASSERT(cx->compartment() == this);
    164 
    165  /* If the string is already in this compartment, we are done. */
    166  JSString* str = strp;
    167  if (str->zoneFromAnyThread() == zone()) {
    168    return true;
    169  }
    170 
    171  /*
    172   * If the string is an atom, we don't have to copy, but we do need to mark
    173   * the atom as being in use by the new zone.
    174   */
    175  if (str->isAtom()) {
    176    cx->markAtom(&str->asAtom());
    177    return true;
    178  }
    179 
    180  /* Check the cache. */
    181  if (StringWrapperMap::Ptr p = lookupWrapper(str)) {
    182    strp.set(p->value().get());
    183    return true;
    184  }
    185 
    186  /* No dice. Make a copy, and cache it. */
    187  JSString* copy = CopyStringPure(cx, str);
    188  if (!copy) {
    189    return false;
    190  }
    191  if (!putWrapper(cx, strp, copy)) {
    192    return false;
    193  }
    194 
    195  strp.set(copy);
    196  return true;
    197 }
    198 
    199 bool Compartment::wrap(JSContext* cx, MutableHandleBigInt bi) {
    200  MOZ_ASSERT(cx->compartment() == this);
    201 
    202  if (bi->zone() == cx->zone()) {
    203    return true;
    204  }
    205 
    206  BigInt* copy = BigInt::copy(cx, bi);
    207  if (!copy) {
    208    return false;
    209  }
    210  bi.set(copy);
    211  return true;
    212 }
    213 
    214 bool Compartment::getNonWrapperObjectForCurrentCompartment(
    215    JSContext* cx, HandleObject origObj, MutableHandleObject obj) {
    216  // Ensure that we have entered a realm.
    217  MOZ_ASSERT(cx->global());
    218 
    219  // The object is already in the right compartment. Normally same-
    220  // compartment returns the object itself, however, windows are always
    221  // wrapped by a proxy, so we have to check for that case here manually.
    222  if (obj->compartment() == this) {
    223    obj.set(ToWindowProxyIfWindow(obj));
    224    return true;
    225  }
    226 
    227  // Note that if the object is same-compartment, but has been wrapped into a
    228  // different compartment, we need to unwrap it and return the bare same-
    229  // compartment object. Note again that windows are always wrapped by a
    230  // WindowProxy even when same-compartment so take care not to strip this
    231  // particular wrapper.
    232  RootedObject objectPassedToWrap(cx, obj);
    233  obj.set(UncheckedUnwrap(obj, /* stopAtWindowProxy = */ true));
    234  if (obj->compartment() == this) {
    235    MOZ_ASSERT(!IsWindow(obj));
    236    return true;
    237  }
    238 
    239  // Disallow creating new wrappers if we nuked the object's realm or the
    240  // current compartment.
    241  if (!AllowNewWrapper(this, obj)) {
    242    obj.set(NewDeadProxyObject(cx, obj));
    243    return !!obj;
    244  }
    245 
    246  // Use the WindowProxy instead of the Window here, so that we don't have to
    247  // deal with this in the rest of the wrapping code.
    248  if (IsWindow(obj)) {
    249    obj.set(ToWindowProxyIfWindow(obj));
    250 
    251    // ToWindowProxyIfWindow can return a CCW if |obj| was a navigated-away-from
    252    // Window. Strip any CCWs.
    253    obj.set(UncheckedUnwrap(obj));
    254 
    255    if (JS_IsDeadWrapper(obj)) {
    256      obj.set(NewDeadProxyObject(cx, obj));
    257      return !!obj;
    258    }
    259 
    260    MOZ_ASSERT(IsWindowProxy(obj) || IsDOMRemoteProxyObject(obj));
    261 
    262    // We crossed a compartment boundary there, so may now have a gray object.
    263    // This function is not allowed to return gray objects, so don't do that.
    264    ExposeObjectToActiveJS(obj);
    265  }
    266 
    267  // If the object is a dead wrapper, return a new dead wrapper rather than
    268  // trying to wrap it for a different compartment.
    269  if (JS_IsDeadWrapper(obj)) {
    270    obj.set(NewDeadProxyObject(cx, obj));
    271    return !!obj;
    272  }
    273 
    274  // Invoke the prewrap callback. The prewrap callback is responsible for
    275  // doing similar reification as above, but can account for any additional
    276  // embedder requirements.
    277  //
    278  // We're a bit worried about infinite recursion here, so we do a check -
    279  // see bug 809295.
    280  auto preWrap = cx->runtime()->wrapObjectCallbacks->preWrap;
    281  if (preWrap) {
    282    AutoCheckRecursionLimit recursion(cx);
    283    if (!recursion.checkSystem(cx)) {
    284      return false;
    285    }
    286    preWrap(cx, cx->global(), origObj, obj, objectPassedToWrap, obj);
    287    if (!obj) {
    288      return false;
    289    }
    290  }
    291  MOZ_ASSERT(!IsWindow(obj));
    292 
    293  return true;
    294 }
    295 
    296 bool Compartment::getOrCreateWrapper(JSContext* cx, HandleObject existing,
    297                                     MutableHandleObject obj) {
    298  // ScriptSourceObject is an internal object that we never need to wrap.
    299  MOZ_ASSERT(!obj->is<ScriptSourceObject>());
    300 
    301  // If we already have a wrapper for this value, use it.
    302  if (ObjectWrapperMap::Ptr p = lookupWrapper(obj)) {
    303    obj.set(p->value().get());
    304    MOZ_ASSERT(obj->is<CrossCompartmentWrapperObject>());
    305    return true;
    306  }
    307 
    308  // Ensure that the wrappee is exposed in case we are creating a new wrapper
    309  // for a gray object.
    310  ExposeObjectToActiveJS(obj);
    311 
    312  // If we're wrapping an object which emulates undefined then the runtime fuse
    313  // should already have been popped.
    314  MOZ_ASSERT_IF(obj->getClass()->emulatesUndefined(),
    315                !cx->runtime()
    316                     ->runtimeFuses.ref()
    317                     .hasSeenObjectEmulateUndefinedFuse.intact());
    318 
    319  // Create a new wrapper for the object.
    320  auto wrap = cx->runtime()->wrapObjectCallbacks->wrap;
    321  RootedObject wrapper(cx, wrap(cx, existing, obj));
    322  if (!wrapper) {
    323    return false;
    324  }
    325 
    326  // We maintain the invariant that the key in the cross-compartment wrapper
    327  // map is always directly wrapped by the value.
    328  MOZ_ASSERT(Wrapper::wrappedObject(wrapper) == obj);
    329 
    330  if (!putWrapper(cx, obj, wrapper)) {
    331    // Enforce the invariant that all cross-compartment wrapper object are
    332    // in the map by nuking the wrapper if we couldn't add it.
    333    // Unfortunately it's possible for the wrapper to still be marked if we
    334    // took this path, for example if the object metadata callback stashes a
    335    // reference to it.
    336    if (wrapper->is<CrossCompartmentWrapperObject>()) {
    337      NukeCrossCompartmentWrapper(cx, wrapper);
    338    }
    339    return false;
    340  }
    341 
    342  obj.set(wrapper);
    343  return true;
    344 }
    345 
    346 bool Compartment::wrap(JSContext* cx, MutableHandleObject obj) {
    347  MOZ_ASSERT(cx->compartment() == this);
    348 
    349  if (!obj) {
    350    return true;
    351  }
    352 
    353  AutoDisableProxyCheck adpc;
    354 
    355  // Anything we're wrapping has already escaped into script, so must have
    356  // been unmarked-gray at some point in the past.
    357  JS::AssertObjectIsNotGray(obj);
    358 
    359  // The passed object may already be wrapped, or may fit a number of special
    360  // cases that we need to check for and manually correct.
    361  if (!getNonWrapperObjectForCurrentCompartment(cx, /* origObj = */ nullptr,
    362                                                obj)) {
    363    return false;
    364  }
    365 
    366  // If the reification above did not result in a same-compartment object,
    367  // get or create a new wrapper object in this compartment for it.
    368  if (obj->compartment() != this) {
    369    if (!getOrCreateWrapper(cx, nullptr, obj)) {
    370      return false;
    371    }
    372  }
    373 
    374  // Ensure that the wrapper is also exposed.
    375  ExposeObjectToActiveJS(obj);
    376  return true;
    377 }
    378 
    379 bool Compartment::rewrap(JSContext* cx, MutableHandleObject obj,
    380                         HandleObject existingArg) {
    381  MOZ_ASSERT(cx->compartment() == this);
    382  MOZ_ASSERT(obj);
    383  MOZ_ASSERT(existingArg);
    384  MOZ_ASSERT(existingArg->compartment() == cx->compartment());
    385  MOZ_ASSERT(IsDeadProxyObject(existingArg));
    386 
    387  AutoDisableProxyCheck adpc;
    388 
    389  // It may not be possible to re-use existing; if so, clear it so that we
    390  // are forced to create a new wrapper. Note that this cannot call out to
    391  // |wrap| because of the different gray unmarking semantics.
    392  RootedObject existing(cx, existingArg);
    393  if (existing->hasStaticPrototype() ||
    394      // Note: Class asserted above, so all that's left to check is callability
    395      existing->isCallable() || obj->isCallable()) {
    396    existing.set(nullptr);
    397  }
    398 
    399  // The passed object may already be wrapped, or may fit a number of special
    400  // cases that we need to check for and manually correct. We pass in
    401  // |existingArg| instead of |existing|, because the purpose is to get the
    402  // address of the object we are transplanting onto, not to find a wrapper
    403  // to reuse.
    404  if (!getNonWrapperObjectForCurrentCompartment(cx, existingArg, obj)) {
    405    return false;
    406  }
    407 
    408  // If the reification above resulted in a same-compartment object, we do
    409  // not need to create or return an existing wrapper.
    410  if (obj->compartment() == this) {
    411    return true;
    412  }
    413 
    414  return getOrCreateWrapper(cx, existing, obj);
    415 }
    416 
    417 bool Compartment::wrap(JSContext* cx,
    418                       MutableHandle<JS::PropertyDescriptor> desc) {
    419  if (desc.hasGetter()) {
    420    if (!wrap(cx, desc.getter())) {
    421      return false;
    422    }
    423  }
    424  if (desc.hasSetter()) {
    425    if (!wrap(cx, desc.setter())) {
    426      return false;
    427    }
    428  }
    429  if (desc.hasValue()) {
    430    if (!wrap(cx, desc.value())) {
    431      return false;
    432    }
    433  }
    434  return true;
    435 }
    436 
    437 bool Compartment::wrap(JSContext* cx,
    438                       MutableHandle<mozilla::Maybe<PropertyDescriptor>> desc) {
    439  if (desc.isNothing()) {
    440    return true;
    441  }
    442 
    443  Rooted<PropertyDescriptor> desc_(cx, *desc);
    444  if (!wrap(cx, &desc_)) {
    445    return false;
    446  }
    447  desc.set(mozilla::Some(desc_.get()));
    448  return true;
    449 }
    450 
    451 bool Compartment::wrap(JSContext* cx, MutableHandle<GCVector<Value>> vec) {
    452  for (size_t i = 0; i < vec.length(); ++i) {
    453    if (!wrap(cx, vec[i])) {
    454      return false;
    455    }
    456  }
    457  return true;
    458 }
    459 
    460 static inline bool ShouldTraceWrapper(JSObject* wrapper,
    461                                      Compartment::EdgeSelector whichEdges) {
    462  switch (whichEdges) {
    463    case Compartment::AllEdges:
    464      return true;
    465    case Compartment::NonGrayEdges:
    466      return !wrapper->isMarkedGray();
    467    case Compartment::GrayEdges:
    468      return wrapper->isMarkedGray();
    469    case Compartment::BlackEdges:
    470      return wrapper->isMarkedBlack();
    471    default:
    472      MOZ_CRASH("Unexpected EdgeSelector value");
    473  }
    474 }
    475 
    476 void Compartment::traceWrapperTargetsInCollectedZones(JSTracer* trc,
    477                                                      EdgeSelector whichEdges) {
    478  // Trace cross compartment wrapper private pointers into collected zones to
    479  // either mark or update them. Wrapped object pointers are updated by
    480  // sweepCrossCompartmentObjectWrappers().
    481 
    482  MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting());
    483  MOZ_ASSERT(!zone()->isCollectingFromAnyThread() ||
    484             trc->runtime()->gc.isHeapCompacting());
    485 
    486  for (WrappedObjectCompartmentEnum c(this); !c.empty(); c.popFront()) {
    487    Zone* zone = c.front()->zone();
    488    if (!zone->isCollectingFromAnyThread()) {
    489      continue;
    490    }
    491 
    492    for (ObjectWrapperEnum e(this, c); !e.empty(); e.popFront()) {
    493      JSObject* obj = e.front().value().unbarrieredGet();
    494      ProxyObject* wrapper = &obj->as<ProxyObject>();
    495      if (ShouldTraceWrapper(wrapper, whichEdges)) {
    496        ProxyObject::traceEdgeToTarget(trc, wrapper);
    497      }
    498    }
    499  }
    500 }
    501 
    502 /* static */
    503 void Compartment::traceIncomingCrossCompartmentEdgesForZoneGC(
    504    JSTracer* trc, EdgeSelector whichEdges) {
    505  MOZ_ASSERT(JS::RuntimeHeapIsMajorCollecting());
    506 
    507  for (ZonesIter zone(trc->runtime(), SkipAtoms); !zone.done(); zone.next()) {
    508    if (zone->isCollectingFromAnyThread()) {
    509      continue;
    510    }
    511 
    512    for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) {
    513      c->traceWrapperTargetsInCollectedZones(trc, whichEdges);
    514    }
    515  }
    516 
    517  // Currently we trace all debugger edges as black.
    518  if (whichEdges != GrayEdges) {
    519    DebugAPI::traceCrossCompartmentEdges(trc);
    520  }
    521 }
    522 
    523 void Compartment::sweepAfterMinorGC(JSTracer* trc) {
    524  crossCompartmentObjectWrappers.sweepAfterMinorGC(trc);
    525 
    526  for (RealmsInCompartmentIter r(this); !r.done(); r.next()) {
    527    r->sweepAfterMinorGC(trc);
    528  }
    529 }
    530 
    531 // Remove dead wrappers from the table or update pointers to moved objects.
    532 void Compartment::traceCrossCompartmentObjectWrapperEdges(JSTracer* trc) {
    533  crossCompartmentObjectWrappers.traceWeak(trc);
    534 }
    535 
    536 void Compartment::fixupCrossCompartmentObjectWrappersAfterMovingGC(
    537    JSTracer* trc) {
    538  MOZ_ASSERT(trc->runtime()->gc.isHeapCompacting());
    539 
    540  // Sweep the wrapper map to update keys (wrapped values) in other
    541  // compartments that may have been moved.
    542  traceCrossCompartmentObjectWrapperEdges(trc);
    543 
    544  // Trace the wrappers in the map to update their cross-compartment edges
    545  // to wrapped values in other compartments that may have been moved.
    546  traceWrapperTargetsInCollectedZones(trc, AllEdges);
    547 }
    548 
    549 void Compartment::fixupAfterMovingGC(JSTracer* trc) {
    550  MOZ_ASSERT(zone()->isGCCompacting());
    551 
    552  for (RealmsInCompartmentIter r(this); !r.done(); r.next()) {
    553    r->fixupAfterMovingGC(trc);
    554  }
    555 
    556  // Sweep the wrapper map to update values (wrapper objects) in this
    557  // compartment that may have been moved.
    558  traceCrossCompartmentObjectWrapperEdges(trc);
    559 }
    560 
    561 void Compartment::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
    562                                         size_t* compartmentObjects,
    563                                         size_t* crossCompartmentWrappersTables,
    564                                         size_t* compartmentsPrivateData) {
    565  *compartmentObjects += mallocSizeOf(this);
    566  *crossCompartmentWrappersTables +=
    567      crossCompartmentObjectWrappers.sizeOfExcludingThis(mallocSizeOf);
    568 
    569  if (auto callback = runtime_->sizeOfIncludingThisCompartmentCallback) {
    570    *compartmentsPrivateData += callback(mallocSizeOf, this);
    571  }
    572 }
    573 
    574 GlobalObject& Compartment::firstGlobal() const {
    575  for (Realm* realm : realms_) {
    576    if (!realm->hasInitializedGlobal()) {
    577      continue;
    578    }
    579    GlobalObject* global = realm->maybeGlobal();
    580    ExposeObjectToActiveJS(global);
    581    return *global;
    582  }
    583  MOZ_CRASH("If all our globals are dead, why is someone expecting a global?");
    584 }
    585 
    586 JS_PUBLIC_API JSObject* js::GetFirstGlobalInCompartment(JS::Compartment* comp) {
    587  return &comp->firstGlobal();
    588 }
    589 
    590 JS_PUBLIC_API bool js::CompartmentHasLiveGlobal(JS::Compartment* comp) {
    591  MOZ_ASSERT(comp);
    592  for (Realm* r : comp->realms()) {
    593    if (r->hasLiveGlobal()) {
    594      return true;
    595    }
    596  }
    597  return false;
    598 }
    599 
    600 void Compartment::traceWeakNativeIterators(JSTracer* trc) {
    601  /* Sweep list of native iterators. */
    602  NativeIteratorListIter iter(&enumerators_);
    603  while (!iter.done()) {
    604    NativeIterator* ni = iter.next();
    605    JSObject* iterObj = ni->iterObj();
    606    if (!TraceManuallyBarrieredWeakEdge(trc, &iterObj,
    607                                        "Compartment::enumerators_")) {
    608      ni->unlink();
    609    }
    610    MOZ_ASSERT(ni->objectBeingIterated()->compartment() == this);
    611  }
    612 }